Ads by Lake Quincy Media

eaglet

本博专注于基于微软技术的搜索相关技术
posts - 177, comments - 3006, trackbacks - 26, articles - 0
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

作者: 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 是否设置合理。

标签: 数据库

Feedback

#1楼  回复 引用 查看   

2011-10-31 14:21 by 阿建      
gooooooooooooooooood
非常有学习的价值

#2楼  回复 引用 查看   

2011-10-31 14:52 by billzhao      
不错,好东东,有机会测试一哈

#3楼  回复 引用 查看   

2011-10-31 16:04 by 蕲人ZB      
非常好的东西 , 值得学习.

#4楼  回复 引用 查看   

2011-10-31 16:55 by 君之蘭      
如果最小连接数开的大的话,如果访问量大,那么数据库压力也会非常大。
如果访问量小就开大也没用啊。所以我觉得还是尽量让查询以最短的时间close,链接数再大,io能力有限。感觉还是小一点比较好。

#5楼  回复 引用 查看   

2011-10-31 17:08 by 草根豆      
学习了..

#6楼  回复 引用 查看   

2011-10-31 18:40 by 那一剑风情      
收藏

#7楼  回复 引用 查看   

2011-10-31 21:28 by ocean      
为了保证在大并发时,尽量少的创建新的物理连接,我们可以适当把 Min Pool Size 调大一些,但也不要太大,因为单个机器TCP链路的数量是有限的

连接池设置中有一个最小连接池大小,默认为0,我们把它设置为大于0的值就可以保持若干物理连接始终不释放了
------------------------------------------------------
上面这个描述的有点问题。MinPoolSize实际上是指第一次创建连接池的时候,打开的物理连接数,默认为0,表示连接池打开的时候,只有一个物理连接,如果我们设置为5,则表示连接池打开的时候,就先建立5个物理连接,然后放在池里面。这个时候当第一次访问数据库的时候,因为要建立5个数据库连接,所以第一次时间会比较长一点。MinPoolSize和最大并发数没有关系,和最大并发数有关系的是MaxPoolSize,这个默认值是100,所以如果你在连接字符串中不指明MaxPoolSize,则最多就是100个物理连接,如果这100个物理连接都在使用,这时又有新线程要Open,则这个线程会等待,直到pool中有可用连接,如果一直没有可用连接,则在经过Connection Timeout的时间之后,返回异常。

当一个连接建立后,close后是回归到连接池的,而不是释放物理连接,但是连接字符串有一个Connection Lifetime属性,当回归连接池的时候,会将连接的创建时间和当前时间进行比较,如果超过了Connection Lifetime,则连接将被销毁。

另外sql server可以接受的最大连接数为32767个。所以一般情况下不用担心连接太多。但是要注意MaxPoolSize,很多人不设置,这个时候就意味着最多100个物理连接(默认值)。

通常建议的做法是MinPoolSize保持默认的0值,MaxPoolSize设置为可能发生的最大连接数。

#8楼  回复 引用 查看   

2011-11-01 10:57 by 钢钢      
很实在的好文章!

@ocean
MinPoolSize = n 是为了保持若干物理连接始终不释放!

#9楼  回复 引用 查看   

2011-11-01 16:23 by magemaster      
你的字体可不可以设置小些

#10楼  回复 引用 查看   

2011-11-01 19:51 by ocean      
物理连接是否释放,和Connection Timeout有关,和MinPoolSize其实关系不大,在连接池中的连接本来就不会释放,随着用户的使用,会自动的达到一个平衡的状态。通常情况下,MinPoolSize仅仅在第一次初始化数据库的时候有用处,即生成连接池的时候最开始初始化的数据库连接。通常并发数都比MinPoolSize大,假设你MinPoolSize=50,这个值只是连接池最初生成的时候就直接打开50个连接,你的同时并发数可能是200,随着用户的访问,物理连接会打开的越来越多,如果大多数情况下你的并发连接都是200,那么池里面的物理连接就始终都会保持在200以上,和MinPoolSize没关系,如果开了200个物理连接。这个时候如果你的并发数猛然降到100,那么多余的100个连接会在Pool里面呆着,而不会销毁,也就是仍然维持了200个连接。所以只要MaxPoolSize设置相对大一些就成了。相反,如果你的并发数通常在30左右,那如果MinPoolSize设置为50,则会浪费20个连接的资源,所以通常建议MinPoolSize设置为0,MaxPoolSize设置为比较大,这样池里面的连接数基本是根据用户的并发数自适应的。

#11楼[楼主]  回复 引用 查看   

2011-11-02 06:55 by eaglet      
@magemaster
字体在我这里看很小啊。是不是浏览器设置的问题?

#12楼  回复 引用 查看   

2011-11-08 11:21 by 个人知识管理      
说实话,连接池提升性能是基本常识。怎样用好连接池,才是难点。还以为讲的是后者。

#13楼  回复 引用 查看   

2011-11-12 22:34 by 心若冰清      
学习了,,,让我了解了。收藏

#14楼  回复 引用 查看   

2011-11-15 16:10 by 银杏叶儿      
了解

#15楼  回复 引用 查看   

2011-11-17 11:47 by 湿袜子      
学问呢,真是让人眼花缭乱啊