代码改变世界

ADO.NET数据连接池

2007-07-02 16:35  DQ  阅读(622)  评论(0编辑  收藏  举报
21世纪什么最贵?数据库连接。对于以数据库做数据存储基石的应用系统来说,数据库连接是整个系统中最珍贵的资源之一。数据库连接池是为了更有效地利用数据库连接的最重要措施。它对于一个大型的应用系统的性能至关重要,特别是Web应用。ADO.NET Data Provider(以下简称Data Provider)会帮我们管理连接池,所以有人说使用连接池就像游儿童池一样轻松。但并不是说有了Data Provider程序员就万事无忧的,不正确地使用连接池可能导致你的应用在池里淹死。笔者希望通过本文能让读者彻底明白连接池的重要性以及能根据实际情况正确配置连接池的参数,明白实际应用中出现的连接泄漏、“死连接”等异常情况和应对方法,让应用轻松畅游连接池。本文主要介绍ADO.NET 1.1的连接池。


    1、什么是连接池 

    连接池是Data Provider提供的一个机制,使得应用程序使用的连接保存在连接池里而避免每次都要完成建立/关闭连接的完整过程。要理解连接池,先要理解程序里SqlConnection.Open()、SqlConnection.Close()和打开/关闭一个“物理连接”的关系。 

    Data Provider在收到连接请求时建立连接的完整过程是:先连接池里建立新的连接(即“逻辑连接”),然后建立该“逻辑连接”对应的“物理连接”。建立“逻辑连接”一定伴随着建立“物理连接”。Data Provider关闭一个连接的完整过程是先关闭“逻辑连接”对应的“物理连接”然后销毁“逻辑连接”。销毁“逻辑连接”一定伴随着关闭“物理连接”。SqlConnection.Open()是向Data Provider请求一个连接,Data Provider不一定需要完成建立连接的完整过程,可能只需要从连接池里取出一个可用的连接就可以;SqlConnection.Close()是请求关闭一个连接,Data Provider不一定需要完成关闭连接的完整过程,可能只需要把连接释放回连接池就可以。 

    下面以一个例子来说明。本文例子都使用Console Application。我们使用操作系统的性能监视器来比较使用连接池与否,数据库的“物理连接”数量的不同。因为性能监视器至少每一秒采集一次数据,为方便观察效果,代码中Open和Close连接后都Sleep一秒。


SqlConnection con = new SqlConnection("server = .;
database = northwind;pooling = false;trusted_connection = true");
for(int i = 0;i < 10;i++) { try { con.Open(); System.Threading.Thread.Sleep(1000); } catch(Exception e){Console.WriteLine(e.Message);} finally { con.Close(); System.Threading.Thread.Sleep(1000); } }

    首先,不使用连接池做测试。以上程序中pooing = false表示不使用连接池,程序使用同一个连接串Open & Close了10次连接,使用性能计数器观察SQL Server的“物理连接”数量。从下面的锯齿图可以看出每执行一次con.Open(),SQLServer的“物理连接”数量都增加一,而每执行一次con.Close(),SQLServer的“物理连接”数量都减少一。由于不使用连接池,每次Close连接的时候Data Provider需要把“逻辑连接”和“物理连接”都销毁了,每次Open连接的时候Data Provider需要 建立“逻辑连接”和“物理连接”,锯齿图因此而成。

图1

    下面启用连接池再测试一次。把连接串的pooling参数改为true,另外在for循环后加上Console.Read()。

    从下图可以看出,从第一次Open到第执行完Console.Read()这段时间内,SQL Server的“物理连接”数量一直保持为1,直到关闭console应用程序的进程后SQL Server的“物理连接”数量才变为0。由于使用了连接池,每次Close连接的时候Data Provider只需把“逻辑连接”释放回连接池,对应的“物理连接”则保持打开的状态。每次Open连接的时候,Data Provider只需从连接池取出一个“逻辑连接”,这样就可以使用其对应“物理连接”而不需建立新的“物理连接”,直线图因此而成。

图2

    在ADO.NET 1.1下使用性能计数器观察连接池有关计数器需要注意两个bug。 

    (1)当应用程序进程关闭后,计数器“SqlClient: Current # pooled connections”和“SqlClient: Current # connection pools”不会减为0,所以每重新运行一次应用程序性能计数器的值在上次的值的基础上一直累加。这是计数器的错误显示,实际上当应用程序关闭后connection pool和pooled connection就减为0。因为关闭应用程序后把性能监视器也关闭,重启应用程序后再重新打开性能监视器就可以看出“SqlClient: Current # pooled connections”和“SqlClient: Current # connection pools”是重新从0开始上升的。 

    (2)用断点调试的情况下,连接串为"server = .;database = northwind;pooling = true;trusted_connection = true" 的connnection第一次Open的时候“SqlClient: Current # pooled connections”就从0变为2。但根据连接串参数的意义,只Open了一个connection,“SqlClient: Current # pooled connections”应该从0变为1(图2是在没有断点调试的情况下得出的曲线)。这不是计数器显示错误,而是ADO.ENT 1.1本身的bug,因为“User Connections”也随着“SqlClient: Current # pooled connections”从0变为2。 

    为什么需要连接池? 

    完成建立/关闭一个连接的完整过程是一个消耗大量资源和时间的一个过程。想象一下一个ASP.NET的系统,里面包含大量访问数据库的代码片,系统有大量的用户同时在使用系统,如果程序每次Open/Close一个连接Data Provider都完成建立/关闭一个连接的完整过程,这样的系统性能肯定让人无法接受。 

    Data Provider提供连接池并通过连接池实现“物理连接”重复使用而避免频繁地建立和关闭“物理连接”,从而大大提高应用系统的性能。图1描述一个应用的不同Client App使用连接池访问数据库,Data Provider负责建立和管理一个或者多个的连接池,每一个连接池里有一个或者多个连接,池里的连接就是“逻辑连接”。连接池里有N个连接表示该连接池与数据库之间有N个“物理连接”。增加一个连接,连接池与数据库的“物理连接”就增加一个,减少一个连接,连接池与数据库的“物理连接”就减少一个。

图3
2、有多少个连接池 

    Data Provider为每个进程管理该进程的连接池,一个进程可以有一个或者多个连接池。Data Provider是根据什么来决定是建立新的连接池还是使用已有的连接池呢?根据数据库连接串。怎样才算是相同的连接串?连接串的字符完全相同?答案为是但也不是。 

    笔者看过有些文章说不一定两个连接串的字符完全相同才算是相同的连接池,例如 "server = .;database = northwind;user = sa;password = sqlserver"与 "server = .;database = northwind; password = sqlserver; user = sa”是相同的连接串。但笔者测试过,Data Provider为以上两个连接串建立两个连接池,证明它们并不是相同的连接串。其实,笔者认为,对于“两个连接串参数相同但顺序不同”,“两个连接串只差一个空格”是否是相同的连接串等问题不需要理会,因为保证两个连接池的字符完全相同是没有难度的事。 

    如果你需要相同的连接串,首先你保证两个连接串每一个字符都相同,但这还不能保证Data Provider只为你建立一个连接池。因为如果你使用Windows认证,那么即使使用相同的连接串"server = .;database = northwind;trusted_connection = true”也有可能建立多个连接池。Windows认证意味着连接数据库使用的数据库用户是运行打开数据库连接Open()的当前用户,如果运行该代码的用户不是固定的话,那么即使每次都使用相同字符的连接串也会产生多个连接池。 

    连接池建立后直到它所属的进程结束才会被销毁。 

    3、一个连接池里有多少个连接 

    明白了怎么区分不同的连接池后,下面我们来看看一个连接池里有多少个连接。一个连接池里的连接数不是静态的数量,它会随着连接池的不同状态而改变。这就涉及连接池建立的时候有多少个连接,什么时候连接会减少,什么时候会增加,连接数的上限是多少等问题。 

    首先来看看能影响连接池里连接数的连接串参数,如下表所示。

参数 默认值  描述
Min Pool Size 0 连接池一旦建立后,池里连接数量的最小值。
Max Pool Size 100 连接池里连接数量的最大值。
Connection Lifetime 0 每当一个连接使用完后释放回连接池,如果当前时间减去该连接建立的时间的值大于这个参数设定的值(秒),该连接被销毁。0表示lifetime没有上限。
Connection Timeout 15 连接请求停止请求并产出错误前等待的时间。当池的连接数达到Max Pool Size而且全部被占用,连接请求需要等待“被占用的”连接被释放回连接池,如果等待超过指定的时间还没有连接被释放就抛出InvalidOperationException。

3.1增加连接 

    一旦连接池被建立,就立即建立由Min Pool Size指定数量的连接。如果只有一个连接被占用,那么其他的连接(如果Min Pool Size大于1)为池里“可用的”连接。如果某进程有连接请求而且请求的连接的连接串与该进程的某个连接池的连接串相同(如果进程里的所有连接池的连接串都不匹配被请求的连接就需要建立新的连接池),那么如果该连接池里有“可用的”连接就从连接池里取出一个“可用的”的连接使用,如果没有“可用的”连接就建立新的连接。一旦程序运行连接的Close或者Dispose方法后,“被占用的”连接被释放回连接池变为“可用的”连接。需要区分连接池里“连接的数量”与“‘可用的’连接数量”。“连接的数量”指连接池里包括“被占用的”连接与“可用的”连接的数量。 

    如果Max Pool Size已经达到而且所有连接都被占用,新的连接请求需要等待。如果有被占用的连接释放回连接池,那么请求得到该连接;如果请求等待超过Connection Timeout的时间,程序会抛出InvalidOperationException。 

    3.2减少连接 

    两种情况下连接池里的连接会减少。 

    (1)每当一个连接使用完后释放回连接池,如果当前时间减去该连接建立的时间的值大于Connection Lifetime设定的值(秒),该连接被销毁。Connection Lifetime是用于集群数据库环境下。例如一个应用系统的中间层访问一个由3台服务器组成的集群数据库,该系统运行一段时间后发现数据库的负荷太大而需要增加第4台数据库服务器。如果不设置Connection Lifetime,你会发现新增加的服务器很久都得不到连接而原来3台服务器的负荷一点都没减少。这是因为中间层的连接一直都不会销毁而建立新的连接的可能性很小(除非出现增加服务器之后数据库的并发访问量超过增加前的并发最大值)。 

    注意:Connection Lifetime很容易让人产生误解。不要认为Connection Lifetime决定了一个连接的生存时间。因为只有连接被释放回连接池的时刻(Close连接之后)才会检查Connection Lifetime值是否达到而决定是否销毁连接,而连接在空闲或者正在使用的时候并不会检查Connection Lifetime。这意味着绝大多数情况下连接从建立到销毁经过的时间比Connection Lifetime大。另外,如果Min Pool Size为N (N > 0),那么连接池里有N个连接不受Connection Lifetime影响。这N个连接会一直在池里直到连接池被销毁。 

    (2)当发现某个连接对应的“物理连接”断开(这种连接称为“死连接”),例如数据库已经被shutdown、网络中断、SQL Server的连接进程被kill、Oracle的连接会话被kill,该连接被销毁。“死连接”出现后不是立刻被发现,直到该连接被占用来访问数据库的时候才会被发现。 

    注意:如果执行Open()方法时候Data Provider只需从连接池取出已有的连接,那么Open()并没有访问数据库,所以这时候“死连接”还不能被发现。 

    下面以一个例子详细解释一个连接池从建立起到进程结束连接数的变化情况。


string connectionString = "server = .;database = northwind;user = sa;
password = sqlserver;min pool size = 2;max pool size = 5;
connection lifetime
= 20;connection timeout = 10";
SqlConnection[] connections = new SqlConnection[7];
for(int i = 0;i < connections.Length;i++) connections[i] = new SqlConnection(connectionString); Open connection[0],8秒后Open connection[1] …8秒后Close connection[0],10秒后Open connection[0] …5秒后Open connection[2][3][4],每隔两秒打开一个 Console.WriteLine("Now the Max Pool Size is reached and
we try
to open connection[5].\r\n"); for(int i = 0;i < 2;i++) { try {connections[5].Open();} catch(InvalidOperationException e) { if(i == 1) return; Console.WriteLine("Can't open connection[5].\r\n" + e.Message); connections[4].Close(); Console.WriteLine("\r\nTry to open connection[5] again."); continue; } } Console.WriteLine("connection[5] is open."); foreach(SqlConnection con in connections) { if(con.State == ConnectionState.Open) { con.Close(); Console.WriteLine("A connection is released back to the pool."); System.Threading.Thread.Sleep(5000); } }

     使用性能监视器观察,得到图4所示结果。我们观察.NET CLR Data的“SqlClient: Current # connection pools”、“SqlClient: Current # pooled connections”以及Sql Server: General Statistic的User Connections计数器。

图4

    由于Min Pool Size = 2,所以open connection[0]的时候连接池里就建立了两个连接。之后open connection[1]、close connection[0]、open connection[0]这段时间里连接池连接数保持为2,因为open连接的并发数量都没超过2。接着,相继open connection[2]、[3]、[4],因为每次请求连接的时候连接池里都没有“可用的”连接,所以每请求一个连接连接数量就增加1,一直攀升到Max Pool Size(5)。这时候connection[0]、[1]的生存时间已经超过Connection Lifetime,但由于它们还没有被Close,所以还会继续生存。接着尝试再请求连接,这时候因为Max Pool Size已达而池里所有连接都被占用,所以第一次尝试失败。进行第二次尝试前先close connection[4],这样就有一个连接被释放回连接池,第二次尝试成功。最后close所有打开的connection,每隔5秒close一个,所有connection被close的时候它们的生存时间都大于Connection Lifetime,但由于Min Pool Size = 2,所以只有3个connection被销毁。 

    另外强调两点: 

    (1)可用看出增加/减少一个连接池的连接,User Connections(即“物理连接”)随着增加/减少一个。(为方便观察,先用Sql Query Analyzer打开一个用户连接) 

    (2)由于使用相同连接串,所以由始至终只有一个连接池。