数据库连接池

时间:2022-05-10 09:06:20

连  接  池

与微软以前的数据访问技术类似,ADO.NET包括对连接池的内置支持。

1  连接句柄和物理连接

如果正在使用Visual Studio,可以使用Visual Studio调试工具检查对象的一些内部私有属性。例如,编写一些代码来打开一个SqlConnection,并在调用Open方法的地方设置断点。右击代码中的对象,并选择【添加监视】,将该对象添加到【监视】窗口。在【监视】窗口中,展开标有Non-Pubic Members的区域。向下滚动,将会看到一个称为InnerConnection的私有属性。

从结构上讲,InnerConnection属性的内容是一个非常薄的层,位于数据库的物理连接之上。为在这里进行讨论,InnerConnection属性和到该数据库的物理连接是可交换的。在逐步执行代码时,将会看到在打开和关闭连接时,InnerConnection属性的值发生变化。当调用Open方法时,SQL Client .NET数据提供程序将SqlConnection对象关联至该数据库的物理连接,所以可以执行查询并返回结果。

打开和关闭数据库连接的代价非常高。为了帮助节省资源并提高性能,.NET Framework中的.NET数据提供程序在默认情况下均使用连接池。

2  连接池是什么

连接池是一种在打开数据存储区的连接时提高应用程序性能的机制。在调用SqlConnection对象的Close方法时,SQL Client .NET数据提供程序并不实际关闭内部连接。相反,数据提供程序将该内部连接存储到一个池中,以便在以后再次使用。甚至在SqlConnection对象被处理之后,该内部连接也保留在池中。如果在以后使用相同连接字符串和凭据调用SqlConnection对象的Open方法,将会再次使用同一内部连接与数据库进行通信。

如果希望确认是否真正再次利用了同一内部连接,可以使用.NET Reflection中的功能以可编程方式访问私有InnerConnection属性的内容。以下代码(其需要对System.Reflection命名空间的引用)在Using代码块中打开一个SqlConnection,并存储SqlConnection的InnerConnection属性的值。通过利用Using代码块,在该代码块的末尾隐式处理了SqlConnection。此代码在Using代码块中打开另一个SqlConnection,并存储SqlConnection的InnerConnection属性的值。最后,此代码对比InnerConnection属性的内容,确认它们实际上为同一对象。

Visual Basic

Dim strConn As String = "Data Source=./SQLExpress;Integrated Security=True;"
Dim propInnerConn As PropertyInfo
propInnerConn = GetType(SqlConnection).GetProperty("InnerConnection", _
                            BindingFlags.NonPublic or BindingFlags.Instance)
Dim objInnerConn1, objInnerConn2 As Object
Using cn As New SqlConnection(strConn)
    cn.Open()
    objInnerConn1 = propInnerConn.GetValue(cn, Nothing)
    cn.Close()
End Using

Using cn As New SqlConnection(strConn)
    cn.Open()
    objInnerConn2 = propInnerConn.GetValue(cn, Nothing)
    cn.Close()
End Using

Console.WriteLine(objInnerConn1 Is objInnerConn2)

Visual C#

string strConn = @"Data Source=./SQLExpress;Integrated Security=True;";
PropertyInfo propInnerConn;
propInnerConn = typeof(SqlConnection).GetProperty("InnerConnection",
                                BindingFlags.NonPublic | BindingFlags.Instance);
object objInnerConn1, objInnerConn2;
using (SqlConnection cn = new SqlConnection(strConn))
{
    cn.Open();
    objInnerConn1 = propInnerConn.GetValue(cn, null);
    cn.Close();
}

using (SqlConnection cn = new SqlConnection(strConn))
{
    cn.Open();
    objInnerConn2 = propInnerConn.GetValue(cn, null);
    cn.Close();
}

Console.WriteLine(objInnerConn1 == objInnerConn2);

两个SqlConnection对象是在不同的Using代码块中创建的,所以其资源将在每个Using代码块的末尾被清除。InnerConnection属性的内容及其所封装的物理连接没有被处理,而是存储在池中,以便在以后被再次利用。

数据库连接池 注意    如果在连接字符串中禁用了连接池(稍后将解释如何禁用),将会看到此内部连接不能被重复利用。

3  连接池如何改进代码

考虑一个访问SQL Server数据库的典型ASP.NET或WebServices应用程序。客户端应用程序每次需要查询数据库时,就会在服务器端代码中进行往返,以打开SqlConnection来执行查询。在许多此类应用程序中,这一代码以相同凭据一次又一次地连接到相同数据库。理论上,这意味着客户端应用程序每次需要执行查询时,服务器端代码需要执行三个操 作——登录到数据库(需要检查所提供的凭据)、执行查询、然后注销。

连接池可以真正地提高此类应用程序的性能。通过将内部连接存储在池中,并在以后进行重复利用,就不再因为登录数据库以及从中注销而降低性能。对SqlConnection对象的Open和Close方法的调用可以短时间内返回,从而可以提高代码的性能和响应速度(请参见图3.4)。

数据库连接池

图3.4  典型ASP.NET或WebServices应用程序中的连接池

4  启用连接池

在ADO.NET中,连接池是默认启用的。以下代码段将同一SqlConnection对象打开和关闭5次。由于连接池是默认启用的,所以当调用Close方法时,到数据库的实际连接没有被实际关闭,而是将该数据库连接发送到池中,以便在以后重复利用。

Visual Basic

Dim strConn As String
strConn = "Data Source=./SQLExpress;Integrated Security=True;"
Dim cn As New SqlConnection(strConn)
For intCounter As Integer = 1 To 5
    cn.Open()
    cn.Close()
Next intCounter

Visual C#

string strConn;
strConn = @"Data Source=./SQLExpress;Integrated Security=True;";
SqlConnection cn = new SqlConnection(strConn);
for (int intCounter = 1; intCounter <= 5; intCounter++)

    cn.Open();
    cn.Close();
}

5  放入池中的连接何时关闭

在调用Close方法时,SqlClient将该连接返回到池中。假定该连接没有被再次使用,将在大约5分钟后将其从池中删除。但具体在多少秒后删除,并没有确切的数值。其行为取决于所生成的随机数以及创建该池时的相对湿度(relative humidity)。当然,如果在退出应用程序时存在已打开的连接池,那么作为应用程序正常清除过程的一部分,这些连接将被关闭和处理。

6  禁用连接池

您可能不希望使用连接池。例如,如果正在使用一个直接与数据库进行通信的简单Windows应用程序,那么可能希望禁用连接池。在采用这一架构时,各个客户端应用程序需要自己的连接。在启用连接池时,每个应用程序的连接被放入池中,如果在清除连接池之前重新打开该连接,将重复利用放入池中的连接。所以,如果应用程序频繁重复使用连接,那么在启用连接池的情况下,对SqlConnection.Open的调用将会更快速地返回。但是,这种方法将会导致在任意给定时刻存在许多活动的数据库连接。禁用连接池将会降低任意时刻的活动数据库连接数目,但这样会强制所有对SqlConnection.Open的调用都建立一个新的数据库连接。

如果希望禁用连接池,可以通过向连接字符串中添加Pooling=False,逐个连接地禁用连接池。

幸运的是,在ADO.NET 2.0中不再需要记忆诸如此类的属性。如果存在疑问,可以检查SqlConnectionStringBuilder类的选项。在这个类中可以找到一个Pooling属性,其取值为Boolean类型。默认情况下,此值被设置为True。将该值设置为False将会禁止将该连接放入池中。因此,在调用SqlConnection对象的Close方法时,将会关闭与数据库的实际连接。

数据库连接池 注意    在“偶尔进行连接”的Windows应用程序中,使用连接池可能很有帮助,具体取决于应用程序。如果应用程序希望定期重新连接到数据库,则可以发挥连接池的作用,将与数据库的物理连接保持打开状态,至少暂时如此。如果在从池中删除该物理连接之前,应用程序尝试重新连接到该数据库,则连接池逻辑(pooling logic)将会重新使用与该数据库的物理连接。

7  有关连接池的常见问题

学习连接池的开发人员越多,出现的问题也会越多。例如,在我听到的连接池相关问题中,最常见的一个是“我怎样才能知道与数据库的物理连接是被真正关闭了,还是仅仅被放入池中了?”,另一个常见问题是“我怎样才能分辨刚刚打开的连接是建立了一个新物理连接,还是重新利用了一个被放在池中的连接?”。

有许多工具可以帮助回答有关连接池的问题。其中一些工具更出色一些。我定期使用SQL Server事件探查器来监视对SQL Server数据库的连接和查询。在ADO.NET的2.0版中,还可以使用Windows中的【性能监视器】。

ADO.NET 2.0中的SQL Client .NET数据提供程序包括用于连接池的性能计数器。现在可以使用诸如【性能监视器】等工具来查看以下数目:放入池中的连接、活动连接、*连接、活动与非活动连接池及活动与非活动连接池组。还可以搜集有关在每秒内进行连接和断开连接数目的信息。

在某些情况下,维护性能计数器会产生一些性能影响。为此,SQL Client .NET数据提供程序没有维护以下性能计数器:活动或*连接的数目,或者每秒内放入池中的连接数目或断开连接的数目。可以通过向应用程序的配置文件中添加一项,以启用应用程序中的这些性能计数器。如需有关使用这些性能计数器的详细信息,请参阅MSDN网站上的文章“Using ADO.NET Performance Counters”(使用ADO.NET性能计数器)。

为便于您提出有关连接池的问题,也便于我回答这些问题,我已经开发了一个示例应用程序,如图3.5所示,它可以作为本书示例代码中的一部分进行下载。这一应用程序允许使用如图3.3所示的SqlConnectionStringBuilder/PropertyGrid对话框生成连接字符串。可以很容易地生成新的SqlConnection,打开和关闭现有连接,以及调用ClearPool和ClearAllPools方法。此示例还可以通过【性能监视器】访问SQL Client性能计数器,而不需要以手动方式添加性能计数器。此应用程序的配置文件中包含一项,其能够启用在默认情况下被关闭的性能计数器。在每次创建、打开或关闭SqlConnection或关闭一个或所有连接池时,此示例中的性能计数器都会被更新。

数据库连接池

图3.5  研究连接池

8  ADO.NET如何确定是否使用放入池中的连接

简单地说,假定连接池未被禁用,则SQL Client .NET数据提供程序在您调用SqlConnection对象的Open方法时检查ConnectionString,并确定池中是否存在可用连接。如果存在可用连接,则SQL Client使用该连接。否则,打开一个到数据库的新连接。

实际上还有一些需要说明的内容。设想一个ASP.NET应用程序,其中有多位用户以模拟(impersonation)登录同一数据库,每位用户都使用自己的凭据访问SQL Server数据库。每位用户的连接字符串都是相同的,但他们的凭据有很大不同。由于SQL Client考虑了用户权限,所以用于确定池中有哪些连接可供使用的逻辑要稍微复杂一些。

9  强制ADO.NET使用新池

在某些时候,可能不再希望从旧连接池中提取连接,而希望建立一个新池。在这种情况下,目标就是采用某种能够对池缓存(pooling)产生影响的方式来修改连接字符串,而对应用程序的剩余部分不产生影响。达到这一目标的最简单方式是在连接字符串的末尾添加一个空格。

10  手动释放池中缓存的连接

前面的技巧对于ADO.NET 1.x版非常方便,因为API的特性都不能帮助释放缓存在池中的连接。在ADO.NET 2.0中,SqlConnection类中有两个新的静态方法可提供帮助——ClearPool和ClearAllPools。

ClearPool方法取得一个SqlConnection对象,并释放缓存在一个连接池中的所有连接,此连接池与该SqlConnection对象相关。假定有10个SqlConnection对象,均使用同一连接字符串和凭据,并启用了连接池。可以调用所有10个对象的Open方法,然后调用其中三个对象的Close方法。

共有10个到SQL Server数据库的开放式连接。这些连接中的7个连接与7个打开的SqlConnection对象相关联。为了使用性能计数器所使用的术语,我们说这7个连接是“活动的”。剩下的三个连接存在于连接池中。再一次使用性能计数器的术语,它们是“*”连接。调用ClearPool方法将释放这三个存在于连接池中的*连接,但不会影响由7个开放式SqlConnection对象所使用的活动连接。

ClearAllPools方法没有参数,它会清除所有*SqlConnection。

11  其他连接池选项

现在简单看一下其他常用连接池选项。每个选项都可通过SqlConnectionStringBuilder和连接字符串使用。

1. Connection Reset

如果仔细查看一个SQL事件探查器的轨迹,可能会注意到调用了一个名为sp_reset_connection的存储过程,您可能希望知道是何时调用这个存储过程的,以及为什么要调用它。

简单地重复使用缓存在池中的SqlConnection可能会产生意料之外的结果。由于缺乏更合适的术语,所以可以说:存在某个与已缓存(pooled)连接相关联的“残余”。开发人员不会总是在关闭连接时清除这些混乱,以将连接返回到其原始状态。在调用Close方法时,可能已经存在与该连接相关联的开放式游标或事务,甚至更糟。如果发出一个“USE AdventureWorks”之类的查询,该连接可能被关联到一个不同于该数据库被打开时的数据库。如果通过调用sq_setapprol为连接指定应用程序角色,这些权限仍然被应用到该连接,直到sp_unsetapprole被调用为止,或者直到sp_reset_connection被调用为止,或者直到该连接被真正关闭(而不仅仅是被放入池中)为止。

SQL Client .NET数据提供程序跟踪哪些SqlConnection使用了从连接池中提取的连接。在调用Open方法时,SqlClient没有调用sp_reset_connection存储过程,而是恰好在该连接的第一个操作之前执行这一查询。

如果能够绝对肯定没有在服务器上为池中缓存的连接留下任何“残余”,就可以向连接字符串中添加Connection Reset=False;。这一设置告诉SqlClient,当重新利用缓存在池中的连接时,不需要调用sp_reset_connection存储过程。但是,我建议不要将Connection Reset设置为False。

2. Min Pool Size

从其名称可以看出,Min Pool Size控制池中的最少连接数目。默认情况下,此属性被设置为0。

Min Pool Size属性可以帮助您准备一个连接池。假定将属性设置为5。在打开第一个连接之后,SqlClient将在一后台线程中打开另外4个连接。在该池中总是至少有5个连接。假定代码创建了10个SqlConnection对象,并将它们全部打开。和以前一样,在打开第一个SqlConnection时,SqlClient将在后台线程打开其他4个连接。接下来的4个SqlConnection对象将使用来自池中的剩余连接。剩余的5个SqlConnection对象将建立新连接。

现在,假定关闭了这10个SqlConnection对象中的8个对象。所有这8个连接都将在池中保持存活。当SqlClient清除此连接池之后(假定大约在这些SqlConnection被关闭5分钟之后,并且没有被重新使用),它将确保至少有5个连接(Min Pool Size设置)仍然存在于连接池中。此数目包括目前使用的连接,两种连接仍在使用。所以将有三个其他连接保存在连接池中。其余5个连接将被丢弃。

Min Pool Size的主要缺点是池中将至少有设定数目的连接保持活动。在ASP.NET应用程序中,这些连接的存在时间可能过长。因此,最好使Min Pool Size保持为零。

3. Max Pool Size

Max Pool Size设置的理解稍微简单一些。此设置用作一种限制特性,防止在单个池中打开的连接数目超出指定数值。Max Pool Size的默认值为100。如果达到了连接池中的最大连接数目,那么在下一次尝试打开一个连接时,经过Connect Timerout设置所指定的时间之后会引发一个InvalidOperationException,其说明“超时时间已到。在从池中获取连接之前超时时间已过。出现这种情况可能是因为所有池连接都已被使用,并已达到最大池 大小”。