ADO.NET數據連接池

【IT168 技術文檔】21世紀什麼最貴?數據庫連接。對於以數據庫做數據存儲基石的應用系統來說,數據庫連接是整個系統中最珍貴的資源之一。數據庫連接池是爲了更有效地利用數據庫連接的最重要措施。它對於一個大型的應用系統的性能至關重要,特別是Web應用。ADO.NET Data Provider(以下簡稱Data Provider)會幫我們管理連接池,所以有人說使用連接池就像遊兒童池一樣輕鬆。但並不是說有了Data Provider程序員就萬事無憂的,不正確地使用連接池可能導致你的應用在池裏淹死。筆者希望通過本文能讓讀者徹底明白連接池的重要性以及能根據實際情況正確配置連接池的參數,明白實際應用中出現的連接泄漏、“死連接”等異常情況和應對方法,讓應用輕鬆暢遊連接池。本文主要介紹ADO.NET 1.1的連接池。


    1、什麼是連接池 

    連接池是Data Provider提供的一個機制,使得應用程序使用的連接保存在連接池裏而避免每次都要完成建立/關閉連接的完整過程。要理解連接池,先要理解程序裏SqlConnection.Open()、SqlConnection.Close()和打開/關閉一個“物理連接”的關係。 

    Data Provider在收到連接請求時建立連接的完整過程是:先連接池裏建立新的連接(即“邏輯連接”),然後建立該“邏輯連接”對應的“物理連接”。建立“邏輯連接”一定伴隨着建立“物理連接”。Data Provider關閉一個連接的完整過程是先關閉“邏輯連接”對應的“物理連接”然後銷燬“邏輯連接”。銷燬“邏輯連接”一定伴隨着關閉“物理連接”。SqlConnection.Open()是向Data Provider請求一個連接,Data Provider不一定需要完成建立連接的完整過程,可能只需要從連接池裏取出一個可用的連接就可以;SqlConnection.Close()是請求關閉一個連接,Data Provider不一定需要完成關閉連接的完整過程,可能只需要把連接釋放回連接池就可以。 

    下面以一個例子來說明。本文例子都使用Console Application。我們使用操作系統的性能監視器來比較使用連接池與否,數據庫的“物理連接”數量的不同。因爲性能監視器至少每一秒採集一次數據,爲方便觀察效果,代碼中Open和Close連接後都Sleep一秒。

SqlConnection con = new SqlConnection("server = .;
database = northwind;pooling = false;trusted_connection = true");
for(int i = 0;i < 10;i++) { try { con.Open(); System.Threading.Thread.Sleep(1000); } catch(Exception e){Console.WriteLine(e.Message);} finally { con.Close(); System.Threading.Thread.Sleep(1000); } }

    首先,不使用連接池做測試。以上程序中pooing = false表示不使用連接池,程序使用同一個連接串Open & Close了10次連接,使用性能計數器觀察SQL Server的“物理連接”數量。從下面的鋸齒圖可以看出每執行一次con.Open(),SQLServer的“物理連接”數量都增加一,而每執行一次con.Close(),SQLServer的“物理連接”數量都減少一。由於不使用連接池,每次Close連接的時候Data Provider需要把“邏輯連接”和“物理連接”都銷燬了,每次Open連接的時候Data Provider需要 建立“邏輯連接”和“物理連接”,鋸齒圖因此而成。

圖1

    下面啓用連接池再測試一次。把連接串的pooling參數改爲true,另外在for循環後加上Console.Read()。

    從下圖可以看出,從第一次Open到第執行完Console.Read()這段時間內,SQL Server的“物理連接”數量一直保持爲1,直到關閉console應用程序的進程後SQL Server的“物理連接”數量才變爲0。由於使用了連接池,每次Close連接的時候Data Provider只需把“邏輯連接”釋放回連接池,對應的“物理連接”則保持打開的狀態。每次Open連接的時候,Data Provider只需從連接池取出一個“邏輯連接”,這樣就可以使用其對應“物理連接”而不需建立新的“物理連接”,直線圖因此而成。

圖2

    在ADO.NET 1.1下使用性能計數器觀察連接池有關計數器需要注意兩個bug。 

    (1)當應用程序進程關閉後,計數器“SqlClient: Current # pooled connections”和“SqlClient: Current # connection pools”不會減爲0,所以每重新運行一次應用程序性能計數器的值在上次的值的基礎上一直累加。這是計數器的錯誤顯示,實際上當應用程序關閉後connection pool和pooled connection就減爲0。因爲關閉應用程序後把性能監視器也關閉,重啓應用程序後再重新打開性能監視器就可以看出“SqlClient: Current # pooled connections”和“SqlClient: Current # connection pools”是重新從0開始上升的。 

    (2)用斷點調試的情況下,連接串爲"server = .;database = northwind;pooling = true;trusted_connection = true" 的connnection第一次Open的時候“SqlClient: Current # pooled connections”就從0變爲2。但根據連接串參數的意義,只Open了一個connection,“SqlClient: Current # pooled connections”應該從0變爲1(圖2是在沒有斷點調試的情況下得出的曲線)。這不是計數器顯示錯誤,而是ADO.ENT 1.1本身的bug,因爲“User Connections”也隨着“SqlClient: Current # pooled connections”從0變爲2。 

    爲什麼需要連接池? 

    完成建立/關閉一個連接的完整過程是一個消耗大量資源和時間的一個過程。想象一下一個ASP.NET的系統,裏面包含大量訪問數據庫的代碼片,系統有大量的用戶同時在使用系統,如果程序每次Open/Close一個連接Data Provider都完成建立/關閉一個連接的完整過程,這樣的系統性能肯定讓人無法接受。 

    Data Provider提供連接池並通過連接池實現“物理連接”重複使用而避免頻繁地建立和關閉“物理連接”,從而大大提高應用系統的性能。圖1描述一個應用的不同Client App使用連接池訪問數據庫,Data Provider負責建立和管理一個或者多個的連接池,每一個連接池裏有一個或者多個連接,池裏的連接就是“邏輯連接”。連接池裏有N個連接表示該連接池與數據庫之間有N個“物理連接”。增加一個連接,連接池與數據庫的“物理連接”就增加一個,減少一個連接,連接池與數據庫的“物理連接”就減少一個。

圖3

 

3.1增加連接 

    一旦連接池被建立,就立即建立由Min Pool Size指定數量的連接。如果只有一個連接被佔用,那麼其他的連接(如果Min Pool Size大於1)爲池裏“可用的”連接。如果某進程有連接請求而且請求的連接的連接串與該進程的某個連接池的連接串相同(如果進程裏的所有連接池的連接串都不匹配被請求的連接就需要建立新的連接池),那麼如果該連接池裏有“可用的”連接就從連接池裏取出一個“可用的”的連接使用,如果沒有“可用的”連接就建立新的連接。一旦程序運行連接的Close或者Dispose方法後,“被佔用的”連接被釋放回連接池變爲“可用的”連接。需要區分連接池裏“連接的數量”與“‘可用的’連接數量”。“連接的數量”指連接池裏包括“被佔用的”連接與“可用的”連接的數量。 

    如果Max Pool Size已經達到而且所有連接都被佔用,新的連接請求需要等待。如果有被佔用的連接釋放回連接池,那麼請求得到該連接;如果請求等待超過Connection Timeout的時間,程序會拋出InvalidOperationException。 

    3.2減少連接 

    兩種情況下連接池裏的連接會減少。 

    (1)每當一個連接使用完後釋放回連接池,如果當前時間減去該連接建立的時間的值大於Connection Lifetime設定的值(秒),該連接被銷燬。Connection Lifetime是用於集羣數據庫環境下。例如一個應用系統的中間層訪問一個由3臺服務器組成的集羣數據庫,該系統運行一段時間後發現數據庫的負荷太大而需要增加第4臺數據庫服務器。如果不設置Connection Lifetime,你會發現新增加的服務器很久都得不到連接而原來3臺服務器的負荷一點都沒減少。這是因爲中間層的連接一直都不會銷燬而建立新的連接的可能性很小(除非出現增加服務器之後數據庫的併發訪問量超過增加前的併發最大值)。 

    注意:Connection Lifetime很容易讓人產生誤解。不要認爲Connection Lifetime決定了一個連接的生存時間。因爲只有連接被釋放回連接池的時刻(Close連接之後)纔會檢查Connection Lifetime值是否達到而決定是否銷燬連接,而連接在空閒或者正在使用的時候並不會檢查Connection Lifetime。這意味着絕大多數情況下連接從建立到銷燬經過的時間比Connection Lifetime大。另外,如果Min Pool Size爲N (N > 0),那麼連接池裏有N個連接不受Connection Lifetime影響。這N個連接會一直在池裏直到連接池被銷燬。 

    (2)當發現某個連接對應的“物理連接”斷開(這種連接稱爲“死連接”),例如數據庫已經被shutdown、網絡中斷、SQL Server的連接進程被kill、Oracle的連接會話被kill,該連接被銷燬。“死連接”出現後不是立刻被發現,直到該連接被佔用來訪問數據庫的時候纔會被發現。 

    注意:如果執行Open()方法時候Data Provider只需從連接池取出已有的連接,那麼Open()並沒有訪問數據庫,所以這時候“死連接”還不能被發現。 

    下面以一個例子詳細解釋一個連接池從建立起到進程結束連接數的變化情況。
string connectionString = "server = .;database = northwind;user = sa;
password = sqlserver;min pool size = 2;max pool size = 5;
connection lifetime
= 20;connection timeout = 10";
SqlConnection[] connections = new SqlConnection[7];
for(int i = 0;i < connections.Length;i++) connections[i] = new SqlConnection(connectionString); Open connection[0],8秒後Open connection[1] …8秒後Close connection[0],10秒後Open connection[0] …5秒後Open connection[2][3][4],每隔兩秒打開一個 Console.WriteLine("Now the Max Pool Size is reached and
we try
to open connection[5]./r/n"); for(int i = 0;i < 2;i++) { try {connections[5].Open();} catch(InvalidOperationException e) { if(i == 1) return; Console.WriteLine("Can't open connection[5]./r/n" + e.Message); connections[4].Close(); Console.WriteLine("/r/nTry to open connection[5] again."); continue; } } Console.WriteLine("connection[5] is open."); foreach(SqlConnection con in connections) { if(con.State == ConnectionState.Open) { con.Close(); Console.WriteLine("A connection is released back to the pool."); System.Threading.Thread.Sleep(5000); } }

     使用性能監視器觀察,得到圖4所示結果。我們觀察.NET CLR Data的“SqlClient: Current # connection pools”、“SqlClient: Current # pooled connections”以及Sql Server: General Statistic的User Connections計數器。

圖4

    由於Min Pool Size = 2,所以open connection[0]的時候連接池裏就建立了兩個連接。之後open connection[1]、close connection[0]、open connection[0]這段時間裏連接池連接數保持爲2,因爲open連接的併發數量都沒超過2。接着,相繼open connection[2]、[3]、[4],因爲每次請求連接的時候連接池裏都沒有“可用的”連接,所以每請求一個連接連接數量就增加1,一直攀升到Max Pool Size(5)。這時候connection[0]、[1]的生存時間已經超過Connection Lifetime,但由於它們還沒有被Close,所以還會繼續生存。接着嘗試再請求連接,這時候因爲Max Pool Size已達而池裏所有連接都被佔用,所以第一次嘗試失敗。進行第二次嘗試前先close connection[4],這樣就有一個連接被釋放回連接池,第二次嘗試成功。最後close所有打開的connection,每隔5秒close一個,所有connection被close的時候它們的生存時間都大於Connection Lifetime,但由於Min Pool Size = 2,所以只有3個connection被銷燬。 

    另外強調兩點: 

    (1)可用看出增加/減少一個連接池的連接,User Connections(即“物理連接”)隨着增加/減少一個。(爲方便觀察,先用Sql Query Analyzer打開一個用戶連接) 

    (2)由於使用相同連接串,所以由始至終只有一個連接池。

4、連接泄漏

   
前面說過,連接被打開後需要執行Close或者Dispose方法後纔會釋放回連接池。如果一個連接已經離開其代碼有效範圍,但還沒被Close或者Dispose,該連接就被泄漏了。所謂泄漏的連接就是代碼中已經不再使用某個連接但該連接卻還沒有被釋放回連接池。下面代碼中,每執行一次Method()就泄漏一個連接,第11次執行的時候就會拋出InvalidOperationException,因爲最大連接數已達而且所有連接都已經被佔用。
private void Method() { string conString = "server = .;database = northwind;user = sa;
password
= sqlserver;max pool size = 10"; SqlConnection con = new SqlConnection(conString); 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執行完操作後不會幫你關閉連接,你需要自己負責關閉連接。 

    5、處理“死連接” 

    “可用的”連接一定能訪問數據庫?不一定。 

    在前面“減少連接”的部分提過,在數據庫被shutdown、網絡中斷、數據庫連接進程/會話被kill情況下連接池會產生“死連接”。“死連接”指連接池裏某個連接對應的“物理連接”已經斷開,但ClientApp執行Open方法時候可以從連接池取得該連接,直到執行數據庫操作Data Provider才發現該連接是“死連接”。注意區分“死連接”和泄漏的連接。 

    “死連接”是“邏輯連接”,是“可用的”連接,但該“邏輯連接”對應的“物理連接”已經不存在;泄漏的連接指“物理連接”存在而對應的“邏輯連接”實際沒有被佔有但被標識爲“被佔用”而導致該“邏輯連接”不能被使用。 

    發現“死連接”後Data Provider會銷燬該連接並拋出SqlException但不會自動嘗試使用其他連接,即使在ADO.NET 2.0裏也是如此。把exception catch下來,然後提示用戶重新操作不是最好的處理方式。不管微軟爲什麼不幫我們嘗試其他連接,我們只能接受現實自己解決。 

    下面例子裏Helper的ExecuteReader把Data Provider拋出的SqlException catch後先把連接置爲“無效”,然後再嘗試使用其他連接,如果再嘗試的次數達到預定值還不成功才拋出SqlException。

public class Helper { private static int TimesTry = 0,MaxTry = 5; public static SqlDataReader ExecuteReader(string conStr,CommandType eType,
string commandText) { SqlConnection cn
= null; SqlDataReader dr = null; SqlCommand cmd = null; try { cn = new SqlConnection(conStr); cmd = new SqlCommand(commandText,cn); cmd.CommandType = eType; cn.Open(); dr = cmd.ExecuteReader(CommandBehavior.CloseConnection); } catch(SqlException e) { if(dr != null) dr.Close(); cn.Close(); System.Threading.Thread.Sleep(2000); if(TimesTry < MaxTry) { dr = ExecuteReader(conStr,eType,commandText); TimesTry++; } else throw e; } return dr; } } string conString = "server = .;database = northwind;
user = sa;max pool size = 1;password = sqlserver;
Application Name
= DeadConnectionExample"; SqlDataReader reader = Helper.ExecuteReader(conString,CommandType.Text,
"
select * from orders"); reader.Close(); System.Threading.Thread.Sleep(15000); SqlConnection con = new SqlConnection("server = .;database = master;
user = sa;password = sqlserver;pooling = false"); con.Open(); SqlCommand cmd = new SqlCommand("SELECT SPID FROM master.dbo.sysprocesses
WHERE PROGRAM_NAME = 'DeadConnectionExample'",con); string spid = cmd.ExecuteScalar().ToString(); cmd = new SqlCommand("kill " + spid,con); cmd.ExecuteNonQuery(); con.Close(); System.Threading.Thread.Sleep(5000); reader = Helper.ExecuteReader(conString,CommandType.Text,"select * from orders"); reader.Close();

    Main方法裏,第一次調用Helper.ExecuteReader後建立了連接池並建立了一個連接,接着我們模擬連接進程被kill後再調用Helper.ExecuteReader。爲模擬連接進程被kill,先在master.dbo.sysprocesses查詢program_name爲DeadConnectionExample(連接串的Application Name)的SPID,然後kill了該連接進程。當再次調用Helper.ExecuteReader的時候就遇到“死連接”(一定遇到,因爲連接池裏只有一個連接)。用性能監視器觀察連接池裏的情況(先打開SQL Quary Analyzer得到一個User Connection以方便觀測)得到圖5。 

    圖5中連接池數量一直保持爲1,因爲kill連接進程所用的連接串沒有使用了連接池。kill了連接進程後User Connections(藍線)立刻下降1,而這時候連接池的連接數量(黃線)沒有隨着下降1,這就出現了一個“死連接”。接着,再從連接池取出連接訪問數據庫的時候就拋出SqlException,這時候連接數量下降1,因爲這時候Data Provider銷燬“死連接”。接着,嘗試使用其他連接,因爲這時候連接池裏連接數量爲0,所以需要建立新連接,連接數量和User Connections同時上升1。爲方便觀測,在嘗試其他連接前線程sleep了兩秒。 

    當然,如果“死連接”是由於網絡中斷、數據庫被shutdown引起,那麼Helper只能最後拋出SqlException。 

    注意:查詢master.dbo.sysprocesses使用的連接串沒有必要使用連接池。 

圖5

6、ADO.NET 2.0性能計數器 

   
前面提到的使用性能計數器時候的兩個bug在ADO.NET 2.0中都不會出現。ADO.NET 2.0中廢掉了1.1所用的“.NET CLR Data”的性能對象,新的性能對象是“.NET Data Provider for Oracle”和“.NET Data Provider for SqlServer”。這兩個性能對象都有14個計數器,這比ADO.NET 1.1能觀察到更多、更深入的連接池信息。其中本文說到的“被佔用的”連接、“可用的”的連接、“邏輯連接”和“物理連接”在ADO.NET 2.0性能計數器中分別叫Active Connection、Free Connection、Soft Connection、Hard Connection。 

    NumberOfFreeConnections、 NumberOfActiveConnections、 SoftDisconnectsPerSecond和SoftConnectsPerSecond默認在性能監視器是不打開的,要觀察這些計數器的值需要在程序的配置文件裏添加下面的配置:
<system.diagnostics> <switches> <add name="ConnectionPoolPerformanceCounterDetail" value="4"/> </switches> </system.diagnostics>

    NumberOfActiveConnectionPoolGroups計數器。前面說過,如果連接串使用Windows認證,那麼不同的Windows用戶有不同的連接池,ADO.NET 2.0中使用NumberOfActiveConnectionPoolGroups把使用Windows認證的相同連接串(字符相同)產生的不同連接池歸爲一組。 

    NumberOfActiveConnections, NumberOfFreeConnections計數器。ADO.NET 1.1裏的計數器沒有提供一個連接池裏的連接有多少個是“被佔用的”,有多少個是“可用的”。NumberOfActiveConnections和NumberOfFreeConnections填補了這個空白。這兩個計數器更加“生動”地描述了連接池裏連接的變化情況。圖6是一個連接相繼Open/Close了4次得到的比ADO.NET 1.1更“生動”的曲線。

圖6

    7、總結 

    明白了連接池的運作機制不等於能正確使用連接池,要充分挖掘連接池給應用系統帶來的性能提高,除了避免泄漏連接需要注意的兩點外, 請參考一下建議: 

    (1)確保每次訪問數據庫使用相同的連接串,連接串不要使用Windows認證。 

    (2)到了非打開不可的時候纔打開連接,連接使用完畢立刻關閉連接。因爲過早佔用和過晚釋放連接意味着增加連接池的不必要負荷(需要建立更多的連接以及連接請求需要等待更長時間)。 

    (3)根據應用系統的實際負荷設置適當的Min Pool Size和Max Pool Size。爲避免連接請求超時,如果應用系統的數據庫最大併發訪問數量大於Max Pool Size的默認置100就需要把Max Pool Size設置得更大;但不是越大越好,畢竟數據庫的負荷承受力有限。如果應用系統的數據庫最大並非訪問數量是N,那麼Min Pool Size不要大於N。 

    (4)如果應用系統不是使用集羣數據庫,把Connection Lifetime設置爲0。在單數據庫服務器的環境下沒必要把連接銷燬,因爲銷燬後一段時間又需要建立。 


    連接池對應用系統的性能提高起着至關重要的作用,但需要連接池有其適用範圍,它適用於需要頻繁訪問數據庫的應用系統。對於低頻率(例如一天只有幾次)的數據庫訪問應用系統就不必要,因爲一直保留一個低使用頻率的“物理連接”不如使用一次就建立一次好。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章