(重要SqlConnection)正确理解 SqlConnection 的连接池机制

20191104又新增加的内容:

==========================================================
综合本篇文章以及CSDN对连接池的讲述,本人对连接池的理解如下(为了突出重点将我的理解放在了这篇文章前面):
1,连接到数据库服务器通常由几个很长时间的步骤组成,必须建立物理通道(例如套接字或命名管道),必须与服务器进行初次握手,必须分析连接字符串信息,必须由服务器对连接进行身份验证,必须运行检查以便在当前事务中登记,等等。
2,在初次打开连接时(初次new SqlConnection(connectionString)并open后),将根据完全匹配算法去创建连接池,该算法将池与连接中的连接字符串关联。每个连接池都与一个不同的连接字符串连接字符串相关联。不同的连接字符串对应不同的连接池。如下示例:

using (SqlConnection connection = new SqlConnection(  
  "Integrated Security=SSPI;Initial Catalog=Northwind"))  
    {  
        connection.Open();        
        // Pool A is created.  
    }  
  
using (SqlConnection connection = new SqlConnection(  
  "Integrated Security=SSPI;Initial Catalog=pubs"))  
    {  
        connection.Open();        
        // Pool B is created because the connection strings differ.  
    }  
  
using (SqlConnection connection = new SqlConnection(  
  "Integrated Security=SSPI;Initial Catalog=Northwind"))  
    {  
        connection.Open();        
        // The connection string matches pool A.  not create
    }  

3,new SqlConnection(connectionString),只单单创建和初始化一个连接到SqlServer数据库的连接对象变量,但是这个对象还没有指定具体的物理连接,还需要open()操作来引用一个有效的物理连接。
4,SqlConnection的Open操作,首先会在与创建该连接对象使用的连接字符串匹配的连接池中,查找空闲可用的物理连接,然后返回这个物理连接给创建的连接对象。如果没有可用的连接,那么会创建一个新的连接,这个时候,会耗费时间到建立物理通道,与服务器握手,鉴权等等操作过程中。注意,连续调用多次Open会报异常,Open与Close要成对出现。
5,SqlConnection的Close,Dispose操作不会关闭连接对象获取的物理连接,而是会将物理连接重新释放到连接池中。所以,用完连接后一定要关闭连接,以便连接回返连接池。using语句,自带Dispose方法。Dispose是对close的调用,额外的操作是将Connection置null。
6,当MinPoolSize在连接字符串中未指定,或指定为零的时候,建立的连接池中将没有可用的连接,并且池中的连接在一段时间不活动后将关闭。如果连接字符串中指定了MinPoolSize,在建立的连接池中会预先创建MinPoolSize个的物理连接,并且应用程序整个生存周期连接不会被破坏。
7,默认连接池的最大连接数量为100.
8,如果调用Open从连接池获取连接时,没有可用连接,则该请求将会排队,当排队请求在达到超时时间(默认15秒)前还无法满足请求,将引发异常。
9,如果连接池中的某个连接的空闲时间达到大约 4-8 分钟,或池进程检测到与服务器的连接已断开,连接池进程会将该连接从池中移除。
10,当用静态对象存储SqlConnection对象时,在多线程中将引发异常。 因为每次Open获取到的都是同一个物理连接,静态对象会一直引用的这个物理连接。 当多线程中,若有个线程打开还未关闭的时候,另一个线程又打开的话,将引发异常。或者当连接长时间不使用的时候,该物理连接将断开,后面在操作数据库的时候将提示连接已断开异常。
MSDN的解释-SQL Server 连接池 (ADO.NET)
该连接的回答- 杨中科老师错了?sql连接不应该用using语句 C#

===========================================================

==========================================================
在.net 页面中链接SQL Server 数据库有专门的连接数据库的类,也就是SqlConnection,其专门用来链接SQL Server 数据库。
相比较与OleDbConnection去连接数据库,用SqlConnection效率更高。
类的创建方法如下:
SqlConnection conn = new SqlConnection(@“Data Source=.\SQLEXPRESS;Initial Catalog=Test; Integrated Security =True”);
解释一下字符串的含义,Data Source 指定数据库服务器的名称,Initial Catalog 指定数据库的名称,Test 为此例要连接的数据库名字,Integrated Security = True 的意思时采用Windows登陆方式登陆。如果采用SQLServer 的方式登陆,就换成uid用户名和pwd密码即可,链接字符串中每部分使用分号间隔。

C#中数据库的链接 - SqlConnection类的使用

===========================原先的 ==========================
作者: eaglet

.net 中通过 SqlConnection 连接 sql server,我们会发现第一次连接时总是很耗时,但后面连接就很快,这个其实和SqlConnection 的连接池机制有关,正确的理解这个连接池机制,有助于我们编写高效的数据库应用程序。

很多人认为 SqlConnection 的连接是不耗时的,理由是循环执行 SqlConnection.Open 得到的平均时间几乎为0,但每次首次open 时,耗时又往往达到几个毫秒到几秒不等,这又是为什么呢?

首先我们看一下 MSDN 上的权威文档上是怎么说的
Connecting to a database server typically consists of several time-consuming steps. A physical channel such as a socket or a named pipe must be established, the initial handshake with the server must occur, the connection string information must be parsed, the connection must be authenticated by the server, checks must be run for enlisting in the current transaction, and so on.
以上摘自 http://msdn.microsoft.com/en-us/library/8xx3tyca%28VS.80%29.aspx
也就是说物理连接建立时,需要做和服务器握手,解析连接字符串,授权,约束的检查等等操作,而物理连接建立后,这些操作就不会去做了。这些操作是需要一定的时间的。**所以很多人喜欢用一个静态对象存储 SqlConnection 来始终保持物理连接,但采用静态对象时,多线程访问会带来一些问题,**实际上,我们完全不需要这么做,因为 SqlConnection 默认打开了连接池功能,当程序 执行 SqlConnection.Close 后,物理连接并不会被立即释放,所以这才出现当循环执行 Open操作时,执行时间几乎为0.

下面我们先看一下不打开连接池时,循环执行 SqlConnection.Open 的耗时

public static void OpenWithoutPooling()
        {
            string connectionString =
                "Data Source=192.168.10.2; Initial Catalog=News; Integrated Security=True;Pooling=False;";

            Stopwatch sw = new Stopwatch();

            sw.Start();
            using (SqlConnection conn =
                new SqlConnection(connectionString))
            {
                conn.Open();
            }

            sw.Stop();
            Console.WriteLine("Without Pooling, first connection elapsed {0} ms", sw.ElapsedMilliseconds);

            sw.Reset();

            sw.Start();

            for (int i = 0; i < 100; i++)
            {
                using (SqlConnection conn = new SqlConnection(connectionString))
                {
                    conn.Open();
                }
            }

            sw.Stop();
            Console.WriteLine("Without Pooling, average connection elapsed {0} ms", sw.ElapsedMilliseconds / 100);
        }

SqlConnection 默认是打开连接池的,如果要强制关闭,我们需要在连接字符串中加入 Pooling=False
调用程序如下:

Test.SqlConnectionTest.OpenWithoutPooling();
                Console.WriteLine("Waiting for 10s");
                System.Threading.Thread.Sleep(10 * 1000);
                Test.SqlConnectionTest.OpenWithoutPooling();
                Console.WriteLine("Waiting for 600s");
                System.Threading.Thread.Sleep(600 * 1000);
                Test.SqlConnectionTest.OpenWithoutPooling();

下面是测试结果Without Pooling, first connection elapsed 13 ms
Without Pooling, average connection elapsed 5 ms
Wating for 10s
Without Pooling, first connection elapsed 6 ms
Without Pooling, average connection elapsed 4 ms
Wating for 600s
Without Pooling, first connection elapsed 7 ms
Without Pooling, average connection elapsed 4 ms

从这个测试结果看,关闭连接池后,平均每次连接大概要耗时4个毫秒左右,这个就是建立物理连接的平均耗时。

下面再看默认情况下的测试代码

public static void OpenWithPooling()
        {
            string connectionString =
                "Data Source=192.168.10.2; Initial Catalog=News; Integrated Security=True;";
            
            Stopwatch sw = new Stopwatch();

            sw.Start();
            using (SqlConnection conn =
                new SqlConnection(connectionString))
            {
                conn.Open();
            }

            sw.Stop();
            Console.WriteLine("With Pooling, first connection elapsed {0} ms", sw.ElapsedMilliseconds);

            sw.Reset();

            sw.Start();

            for (int i = 0; i < 100; i++)
            {
                using (SqlConnection conn = new SqlConnection(connectionString))
                {
                    conn.Open();
                }
            }

            sw.Stop();
            Console.WriteLine("With Pooling, average connection elapsed {0} ms", sw.ElapsedMilliseconds / 100);
        }

调用代码

Test.SqlConnectionTest.OpenWithPooling();
                Console.WriteLine("Waiting for 10s");
                System.Threading.Thread.Sleep(10 * 1000);
                Test.SqlConnectionTest.OpenWithPooling();
                Console.WriteLine("Waiting for 600s");
                System.Threading.Thread.Sleep(600 * 1000);
                Test.SqlConnectionTest.OpenWithPooling();

测试结果
With Pooling, first connection elapsed 119 ms
With Pooling, average connection elapsed 0 ms
Waiting for 10s
With Pooling, first connection elapsed 0 ms
With Pooling, average connection elapsed 0 ms
Waiting for 600s
With Pooling, first connection elapsed 6 ms
With Pooling, average connection elapsed 0 ms

这个测试结果看,第一次耗时是119ms,这是因为我在测试代码中,首先运行的是这个测试过程,119 ms 是程序第一次启动时的首次连接耗时,这个耗时可能不光包括连接数据库的时间,还有 ado.net 自己初始化的用时,所以这个用时可以不管。10秒以后在执行这个测试过程,首次执行的时间变成了0ms,这说明连接池机制发生了作用,SqlConnection Close 后,物理连接并没有被关闭,所以10秒后再执行,连接几乎没有用时间。

但我们发现一个有趣的现象,10分钟后,首次连接时间变成了6ms,这个和前面不打开连接池的测试用时几乎一样,也就是说10分钟后,物理连接被关闭了,又重新打开了一个物理连接。这个现象是因为连接池有个超时时间,默认情况下应该在5-10分钟之间,如果在此期间没有任何的连接操作,物理连接就会被关闭。那么我们有没有办法始终保持物理连接呢?方法是有的。

**连接池设置中有一个最小连接池大小,默认为0,我们把它设置为大于0的值就可以保持若干物理连接始终不释放了。**看代码

public static void OpenWithPooling(int minPoolSize)
        {
            string connectionString =
                string.Format("Data Source=192.168.10.2; Initial Catalog=News; Integrated Security=True;Min Pool Size={0}",
                    minPoolSize);

            Stopwatch sw = new Stopwatch();

            sw.Start();
            using (SqlConnection conn =
                new SqlConnection(connectionString))
            {
                conn.Open();
            }

            sw.Stop();
            Console.WriteLine("With Pooling Min Pool Size={0}, first connection elapsed {1} ms", 
                minPoolSize, sw.ElapsedMilliseconds);

            sw.Reset();

            sw.Start();

            for (int i = 0; i < 100; i++)
            {
                using (SqlConnection conn = new SqlConnection(connectionString))
                {
                    conn.Open();
                }
            }

            sw.Stop();
            Console.WriteLine("With Pooling Min Pool Size={0}, average connection elapsed {1} ms", 
                minPoolSize, sw.ElapsedMilliseconds / 100);
        }

其实只要在连接字符串中加入一个 Min Pool Size=n 就可以了。
调用代码

Test.SqlConnectionTest.OpenWithPooling(1);
                Console.WriteLine("Waiting for 10s");
                System.Threading.Thread.Sleep(10 * 1000);
                Test.SqlConnectionTest.OpenWithPooling(1);
                Console.WriteLine("Waiting for 600s");
                System.Threading.Thread.Sleep(600 * 1000);
                Test.SqlConnectionTest.OpenWithPooling(1);

With Pooling Min Pool Size=1, first connection elapsed 5 ms
With Pooling Min Pool Size=1, average connection elapsed 0 ms
Waiting for 10s
With Pooling Min Pool Size=1, first connection elapsed 0 ms
With Pooling Min Pool Size=1, average connection elapsed 0 ms
Waiting for 600s
With Pooling Min Pool Size=1, first connection elapsed 0 ms
With Pooling Min Pool Size=1, average connection elapsed 0 ms

我们可以看到当 Min Pool Size = 1 时,除了首次连接用时5ms以外,即便过了10分钟,用时还是0ms,物理连接没有被关闭。

多线程调用问题

多线程调用我也做了测试,这里不贴代码了,我大概讲一下结果。如果是多线程访问 SqlConnection ,注意是通过 new SqlConnection 方式访问,

那么这里有两个问题,如果后一个线程在前一个线程 Close 前调用了Open操作,那么 Ado.net 不可能复用一个物理连接,它将为第二个线程分配一个新的物理连接。如果后一个线程 Open 时,前一个线程已经 Close 了,则新的线程使用前一个线程的物理连接。也就是说,如果同时有n个线程连接数据库,最多情况下会创建n条物理连接,最少情况下为1条。如果创建n条物理连接,则用时理论上等于 n * t / cpu , n 为线程数,t 为每次创建物理连接的用时,前面测试的结果大概是5-10ms左右,cpu 为当前机器的CPU数量。另外网络,服务器的负荷也影响这个用时。为了保证在大并发时,尽量少的创建新的物理连接,我们可以适当把 Min Pool Size 调大一些,但也不要太大,因为单个机器TCP链路的数量是有限的,详见我另外一篇文章 Windows 下单机最大TCP连接数

连接字符串中关于 连接池方面的参数

见下面链接 SqlConnection.ConnectionString Property

IIS 回收应用程序池对连接池的影响

在做 ASP.NET 程序时,我们会发现,如果网站20分钟不访问,再次访问就会比较慢,这是因为IIS默认的 idle timeout 是20分钟,如果在20分钟内没有一个访问,IIS 将回收应用程序池,回收应用程序池的结果就相当于应用程序被重启,所有原来的全局变量,session, 物理连接都将清空。回收应用程序池后首次访问,相当于前面我们看到的程序启动后第一次访问数据库,连接的建立时间将比较长。所以如果网站在某些时段访问量很少的话,需要考虑 idle timeout 是否设置合理。

你可能感兴趣的:(C#)