数据库连接池实现

2018/03/02 10:52
阅读数 19

C#数据库连接池

使用C#数据库连接池

连接到数据库服务器通常由几个需要软长时间的步骤组成。必须建立物理通道(例如套接字或命名管道),必须与服务器进行初次连接,必须分析连接字符串信息,必须由服务器对连接进行身份验证,等等。

实际上,大部份的应用程序都是使用一个或几个不同的连接配置。当应用程序的数据量和访问量大的时候,这意味着在运行应用程序的过程中,许多相同的连接将反复地被打开和关闭,从而会引起数据库服务器效率低下甚至引发程序崩溃。为了确保应用程序的稳定和降低性能成本,我们可以在ADO.NET中使用称为连接池的优化方法来管理维护连接。

C#数据库连接池可以减少创建连接的次数。定义最小连接数(固定连接数),当用户在连接上调用Open,连接池就会检查池中是否有可用的连接。如果发现有连接可用,会将该连接返回给调用者,而不是创建新连接。应用程序在该连接上调用Close时,连接池会判断该连接是否在最小连接数之内,如果“是”会将连接回收到活动连接池中而不是真正关闭连接,否则将烧毁连接。连接返回到池中之后,即可在下一个Open调用中重复使用。

创建C#数据库连接池

以下示例使用C#连接SQL数据库:

public class DbConn
        {
            //usingSystem.Data;
            //usingSystem.Data.SqlClient;
            private const int MaxPool = 10; //最大连接数
            private const int MinPool = 5; //最小连接数
            private const bool Asyn_Process = true;
            //设置异步访问数据库
            //在单个连接上得到和管理多个、仅向前引用和只读的结果集(ADO.NET2.0)
            private const bool Mars = true;
            private const int Conn_Timeout = 15; //设置连接等待时间
            private const int Conn_Lifetime = 15; //设置连接的生命周期
            private string ConnString = ""; //连接字符串
            private SqlConnection SqlDrConn = null;
            //连接对象
            public DbConn() //构造函数
            {
                ConnString = GetConnString();
                SqlDrConn = new SqlConnection(ConnString);
            }

            private string GetConnString()
            {
                return "server=localhost;" + "integratedsecurity=sspi;" + "database=pubs;" + "MaxPoolSize=" + MaxPool +
                       ";" + "MinPoolSize=" + MinPool + ";" + "ConnectTimeout=" + Conn_Timeout + ";" +
                       "ConnectionLifetime=" + Conn_Lifetime + ";" + "AsynchronousProcessing=" + Asyn_Process + ";";
                    //+"MultipleActiveResultSets="+Mars+";";}
                public
                DataTable GetDataReader(string StrSql)
                //数据查询
                {
                    //当连接处于打开状态时关闭,然后再打开,避免有时候数据不能及时更新
                    if (SqlDrConn.State == ConnectionState.Open)
                    {
                        SqlDrConn.Close();
                    }
                    try
                    {
                        SqlDrConn.Open();
                        SqlCommandSql Cmd = new SqlCommand(StrSql, SqlDrConn);
                        SqlDataReader SqlDr = SqlCmd.ExecuteReader();
                        if (SqlDr.HasRows)
                        {
                            DataTable dt = new DataTable();
                            //读取SqlDataReader里的内容
                            dt.Load(SqlDr);
                            //关闭对象和连接
                            SqlDr.Close();
                            SqlDrConn.Close();
                            return dt;
                        }
                        return null;
                    }
                    catch (Exception ex)
                    {
                        System.Windows.Forms.MessageBox.Show(ex.Message);
                        return null;
                    }
                    finally
                    {
                        SqlDrConn.Close();
                    }
                }
            }
        }

  

通过调用SqlDrConn.Open()方法打开连接,这时候连接池就会初始化并建立设定的最小连接数。想更清楚了解到连接池的状况可以通过SQL的查询分析器执行存储过程sp_Who,它会列出当前的数据库进程,查看loginname、dbname可以区分用户的连接信息,但要注意的是登录查询分析器本身会使用两个连接,所以最好用另一个用户名登录查询分析器。使用此方法还有一个麻烦地方就是要经常按“执行查询”以更新进程信息。还有另一种方法个人认为较好的,通过控制面板→管理工具→性能,右击添加计算器,性能对象选择SQlServer:GeneralStatistics(常规统计)然后计算器选择UserConnections(用户连接)最后按“添加”就可以实时查看当前连接数。

到了这里,连接池已经实现了,但问题往往会出现在运行过程中。如连接池的连接数满了该怎样处理?在这里我们应该合理设置连接字符串中的ConnectTimeout属性和ConnectionLifetime属性(上面有解释)延长等待时间,尽可能地在每次使用完连接之后调用Close方法关闭连接。但从中也有没法避免的,当连接数满了并且申请连接的时间超过设置连接等待的时间时,程序将会引发InvalidOperationExceptio异常,我们可以通过捕获此异常向用户界面提示“系统正忙,请稍后再连接……”之类的信息来缓解这种情况。此外,也有另一种方法来解决这种情况,就是利用ADO.NET2.0新特性“异步进程”,对数据库进行异步操作,确保连接能够及时调用Close方法关闭连接,这样能大大减少正在使用的连接数。

使用方法:在连接字符串中加上AsynchronousProcessing=true表示使用异步处理操作。

当应用程序不再需要用到连接池的时候可以使用ClearPool或ClearAllPools方法清空连接池也可作重置连接池使用,方法如下:

SqlConnection.ClearPool(SqlConnection connection)清空关联的连接池

SqlConnection.ClearAllPools()清空所有连接池

调用上述方法,如果连接正在使用,连接池会做相应标记,等连接关闭时自动烧毁。

小结C#数据库连接池

优点:当数据库操作和访问频繁的时候,减少创建连接和打开连接所耗的时间,提升数据库服务器的性能。

缺点:数据库连接池中可能存在着多个没有被使用的连接一直连接着数据库,这意味着资源的浪费。

 

在企业级软件开发过程中,为了改善应用程序的性能需要通常使用对象池来控制对象的实例化。例如,在我们每次需要连接一个数据库时都需要创建一个数据库连接,而数据库连接是非常昂贵的对象。所以,为了节省为每次数据库调用都实例化一个数据库连接的资源,我们可以缓存并重用一些创建好的数据库连接对象并通过节省为每次数据库调用都创建一个数据库连接对象的时间和资源来大幅度提高程序性能。

对象池与图书馆很像。图书馆里维护很多书籍。当对某本书的需求增加时,图书馆就会买更多书,否则的话读者们就会一直使用同一本书。在对象池中,首先我们检查对象是否已经被创建且被放到池中,如果已经被放到池中,我们就会得到对象池中缓存的对象;如果没有找到就会创建一个新的对象并放到对象池中以备之后使用。对象池计数广泛地用于大规模应用程序服务,比如企业级Java组件模型(Enterprise Java Beans Servers, EJB),MTS/COM+, 甚至在.NET Framework中.

在这部分,我们将开发一个数据库连接池来缓存数据库连接。创建数据库连接是很昂贵的。在一个典型的Web应用中可能有几千个用户同时访问站点。如果这些用户恰好想要访问数据库的动态数据而我们继续为每个用户创建一个数据库连接的话,我们将对应用程序的性能带来负面影响。创建一个新的对象要求更多内存。内存分配会降低应用程序性能,最后的结果是Web站点在分发动态内容时变得非常慢,或者到达一个临界值导致站点崩溃。连接池维护一个已创建的对象池,所以需要一个数据库连接的应用程序可以从池中借一个连接并在用完以后还给对象池,而不是创建一个新的数据库连接。一旦数据发送给一个用户,对象的数据库连接就会被收回以备之后使用。

实现对象池

让我们看一个我们的由类图描述的数据库连接池应用。图 5 显示了ObjectPool 类和继承自ObjectPool的DBConnectionSingleton 类。

图 5

ObjectPool 类

我们先贴出ObjectPool 类的代码然后开始讨论:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Timers;
 
namespace ObjectPoolSample
{
     public abstract class ObjectPool
     {
         //Last Checkout time of any object from the pool.
         private long lastCheckOut;
 
         //Hashtable of the check-out objects.
         private static Hashtable locked;
 
         //Hashtable of available objects
         private static Hashtable unlocked;
 
         //Clean-Up interval
         internal static long GARBAGE_INTERVAL = 90 * 1000 ; //90 seconds
         static ObjectPool()
         {
             locked = Hashtable.Synchronized( new Hashtable());
             unlocked = Hashtable.Synchronized( new Hashtable());
         }
 
         internal ObjectPool()
         {
             lastCheckOut = DateTime.Now.Ticks;
 
             //Create a Time to track the expired objects for cleanup.
             Timer aTimer = new Timer();
             aTimer.Enabled = true ;
             aTimer.Interval = GARBAGE_INTERVAL;
             aTimer.Elapsed += new ElapsedEventHandler(CollectGarbage);
         }
 
         protected abstract object Create();
 
         protected abstract bool Validate(object o);
 
         protected abstract void Expire(object o);
 
         internal object GetObjectFromPool()
         {
             long now = DateTime.Now.Ticks;
             lastCheckOut = now;
             object o = null ;
 
             lock ( this )
             {
                 try
                 {
                     foreach (DictionaryEntry myEntry in unlocked)
                     {
                         o = myEntry.Key;
                         unlocked.Remove(o);
                         if (Validate(o))
                         {
                             locked.Add(o, now);
                             return o;
                         }
                         else
                         {
                             Expire(o);
                             o = null ;
                         }
                     }
                 }
                 catch (Exception) { }
                 o = Create();
                 locked.Add(o, now);
             }
             return o;
         }
 
         internal void ReturnObjectToPool(object o)
         {
             if (o != null )
             {
                 lock ( this )
                 {
                     locked.Remove(o);
                     unlocked.Add(o, DateTime.Now.Ticks);
                 }
             }
         }
 
         private void CollectGarbage(object sender, ElapsedEventArgs ea)
         {
             lock ( this )
             {
                 object o;
                 long now = DateTime.Now.Ticks;
                 IDictionaryEnumerator e = unlocked.GetEnumerator();
 
                 try
                 {
                     while (e.MoveNext())
                     {
                         o = e.Key;
 
                         if ((now - ( long )unlocked[o]) > GARBAGE_INTERVAL)
                         {
                             unlocked.Remove(o);
                             Expire(o);
                             o = null ;
                         }
                     }
                 }
                 catch (Exception) { }
             }
         }
     }
}

ObjectPool 类有两个重要的方法; GetObjectFromPool(), 从对象池中获取一个对象, ReturnObjectToPool(), 把对象还给对象池。我们以两个哈希表实现对象池,一个称为locked, 另一个称为unlocked. locked 哈希表包含所有正在使用的对象而unlocked 哈希表包含了所有未被使用且可随时使用的对象。ObjectPool 还有三个三个必须重载的方法:Create(), Validate() 和 Expire(), 它们必须由继承类实现。

总而言之,ObjectPool 类中有三个关键部分:

使用GetObjectFromPool() 来从对象池中获取一个对象,当需要向对象池中添加一个对象时必须使用锁,由于这个过程locked 和 unlocked 哈希表的内容会发生变化而我们不想在这个过程中发生冲突。

使用ReturnObjectToPool() 来把一个对象返回给对象池,同样需要使用锁,理由同上。

使用CollectGarbage() 从对象池中清除过期对象,在这个方法中我们遍历unlocked哈希表以便从对象池中找到并移除过期对象。这个过程中unlocked哈希表的内容可能会发生改变所以我们需要使用锁来保证这一过程是原子操作。

GetObjectFromPool() 方法中,我们遍历unlocked哈希表来获取第一个可用对象。获得了以后使用Validate() 方法去验证指定对象。基于不同的缓存对象类型,Validate()方法的实现也可能有很大不同。例如,如果对象是一个数据库连接,那么继承对象池的类就需要实现Validate()方法来检查数据库连接是打开的还是关闭的。如果对象池对象验证通过了,我们从unlocked哈希表中移除这个对象并把它放到locked哈希表中。locked 哈希表中的对象表示正在使用的对象。如果验证失败,我们就使用Expired()方法把对象注销。Expire()方法也需要通过继承类实现并根据不同的缓存对象类型而有不同的实现形式。还是以一个数据库连接为例,过期对象将关闭数据库连接。如果没有找到一个缓存对象,说明unlocked哈希表是空的,我们使用Create()方法创建一个新对象然后把它放入到locked哈希表中。

ReturnObjectToPool() 方法的实现相对简单一些。我们仅仅需要将对象从locked哈希表中移除并把它放回unlocked哈希表中以备另用。在整个回收过程中,我们不得不考虑应用程序的内存使用情况。对象池与内存使用量成正比。所以,我们缓存的对象越多,就需要使用更多内存。为了控制内存使用量,我们应该周期性地对池中的对象进行垃圾回收处理。这可以通过对池中每个对象加一个超时周期来实现。如果在超时时间内一个缓存对象没有被使用,那么它将会被作为垃圾回收。结果就是对象池的内存使用量将很大程序上取决于系统负载。

CollectGarbage() 方法用来处理对象池的垃圾回收。这个方法由ObjectPool构造函数中初始化的一个Timer委托进行调用。在我们的例子中,我们通过GARBAGE_COLLECT 常量将垃圾回收时间间隔定位90秒。

我们还没有实现任何数据库连接相关的代码,所以我们假设ObjectPool 类可以用于对.NET Framework 中的所有类型进行缓存。

DBConnectionSingleton 类

DBConnectionSingleton 类实现了一个数据库连接对象池。这个类的主要目的是为继承自ObjectPool 类的特定数据库连接实现Create(), Validate() 和 Expire()方法。这个类也提供BorrowDBConnection() 和 ReturnDBConnection() 方法来从对象池中借出/返还数据库连接。

DBConnectionSignletion 类的完整代码片段如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;
using System.Data;
 
namespace ObjectPoolSample
{
     public sealed class DBConnectionSingletion : ObjectPool
     {
         private DBConnectionSingletion() { }
 
         public static readonly DBConnectionSingletion Instance =
             new DBConnectionSingletion();
 
         private static string connectionString =
             @ "server=(local);Trusted Connection=yes;database=northwind" ;
 
         public static string ConnectionString
         {
             get
             {
                 return connectionString;
             }
             set
             {
                 connectionString = value;
             }
         }
 
         protected override object Create()
         {
             SqlConnection conn = new SqlConnection(connectionString);
             conn.Open();
             return conn;
         }
 
         protected override bool Validate(object o)
         {
             try
             {
                 SqlConnection conn = (SqlConnection)o;
                 return !conn.State.Equals(ConnectionState.Closed);
             }
             catch (SqlException)
             {
                 return false ;
             }
         }
 
         protected override void Expire(object o)
         {
             try
             {
                 SqlConnection conn = (SqlConnection)o;
                 conn.Close();
             }
             catch (SqlException) { }
         }
 
         public SqlConnection BorrowDBConnection()
         {
             try
             {
                 return (SqlConnection)base.GetObjectFromPool();
             }
             catch (Exception e)
             {
                 throw e;
             }
         }
 
         public void ReturnDBConnection(SqlConnection conn)
         {
             base.ReturnObjectToPool(conn);
         }
     }
}

由于你正在处理的是SqlConnection对象,所以Expire()方法用来关闭SqlConnection, Create() 方法用来创建SqlConnection 而 Validate() 则用来检查SqlConnection 是打开的还是关闭的。使用DBConnectionSigleton 对象实例可以使整个同步问题对客户端应用程序透明。

为什么要使用单例模式?

Singleton 是一个著名的创建型设计模式,当你需要一个对象仅对应一个实例时通常需要使用它。设计模式一书(ISBN 0-201-70265-7)中对设计单例模式目的定义为保证一个类仅有一个实例,并提供全局唯一的方式来访问它。为了实现一个单例,我们需要一个私有构造函数以便于客户端应用程序无论如何都没法创建一个新对象,使用静态的只读属性来创建单例类的唯一实例。.NET Framework 在JIT 过程中仅当有任何方法使用静态属性时才会将其实例化。如果属性没有被使用,那么也就不会创建实例。更准确地说,仅当有任何类/方法对类的静态成员进行调用时才会构造对应单例类的实例。这个特性称作惰性初始化并把创建对象的过程留给第一次访问实例属性的代码。.NET Framework 保证共享类型初始化时的类型安全。所以我们不需要担心DBConnectionSingleton对象的线程安全问题,因为在应用程序整个生命周期内金辉创建一个实例。实例静态属性维护DBConnectionSingleton类对象的唯一实例。

使用数据库连接池

现在已经准备好使用数据库连接池了,下面的代码片段显示了如何实例化并使用数据库连接池:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Data.SqlClient;
 
namespace ObjectPoolSample
{
     class Program
     {
         static void Main(string[] args)
         {
             //Initialize the Pool
             DBConnectionSingletion pool = DBConnectionSingletion.Instance;
 
             //Set the ConnectionString of the DatabaseConnectionPool
             ConnectionStringSettings<wbr> settings = ConfigurationManager.ConnectionStrings[ "NorthwindConnectionStrin<wbr>g" ];
             DBConnectionSingletion.ConnectionString = settings.ConnectionString;
             //Borrow the SqlConnection object from the pool
             SqlConnection conn = pool.BorrowDBConnection();
 
             //Return the Connection to the pool after using it
             pool.ReturnDBConnection(conn);
 
             Console.ReadLine();
         }
     }
}

在上面的例子中,我们通过DBConnectionSingletion 类的实例属性来初始化它的实例。如上面讨论的,我们假设使用单例设计模式可以保证我们有且仅有一个DBConnectionSingletion 对象的实例。我们把ConnectionString 属性设置为本机SQL Server实例上的北风数据库。现在,我们可以使用对象池的BorrowDBConnection() 方法来从对象池借一个数据库连接, 然后通过调用对象池的ReturnDBConnection() 方法来返还数据库连接。如果你真的想看看应用程序池是如何运行的,那么最好的方式就是打开Visual Studio .NET 中的工程并在调试模式下跟踪上面给出的应用程序代码。

 

总结

在企业级计算的多线程世界中同步是一个极其重要的概念。它被广泛用于数据库,消息队列以及Web 服务器等闻名应用上。任何开发多线程应用程序的开发人员都必须对他们的同步概念特别清楚。不是为了让每个对象都是线程安全的而导致系统不堪重负,而是应该关注死锁情况并在程序设计之初就解决尽可能多的死锁问题。理解同步带来的性能瓶颈问题同样很重要,因为它将影响应用程序的总体性能。在这一章,除了探讨.NET Framework 中自带的同步特性,我们也开发了两个有用的应用程序:

一个自定义的线程安全包装器。在这个例子中,你学到了如何为你的类库添加原生同步支持并为调用类库的开发人员提供是否使用同步的选项。这将帮助第三方开发人员关注于他们自己的应用程序而不是类库的线程安全问题。

一个数据库连接池。在这个例子中,你开发了可以用于任意相似对象类型的对象池。有了对象池,我们继续开发了一个继承自对象池的数据库连接池。对象池可以用于任意对象

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
OSCHINA
登录后可查看更多优质内容
返回顶部
顶部