2、有多少個連接池
Data Provider爲每個進程管理該進程的連接池,一個進程可以有一個或者多個連接池。Data Provider是根據什麼來決定是建立新的連接池還是使用已有的連接池呢?根據數據庫連接串。怎樣纔算是相同的連接串?連接串的字符完全相同?答案爲是但也不是。
筆者看過有些文章說不一定兩個連接串的字符完全相同纔算是相同的連接池,例如 "server = .;database = northwind;user = sa;password = sqlserver"與 "server = .;database = northwind; password = sqlserver; user = sa”是相同的連接串。但筆者測試過,Data Provider爲以上兩個連接串建立兩個連接池,證明它們並不是相同的連接串。其實,筆者認爲,對於“兩個連接串參數相同但順序不同”,“兩個連接串只差一個空格”是否是相同的連接串等問題不需要理會,因爲保證兩個連接池的字符完全相同是沒有難度的事。
如果你需要相同的連接串,首先你保證兩個連接串每一個字符都相同,但這還不能保證Data Provider只爲你建立一個連接池。因爲如果你使用Windows認證,那麼即使使用相同的連接串"server = .;database = northwind;trusted_connection = true”也有可能建立多個連接池。Windows認證意味着連接數據庫使用的數據庫用戶是運行打開數據庫連接Open()的當前用戶,如果運行該代碼的用戶不是固定的話,那麼即使每次都使用相同字符的連接串也會產生多個連接池。
連接池建立後直到它所屬的進程結束纔會被銷燬。
3、一個連接池裏有多少個連接
明白了怎麼區分不同的連接池後,下面我們來看看一個連接池裏有多少個連接。一個連接池裏的連接數不是靜態的數量,它會隨着連接池的不同狀態而改變。這就涉及連接池建立的時候有多少個連接,什麼時候連接會減少,什麼時候會增加,連接數的上限是多少等問題。
首先來看看能影響連接池裏連接數的連接串參數,如下表所示。
參數 |
默認值 |
描述 |
Min Pool Size |
0 |
連接池一旦建立後,池裏連接數量的最小值。 |
Max Pool Size |
100 |
連接池裏連接數量的最大值。 |
Connection Lifetime |
0 |
每當一個連接使用完後釋放回連接池,如果當前時間減去該連接建立的時間的值大於這個參數設定的值(秒),該連接被銷燬。0表示lifetime沒有上限。 |
Connection Timeout |
15 |
連接請求停止請求併產出錯誤前等待的時間。當池的連接數達到Max Pool Size而且全部被佔用,連接請求需要等待“被佔用的”連接被釋放回連接池,如果等待超過指定的時間還沒有連接被釋放就拋出InvalidOperationException。 |
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()並沒有訪問數據庫,所以這時候“死連接”還不能被發現。
下面以一個例子詳細解釋一個連接池從建立起到進程結束連接數的變化情況。
使用性能監視器觀察,得到圖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。在單數據庫服務器的環境下沒必要把連接銷燬,因爲銷燬後一段時間又需要建立。
連接池對應用系統的性能提高起着至關重要的作用,但需要連接池有其適用範圍,它適用於需要頻繁訪問數據庫的應用系統。對於低頻率(例如一天只有幾次)的數據庫訪問應用系統就不必要,因爲一直保留一個低使用頻率的“物理連接”不如使用一次就建立一次好。