在 Microsoft .NET Framework 中,System.Transactions 命名空間使得事務的處理比采用以往任何 一種技術都要簡單。此前,我曾經撰寫過一個數據點專欄,介紹了 System.Transactions 在 Microsoft® .NET Framework 2.0 Beta 1 以及 SQL Server™ 2005 下的工作方式。當然,在 產品的發布過程中,既增加了一些功能,也去掉了一些功能;有些 TransactionScopeOptions 已經發生 了變化。
從那以後,讀者們提出了很多有關 System.Transactions 的問題,這也促使我下定決心再探其究竟。 下面,我們就來看看它現在的工作方式,我會告訴您如何使用這個命名空間、它在什麼情況下有效以及在 什麼情況下不能發揮作用。通過以下的內容,您將了解到如何充分利用 .NET 架構去更有效地使用命名空 間。此外,我還將使用一些事務來演示最佳的實踐操作。本文用到的所有示例均可從 MSDN®雜志網站 下載。
一語道破天機
我們先來看看如何將兩條數據庫命令轉換為一個事務,具體方法就是 構建一個封裝器把這兩條命令封裝起來。具體操作非常簡單。只要引用 System.Transactions.dll,然後 把您需要的事務性代碼封裝在一個 using 語句內,這個 using 語句會創建一個 TransactionScope,最 後,在事務結束時調用 Complete 方法。
圖 1 所顯示的就是一個正在創建的事務,這個事務自身 還封裝了多個數據庫查詢。只要任意一個 SqlCommand 對象引發異常,程序流控制就會跳出 TransactionScope 的 using 語句塊,隨後,TransactionScope 將自行釋放並回滾該事務。由於這段代 碼使用了 using 語句,所以 SqlConnection 對象和 TransactionScope 對象都將被自動釋放。由此可見 ,只需添加很少的幾行代碼,您就可以構建出一個事務模型,這個模型可以對異常進行處理,執行結束後 會自行清理,此外,它還可以對命令的提交或回滾進行管理。
Figure 1 A Simple Transaction
// Create the TransactionScope using (TransactionScope ts = new TransactionScope()) { using (SqlConnection cn2005 = new SqlConnection(someSql2005)) { SqlCommand cmd = new SqlCommand(sqlUpdate, cn2005); cn2005.Open(); cmd.ExecuteNonQuery(); } using (SqlConnection cn2005 = new SqlConnection(anotherSql2005)) { SqlCommand cmd = new SqlCommand(sqlDelete, cn2005); cn2005.Open(); cmd.ExecuteNonQuery(); } // Tell the transaction scope that the transaction is in // a consistent state and can be committed ts.Complete(); // When the end of the scope is reached, the transaction is // completed, committed, and disposed. }
您在圖 1 中所看到的示例還有很多可以靈活設置的部分。TransactionScope 包含了所有的資 源管理器連接,這些連接會自動參與到事務中。這樣一來,您就可以為 TransactionScope 設置不同的選 項,在圖 1 的示例中,我們使用的是默認設置。
當前事務與參與方式
TransactionScope 類是 System.Transactions 的核心所在。這個類經過實例化,就會創建出一個當前事務(也稱為氛圍事 務 (ambient transaction)),任何資源管理器都可以參與這個事務。舉例來說,假設我們已經創建了 TransactionScope 的一個實例,並打開了與某個資源管理器的連接,這個資源管理器的默認設置是自動 參與事務,因此,這個連接也將加入到事務范圍內。
您可以在代碼的任何位置上隨時檢查是否存 在事務范圍,具體方法就是查看 System.Transactions.Transaction.Current 屬性。如果這個屬性為 “null”,說明不存在當前事務。資源管理器在打開它與其資源的連接時,它會檢查是否存在 事務。如果這個資源管理器已被設置為自動參與當前事務,那麼它將加入到這個事務中。SQL Server 連 接字符串的屬性之一就是 auto-enlist。默認情況下,auto-enlist 設置為 true,因此,它會加入到任 何活動事務中。您也可以通過給連接字符串顯式添加一個“auto-enlist=false”來改變默認 設置,如下所示:
Server=(local)\SQL2005;Database=Northwind; Integrated Security=SSPI;auto-enlist=false
這就是 System.Transactions 的神奇之處。我並沒有改變圖 1 中的任何 ADO.NET 代碼,但它卻依然 能夠充分利用 TransactionScope。我所做的只是創建了一個 TransactionScope 對象,以及一個在連接 打開後參與到活動事務中的 SqlConnection 對象。
事務的設置
若要更改 TransactionScope 類的默認設置,您可以創建一個 TransactionOptions 對象,然後通過它在 TransactionScope 對象上設置隔離級別和事務的超時時間。TransactionOptions 類有一個 IsolationLevel 屬性,通過這個屬性可以更改隔離級別,例如從默認的可序列化 (Serializable) 改為 ReadCommitted,甚至可以改為 SQL Server 2005 引入的新的快照 (Snapshot) 級別。(請記住,隔離級 別僅僅是一個建議。大多數數據庫引擎會試著使用建議的隔離級別,但也可能選擇其他級別。)此外, TransactionOptions 類還有一個 TimeOut 屬性,這個屬性可以用來更改超時時間(默認設置為 1 分鐘 )。
圖 1 示例中使用了默認的 TransactionScope 對象及其默認構造函數。也就是說,它的隔離 級別設置為可序列化 (Serializable),事務的超時時間為 1 分鐘,而且 TransactionScopeOptions 的 設置為 Required。除此以外,還有 7 種用於 TransactionScope 的重載構造函數,您可以使用這些構造 函數來更改這些設置。我在圖 2 中列出了 TransactionScopeOptions 枚舉器的各項設置。這些枚舉器使 您能夠控制嵌套事務彼此之間的響應方式。圖 3 中的代碼實際上就是更改這些設置。
Figure 3 Changing Transactional Settings
// Create the TransactionOptions object TransactionOptions tOpt = new TransactionOptions(); // Set the Isolation Level tOpt.IsolationLevel = IsolationLevel.ReadCommitted; // Set the timeout to be 2 minutes // Uses the (hours, minutes, seconds) constructor // Default is 60 seconds tOpt.Timeout = new TimeSpan(0, 2, 0); string cnString = ConfigurationManager.ConnectionStrings[ "sql2005DBServer"].ConnectionString); // Create the TransactionScope with the RequiresNew transaction // setting and the TransactionOptions object I just created using (TransactionScope ts = new TransactionScope(TransactionScopeOption.RequiresNew, tOpt)) { using (SqlConnection cn2005 = new SqlConnection(cnString) { SqlCommand cmd = new SqlCommand(updateSql1, cn2005); cn2005.Open(); cmd.ExecuteNonQuery(); } ts.Complete(); }
Figure 2 TransactionScopeOptions Enumerators
TransactionScopeOptions 描述 Required 如果已經存在一個事務,那麼這個事務范圍將加入已有的事務。 否則,它將創建自己的事務。 RequiresNew 這個事務范圍將創建自己的事務 。 Suppress 如果處於當前活動事務范圍內,那麼這個事務范圍既不會加入 氛圍事務 (ambient transaction),也不會創建自己的事務。當部分代碼需要留在事務外部時,可以使用 該選項。釋放
用好 System.Transactions 的關鍵就在於了解事務如何結 束以及何時結束。如果一個 TransactionScope 對象沒有被正確釋放,那麼這個事務將保持打開狀態,直 到這個對象被垃圾收集器所收集,或者已超過超時時間為止。對打開的事務置之不理是有一定危險性的, 其中之一就是處於活動狀態的事務會鎖定資源管理器的資源。下面這段代碼或許能幫助您更好的理解這個 問題:
TransactionScope ts = new TransactionScope(); SqlConnection cn2005 = new SqlConnection(cnString); SqlCommand cmd = new SqlCommand(updateSql1, cn2005); cn2005.Open(); cmd.ExecuteNonQuery(); cn2005.Close(); ts.Complete();
這段代碼會創建 TransactionScope 對象的一個實例,當 SqlConnection 打開 後,它將加入到該事務中。如果一切順利,該命令將得到執行,連接將會關閉,事務將會完成,而且它也 會被釋放掉。但是,如果運行過程引發異常,那麼程序流控制就會跳過關閉 SqlConnection 和釋放 TransactionScope 的操作,導致該事務在比預期更長的時間內保持打開狀態。因此,重中之重就是要確 保正確釋放 TransactionScope,使事務要麼快速提交,要麼快速回滾。您可以通過兩種簡單的方法來處 理這個問題:使用 try/catch/finally 代碼塊,或者使用 using 語句。您可以在 try/catch/finally 代碼塊之外聲明這些對象,在 try 代碼塊中添加代碼來創建對象並執行命令,並將對 TransactionScope 和 SqlConnection 的釋放放到 finally 代碼塊中。這種方法可以確保事務及時關閉。
我本人更喜歡 使用 using 語句,因為它能夠隱性地為你創建一個 try/catch 代碼塊。使用 using 語句時,即便代碼 塊中途引發異常,using 語句也能夠保證 TransactionScope 將會被釋放。無論何時退出代碼塊,using 語句都會確保已調用了 TransactionScope 的 Dispose 方法。這一點非常重要,因為就在釋放 TransactionScope 之前,該事務已經完成了。事務完成時,TransactionScope 就會判斷是否已經調用了 Complete 方法。如果已經調用,那麼該事務就會被提交;否則,該事務就會回滾。前面部分的代碼可以 這樣來寫:
using (TransactionScope ts = new TransactionScope()) { using (SqlConnection cn2005 = new SqlConnection(cnString) { SqlCommand cmd = new SqlCommand(updateSql1, cn2005); cn2005.Open(); cmd.ExecuteNonQuery(); } ts.Complete(); }
請注意,對 TransactionScope 對象和 SqlConnection 對象,我都使用了 using 語句。這樣做 是為了確保一旦引發異常,這兩個對象都可以得到快速、正確的釋放。如果代碼塊沒有引發異常,那麼這 兩個對象將在 using 語句代碼塊結束時(最後一個大括號的位置)被釋放。
輕型事務
System.Transactions 的強大功能之一就是它對輕型事務的支持。除非情況需要,否則輕型事務 是不會用到 Microsoft 分布式事務處理協調器 (DTC) 的。如果事務是本地的,那麼它將是一個輕型事務 。如果事務變成了分布式的,而且涉及到了第二個資源管理器,那麼這個輕型事務就會提升為一個完全分 布式的事務,而且一定要用到 DTC。
在分布式的應用場景中必須使用 DTC,這無疑會大大增加成 本。因此,除非萬不得已,否則最好避免這種情況。幸運的是,SQL Server 2005 提供了對輕型事務的支 持;而 SQL Server 以往的版本都沒有這種功能(也就是說,在 SQL Server 2000 下,所有的事務都要 升級為分布式事務)。我們可以通過幾個例子來體驗一下輕型事務帶來的益處。
在圖 4 的示例中 ,我所創建的 TransactionScope 會針對一個 SQL Server 2000 數據庫執行兩條命令。與 SQL Server 2000 的連接打開後,它會加入到這個 TransactionScope;因為 SQL Server 2000 不支持輕型事務,因 此必須使用 DTC,盡管這顯然不是一個分布式應用場景(因為我只針對一個數據庫,通過同一個連接來執 行操作)。
Figure 4 No Lightweight Support
using (TransactionScope ts = new TransactionScope()) { using (SqlConnection cn2000 = new SqlConnection(cnString2000)) { cn2000.Open(); SqlCommand cmd1 = new SqlCommand(updateSql1, cn2000); cmd1.ExecuteNonQuery(); SqlCommand cmd2 = new SqlCommand(updateSql2, cn2000); cmd2.ExecuteNonQuery(); } ts.Complete(); }
在下一個例子中(如圖 5 所示),我所創建的 TransactionScope 會針對一個 SQL Server 2005 數據庫執行兩條命令。由於 SQL Server 2005 支持輕型事務,因此,只有當涉及到第二個資源管理 器時,它才會變成一個分布式事務。
Figure 5 Lightweight Support
using (TransactionScope ts = new TransactionScope()) { using (SqlConnection cn2005 = new SqlConnection(cnString2005)) { cn2005.Open(); SqlCommand cmd1 = new SqlCommand(updateSql1, cn2005); cmd1.ExecuteNonQuery(); SqlCommand cmd2 = new SqlCommand(updateSql2, cn2005); cmd2.ExecuteNonQuery(); } ts.Complete(); }
那麼,我們應該在哪些情況下使用 System.Transactions 呢?如果你打算使用分布式事務, 那麼 System.Transactions 將大有裨益。同樣,如果你的資源管理器支持輕型事務,那麼 System.Transactions 也將是不二的選擇。但是,用 System.Transactions 去包含所有的數據庫命令卻 不一定是最佳的方法。
舉例來說,我們假設你的應用程序要針對一個不支持輕型事務的數據庫執 行多條命令。業務規則規定這些操作需要包含在一個事務中,以保持其原子性。如果該事務中的命令只針 對單個數據庫,那麼 ADO.NET 事務將比 System.Transactions 效率更高,因為在這種情況下,ADO.NET 事務不會調用 DTC。如果你的應用程序中確實需要一些分布式的環節,那麼 System.Transactions 就會 是一種不錯的選擇。
那麼,System.Transactions 能夠支持哪些資源管理器呢?實際上, System.Transactions 可以支持所有的資源管理器,只不過,支持輕型事務的資源管理器能夠充分利用可 自動提升的事務。
嵌套
在前文中,我已經提到了 TransactionScopeOptions 枚舉器以及 如何將其設置為 Required(默認值)、RequiresNew 或 Suppress。當你遇到嵌套方法和事務時,這個枚 舉器就能起到作用了。舉例來說,假設 Method1 創建一個 TransactionScope,針對一個數據庫執行一條 命令,然後調用 Method2。Method2 創建一個自身的 TransactionScope,並針對一個數據庫執行另一條 命令。您可以通過多種方法來處理這個問題。您可能希望 Method2 的事務加入到 Method1 的事務中,也 可能想讓 Method2 創建一個屬於自己的單獨的事務。在這種情況下,TransactionScopeOptions 枚舉器 的價值就得到了充分體現。圖 6 顯示的是嵌套事務。
Figure 6 Nesting Transactions
private void Method1() { using (TransactionScope ts = new TransactionScope(TransactionScopeOption.Required)) { using (SqlConnection cn2005 = new SqlConnection()) { SqlCommand cmd = new SqlCommand(updateSql1, cn2005); cn2005.Open(); cmd.ExecuteNonQuery(); } Method2(); ts.Complete(); } } private void Method2() { using (TransactionScope ts = new TransactionScope(TransactionScopeOption.RequiresNew)) { using (SqlConnection cn2005 = new SqlConnection()) { SqlCommand cmd = new SqlCommand(updateSql2, cn2005); cn2005.Open(); cmd.ExecuteNonQuery(); } ts.Complete(); } }
在這裡,內層事務 (Method2) 將創建出第二個 TransactionScope,而不是加入外層事務(來 自 Method1)。Method2 的 TransactionScope 是使用 RequiresNew 設置創建的,也就是告訴這個事務 要創建自己的范圍,而不是加入一個已有的范圍。如果您希望這個事務加入到已有事務中,您可以保留默 認設置不變,或者將該選項設置為 Required。
事務加入到一個 TransactionScope(因為它們使 用了 Required 設置)中後,只有它們全部投票,事務才能成功完成 (Complete),也才能提交事務。在 同一個 TransactionScope 中,如果任何一個事務沒有調用 ts.Complete,也就是說沒有投票完成 (Complete),那麼當外層的 TransactionScope 被釋放後,它將會回滾。
總結
進入和退出 事務都要快,這一點非常重要,因為事務會鎖定寶貴的資源。最佳實踐要求我們在需要使用事務之前再去 創建它,在需要對其執行命令前迅速打開連接,執行動作查詢 (Action Query),並盡可能快地完成和釋 放事務。在事務執行期間,您還應該避免執行任何不必要的、與數據庫無關的代碼,這能夠防止資源被毫 無疑義地鎖定過長的時間。
輕型事務的強大功能之一就在於它能夠判斷出自己是否需要提升為分 布式事務。正如我在前面所演示的,正確使用 System.Transactions 能夠給您帶來很多益處。關鍵就是 要了解如何使用,以及在什麼情況下使用。
本文配套源碼:http://www.bianceng.net/dotnet/201212/748.htm