连 接 池
与微软以前的数据访问技术类似,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考虑了用户权限,所以用于确定池中有哪些连接可供使用的逻辑要稍微复杂一些。