6.3 连接池
数据库连接池((Cnnection Pool)允许应用程序重用已存在于池中的数据库连接,以避免反复建立新的数据库连接。这种技术能有效提高应用程序的伸缩性,因为有限的数据库连接能够给大量的客户提供服务。这种技术同时也提高了系统性能,避免了建立新连接的大量开销。
6.3.1 什么是连接池
在日常应用程序的开发中,连接数据库服务器是应用程序中耗费大量资源且相对较慢的操作,但它们又是至关紧要的。通常,连接到数据库服务器必须由几个需要花费很长时间的步骤组成,如建立物理通道(例如套接字或命名管道)、与服务器进行初次握手、分析连接字符串信息、由服务器对连接进行身份验证、运行检查以便在当前事务中登记,等等。如果在执行应用程序期间,这些相同的连接反复地打开和关闭,那么系统为连接所花费的开销是非常大的。因此,为了避免这种相同连接不断地重复打开与关闭操作,从而使打开连接花费的系统开销最小,ADO.NET使用称为连接池的优化方法。
可以将连接池看成是已打开的及可重用的数据库所连接的一个容器。连接池使新连接必须打开的次数得以减少。池进程保持物理连接的所有权。通过为每个给定的连接配置保留一组活动连接来管理连接。每当用户在连接上调用Open时,池进程就会查找池中可用的连接。如果某个池连接可用,会将该连接返回给调用者,而不是打开新连接。应用程序在该连接上调用Close时,池进程会将连接返回到活动连接池中,而不是关闭连接。连接返回到池中之后,即可在下一个Open调用中重复使用。
连接池中提供了空闲的、打开的、可重用的数据库连接,而不再需要每次在请求数据库数据时新打开一个数据库连接。当数据库连接关闭或释放时,将返回到连接池中保持空闲状态,直到新的连接请求到来。连接池在所有的数据库连接都关闭时才从内存中释放。因此,如果有效地使用连接池,打开和关闭数据库所耗费的系统资源将大大减小。
ADO. NET的Data Providers在默认情况下将使用连接池,如果不想使用连接池,则必须在连接字符串中指定“polling=false”。如下面的代码所示:
<connectionStrings>
<add name="ConnectionString"
connectionString="server=.;database=ASPNET4;uid=sa;
pwd=mawei;pooling=false;"/>
</connectionStrings>
使用OLE DB Connection对象时,在连接字符串中指定“OLE DB Services=-4”来禁止使用连接池。如下面的代码所示:
Provider=SQLOLEDB;OLE DB Services=-4;
Data Source=localhost;Integrated Security=SSPI;
虽然采用数据库连接池后,数据库连接请求可以直接通过连接池满足,而不需要为该请求重新连接、认证到数据库服务器,这样就节省了时间,从而提高应用程序的性能及可伸缩性。但它也存在着这样一个缺点:数据库连接池中可能存在着多个没有被使用的连接一直连接着数据库,这意味着资源的浪费。因此,在连接池的使用中,做如下建议:
1)当需要数据库连接时才去创建连接池,而不是提前建立。一旦使用完连接立即关闭它,不要等到垃圾收集器来处理它。
2)在关闭数据库连接前确保关闭了所有用户定义的事务。
3)不要关闭数据库中所有的连接,至少保证连接池中有一个连接可用。如果内存和其他资源是你必须首先考虑的问题,可以关闭所有的连接,然后在下一个请求到来时创建连接池。
6.3.2 连接池如何工作
要理解连接池,首先需要了解程序里打开((Oen())/关闭((Cose())一个“物理连接”的关系。
1)Data Provider在收到连接请求时建立连接的过程为:先在连接池里建立新的连接(即“逻辑连接”),然后建立该“逻辑连接”对应的“物理连接”,建立“逻辑连接”一定伴随着建立“物理连接”。
2)Data Provider关闭一个连接的过程为:先关闭“逻辑连接”对应的“物理连接”,然后销毁“逻辑连接”。销毁“逻辑连接”一定伴随着关闭“物理连接”。
其中,Open()是向Data Provider请求一个连接,Data Provider不一定需要完成建立连接的完整过程,可能只需要从连接池里取出一个可用的连接就可以;而Close()是请求关闭一个连接,Data Provider不一定需要完成关闭连接的完整过程,可能只需要把连接释放回连接池就可以。
图6-4描述了一个应用中的不同客户端应用程序使用连接池访问数据库的情况。Data Provider负责建立和管理一个或者多个连接池,每一个连接池里有一个或者多个连接,池里的连接就是“逻辑连接”。连接池里有N个连接表示该连接池与数据库之间有N个“物理连接”。如果增加一个连接,连接池与数据库的“物理连接”就增加一个;如果减少一个连接,连接池与数据库的“物理连接”就减少一个。
图 6-4 连接池的使用
为了更好地理解使用连接池与不使用连接池的区别,下面以一个例子来说明。在这里,使用操作系统的性能监视器来比较使用连接池与不使用连接池的运行情况以及数据库的“物理连接”数量的不同。因为性能监视器至少每一秒采集一次数据,为方便观察效果,代码中Open()和Close()连接后都Sleep1秒。代码如下所示:
public partial class WebForm1:System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
string connectionString=
WebConfigurationManager.ConnectionStrings
["ConnectionString"].ConnectionString;
SqlConnection con=new SqlConnection(connectionString);
for(int i=0;i<10;i++)
{
try
{
con.Open();
System.Threading.Thread.Sleep(1000);
}
catch(Exception ex)
{
throw ex;
}
finally
{
con.Close();
System.Threading.Thread.Sleep(1000);
}
}
}
}
首先,不使用连接池做测试,即将配置文件Web.config修改如下:
<?xml version="1.0"?>
<configuration>
<connectionStrings>
<add name="ConnectionString"
connectionString="server=.;database=ASPNET4;
uid=sa;pwd=mawei;pooling=false;"/>
</connectionStrings>
……
</configuration>
其中,pooling=false表示不使用连接池,程序使用同一个连接串Open()/Close()了10次连接,使用性能计数器观察SQL Server的“理连接”数量。从图6-5中可以看出:每执行一次con.Open(),
图 6-5 pooling=false的情况
SQL Server的“物理连接”数量都增加1,而每执行一次con.Close(),SQL Server的“物理连接”数量都减少1。由于不使用连接池,每次Close连接的时候Data Provider需要把“逻辑连接”“物理连接”都销毁了,每次Open连接的时候Data Provider需要建立“逻辑连接”和“物理连接”。因此,图6-5中的波形呈锯齿状。
上面测试了不使用连接池((poling=false)的运行情况,接下来测试使用连接池的运行情况,即将配置文件Web.config修改如下:
<?xml version="1.0"?>
<configuration>
<connectionStrings>
<add name="ConnectionString"
connectionString="server=.;database=ASPNET4;
uid=sa;pwd=mawei;pooling=true;"/>
</connectionStrings>
……
</configuration>
现在运行程序,监视结果如图6-6所示。
图 6-6 pooling=true的情况
如图6-6所示,由于使用了连接池,每次Close()连接的时候Data Provider只需把“逻辑连接”释放回连接池,对应的“物理连接”则保持打开的状态。每次Open()连接的时候,Data Provider只需从连接池取出一个“逻辑连接”,这样就可以使用其对应“物理连接”而不需建立新的“物理连接”,因此,图6-6呈现出直线图。
注意连接池中包含打开的可重用的数据库连接。在同一时刻同一应用程序域中可以有多个连接池,但连接池不可以跨应用程序域共享。一个连接池是通过一个唯一的连接字符串来创建。连接池是根据第一次请求数据库连接的连接字符串来创建的,当另外一个不同的连接字符串请求数据库连接时,将创建另一个连接池。因此一个连接字符对应一个连接池,而不是一个数据库对应一个连接池。
6.3.3 连接池中的连接
上文阐述了连接池的概念。这时,你或许会问,一个连接池里到底需要放多少个连接才是最合理的?
其实,面对这样的问题,我们不能够给出一个准确的数据。因为一个连接池里的连接数不是静态的数量,它会随着连接池的不同状态而改变。这就涉及连接池建立的时候有多少个连接,什么时候连接会减少,什么时候会增加,连接数的上限是多少等问题。
通常,连接池是由连接池管理器维护的。当后续的连接请求到来,连接池管理器在连接池中寻找可用的空闲的连接,如果存在就交给应用程序使用。以下描述了当一个新的连接请求到来时连接管理器如何工作:
1)如果有未用连接可用,返回该连接。
2)如果池中连接都已用完,创建一个新连接添加到池中。
3)如果池中连接已达到最大连接数,请求进入等待队列,直到有空闲连接可用。
通过连接字符串中传递的参数可以控制连接池的连接,如表6-5所示。
1.连接的增加
连接池是为每个唯一的连接字符串创建的。一旦连接池被建立,就立即建立由Min Pool Size指定数量的连接。如果只有一个连接被占用,那么其他的连接(如果Min Pool Size大于1)为池里“可用的”连接。如果某进程有连接请求,而且请求的连接的连接串与该进程的某个连接池的连接串相同(如果进程里的所有连接池的连接串都不匹配,被请求的连接就需要建立新的连接池),那么如果该连接池里有“可用的”连接,就从连接池里取出一个“可用的”的连接使用,如果没有“可用的”连接就建立新的连接。
一旦程序运行连接的Close()或者Dispose()方法后,“被占用的”连接被释放回连接池变为“可用的”连接。在这里,需要区分连接池里的“连接的数量”与“可用的连接数量”。“连接的数量”指连接池里包括“被占用的连接数量”与“可用的连接的数量”;而“可用的连接数量”指系统现在能够使用的连接数量,即“连接的数量”减去“被占用的连接数量”。
如果Max Pool Size已经达到而且所有连接都被占用,新的连接请求需要等待。如果有被占用的连接释放回连接池,那么请求得到该连接;如果请求等待超过Connection Timeout的时间,程序会抛出InvalidOperationException。
2.连接的减少
通常,在如下两种情况下,连接池里的连接会减少:
1)每当一个连接使用完后释放回连接池,如果当前时间减去该连接建立的时间的值大于Connection Lifetime设定的值(秒),该连接被销毁。
一般情况下,Connection Lifetime参数常用于集群数据库环境下。例如,一个应用系统的中间层访问一个由3台服务器组成的集群数据库,该系统运行一段时间后发现数据库的负荷太大而需要增加第4台数据库服务器。如果不设置Connection Lifetime,会发现新增加的服务器很久都得不到连接,而原来3台服务器的负荷一点都没减少。这是因为中间层的连接一直都没有销毁,而建立新的连接的可能性很小(除非出现增加服务器之后数据库的并发访问量超过增加前的并发最大值)。
在这里需要说明的是,Connection Lifetime很容易让人产生误解。不要认为Connection Lifetime决定了一个连接的生存时间。因为只有连接被释放回连接池的时刻((Cose()连接之后),才会检查Connection Lifetime值是否达到,从而决定是否销毁连接;而连接在空闲或者正在使用的时候并不会检查Connection Lifetime。这意味着,在绝大多数情况下,连接从建立到销毁经过的时间比Connection Lifetime大。另外,如果Min Pool Size为N(N>0),那么连接池里有N个连接不受Connection Lifetime影响,这N个连接会一直在池里,直到连接池被销毁。
最后,如果应用系统不是使用集群数据库,则可以把Connection Lifetime设置为0。因为在单数据库服务器的环境下没必要把连接销毁,因为销毁之后的一段时间内又需要建立。
2)当发现某个连接对应的“物理连接”断开时,该连接被销毁。我们把这种连接称为“死连接”,例如数据库已经被shutdown、网络中断、SQL Server的连接进程被kill、Oracle的连接会话被kill等。“死连接”出现后不会立刻被发现,而是直到该连接被占用来访问数据库的时候才会被发现。
需要注意的是,如果执行Open()方法的时候,Data Provider只需从连接池取出已有的连接,那么Open()并没有访问数据库,所以这时“死连接”还不能被发现。
6.3.4 连接遗漏
前面已经阐述过,连接被打开((Oen())后,就需要执行Close()或者Dispose()方法后才会释放回连接池。但如果一个连接已经离开其代码有效范围,却还没被Close()或者Dispose(),则该连接就被泄漏了。“泄漏”的连接就是指:代码中已经不再使用某个连接,但该连接却还没有被释放回连接池。
如下面的代码中,每执行一次FA()就泄漏一个连接,而执行到第21次执行的时候就会抛出InvalidOperationException,因为最大连接数已到达,而且所有连接都已经被占用。
public void FA()
{
string connectionString=
"server=.;database=ASPNET4;uid=sa;pwd=mawei;
pooling=true;max pool size=20";
SqlConnection con=new SqlConnection(connectionString);
con.Open();
}
如果一个应用系统里存在会泄漏连接的代码,系统运行一段时间后连接就泄漏殆尽。即使把Max Pool Size设得很大也解决不了问题,因为单是一直存在太多的数据库连接已经让人不能容忍,况且这些是不能使用的“物理连接”。
要避免连接的泄漏,请注意下面几点:
1)除非使用CommandBehavior.CloseConnection作ExecuteReader参数,否则Close DataReader不会Close关联的连接。在多层结构的系统中,如果中间层向表现层返回DataReader,那么必须使用CommandBehavior.CloseConnection作ExecuteReader参数,这样当表现层执行DataReader的Close()方法时就会Close连接,不然表现层想帮助你也变得有心无力了。
2)执行DataAdapter的Fill和Update方法时,如果连接没有打开,那么DataAdapter会自动打开连接,执行完操作后自动关闭连接;但如果连接已经打开,DataAdapter执行完操作后不会帮你关闭连接,你需要自己负责关闭连接。
6.3.5 自定义连接池的实现类
为了能够更好地理解连接池的工作原理,并掌握连接池的使用方法,在这里自定义一个连接池类ConnectionPool,并在该类里实现连接池的相关功能,模拟连接池的运行。如代码清单6-1所示。
代码清单6-1 ConnectionPool.cs
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Collections;
using System.Configuration;
using System.Web;
namespace_6_1
{
public class ConnectionPool
{
//池管理对象
private static ConnectionPool connectionPool=null;
//池管理对象实例
private static Object objlock=typeof(ConnectionPool);
//池中连接数
private static int poolSize=100;
//已经使用的连接数
private int useConnectionCount=0;
//连接保存的集合
private ArrayList poolArrayList=new ArrayList();
//连接字符串
private static string connectionString="";
public static int PoolSize
{
set
{
poolSize=value;
}
}
public static string ConnectionString
{
set
{
connectionString=value;
}
}
///<summary>
///创建获取连接池对象
///</summary>
///<returns></returns>
public static ConnectionPool GetPool()
{
lock(objlock)
{
if(connectionPool==null)
{
connectionPool=new ConnectionPool();
}
return connectionPool;
}
}
///<summary>
///获取池中的连接
///</summary>
///<returns></returns>
public SqlConnection GetConnection()
{
lock(poolArrayList)
{
SqlConnection tmp=null;
if(poolArrayList.Count>0)
{
tmp=((SlConnection)poolArrayList[0];
poolArrayList.RemoveAt(0);
//不成功
if(!IsConnection(tmp))
{
//可用的连接数据已去掉一个
useConnectionCount——;
tmp=GetConnection();
}
}
else
{
//可使用的连接小于连接数量
if(useConnectionCount<poolSize)
{
try
{
//创建连接
SqlConnection conn=
new SqlConnection(connectionString);
conn.Open();
useConnectionCount++;
tmp=conn;
}
catch(Exception ex)
{
throw ex;
}
}
}
return tmp;
}
}
///<summary>
///关闭连接,加连接回到池中
///</summary>
///<param name="con">SqlConnection对象</param>
public void closeConnection(SqlConnection con)
{
lock(poolArrayList)
{
if(con!=null)
{
poolArrayList.Add(con);
}
}
}
///<summary>
///目的保证所创连接成功,测试池中连接
///</summary>
///<param name="con">SqlConnection对象</param>
///<returns></returns>
private bool IsConnection(SqlConnection con)
{
//主要用于不同用户
bool result=true;
if(con!=null)
{
string sql="select 1";//随便执行对数据库操作
SqlCommand cmd=new SqlCommand(sql, con);
try
{
cmd.ExecuteScalar().ToString();
}
catch
{
result=false;
}
}
return result;
}
}
}
在代码清单6-1中,使用了ArrayList类型的变量poolArrayList来保存连接集合,并在GetConnection()方法里实现了连接池中连接的管理。这样,在获取连接时,就不用创建连接而直接从池中获取数据。使用示例如下面的代码所示:
protected void Page_Load(object sender, EventArgs e)
{
string strsql="select*from SiteMap";
ConnectionPool.PoolSize=10;
ConnectionPool.ConnectionString=
WebConfigurationManager.ConnectionStrings
["ConnectionString"].ConnectionString;
SqlDataAdapter adapter=new SqlDataAdapter(strsql,
ConnectionPool.GetPool().GetConnection());
DataSet ds=new DataSet();
adapter.Fill(ds);
GridView1.DataSource=ds;
GridView1.DataBind();
}
上面的示例运行结果如图6-7所示。
图 6-7 自定义连接池的测试示例