数据库连接及线程池
數據庫連接及線程池??
2007-09-17 13:51:21|??分類: 編程技術 |??標簽: |字號大中小?訂閱
3.1 自己的實踐過程
曾幾何時,記住了一句話:“建立數據庫連接是一個代價高昂的過程”,也從那時開始,我在構建系統時,一旦建立起了數據庫連接,就保存起來,任何要用數據庫的地方,都使用這個數據庫連接對象進行操作。
?????? 這樣的行為,在以前寫的單線程程序中,倒也可以接受,但在這次寫的多線程程序中,就出現問題了。在這次的程序中,最開始設計時,在工作線程類中,設置了一個SqlConnection的靜態成員以接受系統中已經建立起的數據庫連接對象。大致的代碼如下:
| public class NoPoolThread ??? { ??????? public static System.Data.SqlClient.SqlConnection DatabaseConnection = null;??????? ??????? private System.Threading.Thread WorkThread = null; ??????? private bool Continue = false; ? ??????? public void Start() ??????? { ??????????? if (WorkThread == null) ????????? ??{ ??????????????? ThreadStart workfun = new ThreadStart(DoWork); ??????????????? WorkThread = new Thread(workfun); ??????????????? Continue = true; ? ??????????????? WorkThread.Start(); ??????????? } ??????? } ? ??????? private void DoWork() ??????? { ??? ????????System.Diagnostics.Debug.Assert(DatabaseConnection != null); ?????????? ??????????? while (Continue) ??????????? { ??????????????? //省略了使用數據庫連接對象進行操作的代碼 ??????????????? Thread.Sleep(300); ??????????? }// end while ??????? } ? ? ??????? public void Stop() ??????? { ??????????? if (WorkThread != null) ??????????? { ??????????????? Continue = false;??????????????? ??????????? } ??????? }??????? ??? } |
?
?????? 在主程序中,使用下面的代碼來啟動工作線程
| SqlConnection connection = null; NoPoolThread[] threads = new NoPoolThread[99]; ? private void button1_Click(object sender, EventArgs e) { connection = new SqlConnection(GetConnectString()); ???? connection.Open(); ? ???? NoPoolThread.DatabaseConnection = connection; ???? for ( int i=0; i<threads.Length; i++) ???? { ???????? threads[i] = new NoPoolThread(); ???????? threads[i].Start(); ???? } } |
?
?????? 憑借以往的經驗,滿心期望著:NoPoolThread你給我好好干吧,成功就在眼前。殊不知,異常馬上就出現,基本上是說:“已有打開的與此命令相關聯的 DataReader,必須首先將它關閉。ExecuteNonQuery 要求已打開且可用的連接。連接的當前狀態為已關閉”。
??????
然后把threads數組長度改成1,即又恢復到只有一個線程使用數據庫連接對象時,就又恢復正常。
??????
?????? 這樣就說明了,在多個線程中使用同一個SqlConnection對象進行數據庫操作的想法是不現實的也是行不通的。
?
?????? 于是,就大著膽子,冒著“巨大的代價”,進行修改,在任何使用SqlConnection的地方,都臨時創建的SqlConnection對象一個對象,但是要創建對象,需要連接字符串啊,怎么來呢,不想重新寫一遍,就用現成的、已經傳到線程對象里面的哪個數據庫連接對象里面的連接字符串吧,于是修改代碼如下:
| private void DoWork() { ???? System.Diagnostics.Debug.Assert(DatabaseConnection != null); SqlConnection connection = new SqlConnection(DatabaseConnection.ConnectionString); ?????????? ???? while (Continue) ???? { ????????? //使用connection對象 ????????? Thread.Sleep(300); ????? }// end while ? ????? connection.Close(); } |
?
3.2 SqlConnection的連接
?????? 這樣修改的結果,每個線程使用自己的SqlConnection對象對數據庫進行操作,使各個線程之間互不影響。根據測試,程序可以順暢運行,而且在性能上沒有明顯損失,這多少有些以為,于是繼續查找資料。由于使用的時SqlConnection對象,所以就以它為線索,首先從MSDN開始,MSDN給出的解釋有如下內容:
|
?
?????? 從上面的解釋來看,我修改后的代碼,無意間啟用了數據庫連接池——因為除非你在連接字符串里面明確禁用連接池功能,否則默認的數據庫連接是從連接池中獲得連接的。
??????
3.3 連接池的相關概念
這里,既然提及了連接池,自己在這方面的認識又很模糊,于是就自己給自己了一個學習連接池的機會。
連接池及ADO.NET
連接到數據庫服務器通常由幾個需要很長時間的步驟組成。必須建立物理通道(例如套接字或命名管道),必須與服務器進行初次握手,必須分析連接字符串信息,必須由服務器對連接進行身份驗證,必須運行檢查以便在當前事務中登記,等等。——這就是建立數據庫連接的代價,但仍然只是定性的描述。
?
實際上,大多數應用程序僅使用一個或幾個不同的連接配置。這意味著在執行應用程序期間,許多相同的連接將反復地打開和關閉。為了使打開的連接成本最低,ADO.NET 使用稱為連接池的優化方法。
?
連接池是一種在打開數據存儲區的連接時提高應用程序性能的機制,可以顯著提高應用程序的性能和可縮放性。使用連接池減少新連接需要打開的次數。
?
池進程保持物理連接的所有權。通過為每個給定的連接配置保留一組活動連接來管理連接。只要用戶在連接上調用 Open,池進程就會檢查池中是否有可用的連接。如果某個池連接可用,會將該連接返回給調用者,而不是打開新連接。應用程序在該連接上調用 Close 時,池進程會將連接返回到活動連接池集中,而不是真正關閉連接。連接返回到池中之后,即可在下一個 Open 調用中重復使用。
?
只有配置相同的連接可以建立池連接。ADO.NET 同時保留多個池,每個配置一個池。連接由連接字符串以及 Windows 標識(在使用集成的安全性時)分為多個池。
?
池連接可以大大提高應用程序的性能和可縮放性,考慮一個訪問SQL Server數據庫的典型ASP.NET或WebServices應用程序。客戶端應用程序每次需要查詢數據庫時,就會在服務器端代碼中進行往返,以打開SqlConnection來執行查詢。在許多此類應用程序中,這一代碼以相同憑據一次又一次地連接到相同數據庫。理論上,這意味著客戶端應用程序每次需要執行查詢時,服務器端代碼需要執行三個操作——登錄到數據庫(需要檢查所提供的憑據)、執行查詢、然后注銷。連接池可以真正地提高此類應用程序的性能——通過將內部連接存儲在池中,并在以后進行重復利用,就不再因為登錄數據庫以及從中注銷而降低性能。對SqlConnection對象的Open和Close方法的調用可以短時間內返回,從而可以提高代碼的性能和響應速度(請參見3.1圖)。
?
?
圖3.1 典型ASP.NET或WebServices應用程序中的連接池
?
默認情況下,ADO.NET 中啟用連接池。除非顯式禁用,否則,連接在應用程序中打開和關閉時,池進程將對連接進行優化。還可以提供幾個連接字符串修飾符來控制連接池的行為。有關更多信息,請參見MSDN中的“使用連接字符串關鍵字控制連接池”。
?
在調用SqlConnection對象的Close方法時,SQL Client .NET數據提供程序并不實際關閉內部連接。相反,數據提供程序將該內部連接存儲到一個池中,以便在以后再次使用。甚至在SqlConnection對象被處理之后,該內部連接也保留在池中。如果在以后使用相同連接字符串和憑據調用SqlConnection對象的Open方法,將會再次使用同一內部連接與數據庫進行通信。
?
因此,微軟建議在使用完連接時一定要關閉或斷開連接,以便連接可以返回池。要關閉連接,可以使用 Connection 對象的 Close 或 Dispose 方法,也可以通過在 C# 的 using 語句中或在 Visual Basic 的 Using 語句中打開所有連接。不是顯式關閉的連接可能不會添加或返回到池中。例如,如果連接已超出范圍但沒有顯式關閉,則僅當達到最大池大小而該連接仍然有效時,該連接才會返回到連接池中。不是顯式關閉的連接可能無法返回池。例如,如果連接已超出范圍但沒有顯式關閉,則僅當達到最大池大小而該連接仍然有效時,該連接才會返回到連接池中。參見下面的示例。
?
另外,如果你希望確認是否真正再次利用了同一內部連接,可以使用.NET Reflection中的功能以可編程方式訪問私有InnerConnection屬性的內容。以下代碼(其需要對System.Reflection命名空間的引用)在Using代碼塊中打開一個SqlConnection,并存儲SqlConnection的InnerConnection屬性的值。通過利用Using代碼塊,在該代碼塊的末尾隱式處理了SqlConnection。此代碼在Using代碼塊中打開另一個SqlConnection,并存儲SqlConnection的InnerConnection屬性的值。最后,此代碼對比InnerConnection屬性的內容,確認它們實際上為同一對象。
| string strConn = @"Data Source=.\SQLExpress;Integrated Security=True;"; PropertyInfo propInnerConn; propInnerConn = typeof(SqlConnection).GetProperty("InnerConnection", ????????????????????? ??????????BindingFlags.NonPublic | BindingFlags.Instance); object objInnerConn1, objInnerConn2; using (SqlConnection cn = new SqlConnection(strConn)) { ??? cn.Open(); ??? objInnerConn1 = propInnerConn.GetValue(cn, null); ??? cn.Close(); } ? using (SqlConnection cn = new SqlConnection(strConn)) { ??? cn.Open(); ??? objInnerConn2 = propInnerConn.GetValue(cn, null); ??? cn.Close(); } ? Console.WriteLine(objInnerConn1 == objInnerConn2); ? 兩個SqlConnection對象是在不同的Using代碼塊中創建的,所以其資源將在每個Using代碼塊的末尾被清除。InnerConnection屬性的內容及其所封裝的物理連接沒有被處理,而是存儲在池中,以便在以后被再次利用。 ? 注意??? 如果在連接字符串中禁用了連接池(稍后將解釋如何禁用),將會看到此內部連接不能被重復利用。 |
?
連接池的創建和清除
?????? 在初次打數據庫開連接時——例如調用SqlConnection.Open方法時,池進程將根據完全匹配算法創建連接池,該算法將池與連接中的連接字符串關聯。每個連接池與不同的連接字符串關聯。打開新連接時,如果連接字符串并非與現有池完全匹配,將創建一個新池。按進程、按應用程序域、按連接字符串以及(在使用集成的安全性時)按 Windows 標識來建立池連接。
?????? ADO.NET 2.0 引入了兩種新的方法來清除池:ClearAllPools 和 ClearPool。ClearAllPools 清除給定提供程序的連接池,ClearPool 清除與特定連接關聯的連接池。如果在調用時連接正在使用,將進行相應的標記。連接關閉時,將被丟棄,而不是返回池中。
?
連接的添加和移除
?????? 連接池是為每個唯一的連接字符串創建的。當創建一個池后,將創建多個連接對象并將其添加到該池中,以滿足最小池大小的要求。連接根據需要添加到池中,但是不能超過指定的最大池大小(默認值為 100)。連接在關閉或斷開時釋放回池中。
在請求 SqlConnection 對象時,如果存在可用的連接,將從池中獲取該對象。連接要可用,必須未使用,具有匹配的事務上下文或未與任何事務上下文關聯,并且具有與服務器的有效鏈接。
連接池進程通過在連接釋放回池中時重新分配連接,來滿足這些連接請求。如果已達到最大池大小且不存在可用的連接,則該請求將會排隊。然后,池進程嘗試重新建立任何連接,直到到達超時時間(默認值為 15 秒)。如果池進程在連接超時之前無法滿足請求,將引發異常。
?
?????? 連接池進程定期掃描連接池,查找沒有通過 Close 或 Dispose 關閉的未用連接,并重新建立找到的連接。如果應用程序沒有顯式關閉或斷開其連接,連接池進程可能需要很長時間才能重新建立連接,所以,最好確保在連接中顯式調用 Close 和 Dispose。
如果連接長時間空閑,或池進程檢測到與服務器的連接已斷開,連接池進程會將該連接從池中移除。注意,只有在嘗試與服務器進行通信之后才能檢測到斷開的連接。如果發現某連接不再連接到服務器,則會將其標記為無效。無效連接只有在關閉或重新建立后,才會從連接池中移除。
如果存在與已消失的服務器的連接,那么即使連接池管理程序未檢測到已斷開的連接并將其標記為無效,仍有可能將此連接從池中取出。這種情況是因為檢查連接是否仍有效的系統開銷將造成與服務器的另一次往返,從而抵消了池進程的優勢。發生此情況時,初次嘗試使用該連接將檢測連接是否曾斷開,并引發異常。
?
上述連接的添加和移除,全都是由后臺運行的池進程管理的。
?
禁用連接池
您可能不希望使用連接池。例如,如果正在使用一個直接與數據庫進行通信的簡單Windows應用程序,那么可能希望禁用連接池。在采用這一架構時,各個客戶端應用程序需要自己的連接。在啟用連接池時,每個應用程序的連接被放入池中,如果在清除連接池之前重新打開該連接,將重復利用放入池中的連接。所以,如果應用程序頻繁重復使用連接,那么在啟用連接池的情況下,對SqlConnection.Open的調用將會更快速地返回。但是,這種方法將會導致在任意給定時刻存在許多活動的數據庫連接。禁用連接池將會降低任意時刻的活動數據庫連接數目,但這樣會強制所有對SqlConnection.Open的調用都建立一個新的數據庫連接。
?
如果希望禁用連接池,可以通過向連接字符串中添加Pooling=False,逐個連接地禁用連接池。幸運的是,在ADO.NET 2.0中不再需要記憶諸如此類的屬性。如果存在疑問,可以檢查SqlConnectionStringBuilder類的選項。在這個類中可以找到一個Pooling屬性,其取值為Boolean類型。默認情況下,此值被設置為True。將該值設置為False將會禁止將該連接放入池中。因此,在調用SqlConnection對象的Close方法時,將會關閉與數據庫的實際連接。
注意??? 在“偶爾進行連接”的Windows應用程序中,使用連接池可能很有幫助,具體取決于應用程序。如果應用程序希望定期重新連接到數據庫,則可以發揮連接池的作用,將與數據庫的物理連接保持打開狀態,至少暫時如此。如果在從池中刪除該物理連接之前,應用程序嘗試重新連接到該數據庫,則連接池邏輯(pooling logic)將會重新使用與該數據庫的物理連接。
?
查看數據庫連接
?????? 在使用了數據庫連接池的情況下,那么建立到數據庫上的連接到底怎么樣呢,這可以使用Windows的性能查看器來看。在運行SqlServer的服務器上,使用【管理工具】→【性能】就可以調出性能查看器,然后添加計數器:1、在性能對象下拉框中選擇:SQLServer:General Statistics,然后選擇User Connections計數器,這樣就可以看到連接到SQL Server上的數據庫連接數了。
?
?????? 為此,做了一個測試函數,該函數不斷的建立數據庫連接,讀取userinfo表的總函數,然后返回,代碼如下。
| private int GetNextAge() ??????? { ??????????? string sql = "SELECT? COUNT(username) FROM?? userinfo"; ? ??????????? int nextage = -1; ? ??????????? try ??????????? { ??????????????? SqlConnection connection = new SqlConnection(ConnectString); ??????????????? connection.Open();?????????????????????????????? ? ??????????????? SqlDataAdapter da = new SqlDataAdapter(sql, connection); ??????????????? DataSet ds = new DataSet(); ??????????????? da.Fill(ds); ? ??????????????? if (ds.Tables.Count != 0 && ??????????????????? ds.Tables[0].Rows.Count != 0) ??????????????? { ??????????????????? string nextagevalue = ds.Tables[0].Rows[0][0].ToString(); ??????????????????? nextage = Int32.Parse(nextagevalue); ??????????????? } ???????????? ???//下面一句標記為:【關閉語句】可以注釋掉下面一句觀看情況 ??????????????? connection.Close(); ? ??????????? } ??????????? catch (Exception e) ??????????? { ??????????????? Console.WriteLine(e.Message); ??????????????? Console.WriteLine(e.StackTrace); ??????????? } ? ??????????? return nextage + 1; ??????? } |
?
下面是上面代碼的運行中對性能計數器的截圖,同時,也把進程的CPU、內存占用情況也列了出來。
①及時關閉連接,不啟用連接池。
?
?
圖3.2:及時關閉連接,不使用連接池——開始循環調用
?
?
圖3.3:及時關閉連接,不使用連接池——結束循環調用
?
② 及時關閉連接,啟用連接池。
?
?
圖3.4:及時關閉連接,啟用連接池——開始循環調用
?
?
圖3.5:及時關閉連接,啟用連接池——結束循環調用
?
③ 不及時關閉連接,不啟用連接池。
?
?
圖3.6:不及時關閉連接,不啟用連接池——開始循環調用
?
?
圖3.7:不及時關閉連接,不啟用連接池——結束循環調用
?
④ 不及時關閉連接,啟用連接池。
?
?
圖3.8:不及時關閉連接,啟用連接池——開始循環調用
?
?
圖3.9:不及時關閉連接,啟用連接池——結束循環調用
?
?????? 從上面的測試可以看出
- 不使用連接池,如果及時關閉連接,那么數據庫連接數還有可能比使用線程池時低。這是因為,池進程可能并沒有真正釋放一些鏈接導致的。
- 使用線程池時,CPU都保持在一個較低的水平。
轉載于:https://www.cnblogs.com/MakethingsEasy/archive/2012/08/10/2632396.html
總結
- 上一篇: 【Asp.Net】:如何处理大量页面的身
- 下一篇: poj 2948 Martian M