閱讀697 返回首頁    go 阿裏雲 go 技術社區[雲棲]


談談分布式事務之三: System.Transactions事務詳解[上篇]

在.NET 1.x中,我們基本是通過ADO.NET實現對不同數據庫訪問的事務。.NET 2.0為了帶來了全新的事務編程模式,由於所有事務組件或者類型均定義在System.Transactions程序集中的System.Transactions命名空間下,我們直接稱基於此的事務為System.Transactions事務。System.Transactions事務編程模型使我們可以顯式(通過System.Transactions.Transaction)或者隱式(基於System.Transactions.TransactionScope)的方式進行事務編程。我們先來看看,這種全新的事務如何表示。

一、System.Transactions.Transaction

在System.Transactions事務體係下,事務本身通過類型System.Transactions.Transaction類型表示,下麵是Transaction的定義:

   1: [Serializable]
   2: public class Transaction : IDisposable, ISerializable
   3: {   
   4:     public event TransactionCompletedEventHandler TransactionCompleted;
   5:     
   6:     public Transaction Clone();
   7:     public DependentTransaction DependentClone(DependentCloneOption cloneOption);
   8:     
   9:     public Enlistment EnlistDurable(Guid resourceManagerIdentifier, IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions);
  10:     public Enlistment EnlistDurable(Guid resourceManagerIdentifier, ISinglePhaseNotification singlePhaseNotification, EnlistmentOptions enlistmentOptions);    
  11:     public bool EnlistPromotableSinglePhase(IPromotableSinglePhaseNotification promotableSinglePhaseNotification);  
  12:     public Enlistment EnlistVolatile(IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions);
  13:     public Enlistment EnlistVolatile(ISinglePhaseNotification singlePhaseNotification, EnlistmentOptions enlistmentOptions);
  14:  
  15:     public void Rollback();
  16:     public void Rollback(Exception e);
  17:  
  18:     void ISerializable.GetObjectData(SerializationInfo serializationInfo, StreamingContext context);
  19:  
  20:     public static Transaction Current { get; set; }   
  21:  
  22:     public IsolationLevel IsolationLevel { get; }
  23:     public TransactionInformation TransactionInformation { get; }
  24: }

1、Transaction是可序列化的

從上麵的定義我們可以看到,Transaction類型(在沒有特殊說明的情況下,以下的Transaction類型指的就是System.Transactions.Transaction)上麵應用的SerializableAttribute特性,並且實現了ISerializable接口,意味著一個Transaction對象是可以被序列化的。Transaction的這一特性在WCF整個分布式事務的實現意義重大,原因很簡單:要讓事務能夠控製整個服務操作,必須實現事務的傳播,而傳播的前提就是事務可被序列化

2、如何登記事務參與者

Transaction中,定義了五個EnlistXxx方法用於將涉及到的資源管理器登記到當前事務中。其中EnlistDurable和EnlistVolatile分別實現了對持久化資源管理器和易失資源管管理器的事務登記,而EnlistPromotableSinglePhase則針對的是可被提升的資源管理器(比如基於SQL Server 2005和SQL Server 2008)。

事務登記的目的是建立事務提交樹,使得處於根節點的事務管理器能夠在事務提交的時候能夠沿著這棵樹將相應的通知發送給所有的事務參與者。這種至上而下的通知機製依賴於具體采用事務提交協議,或者說某個資源要求參與到當前事務之中,必須滿足基於協議需要的接收和處理相應通知的能力。System.Transactions將不同事務提交協議對參與者的要求定義在相應的接口中。其中IEnlistmentNotificationISinglePhaseNotification分別是基於2PC和SPC(關於2PC和SPC,在上篇中有詳細的介紹)。

如果我們需要為相應的資源開發能夠參與到System.Transactions事務的資源管理器,需要事先實現IEnlistmentNotification接口,對基本的2PC協議提供支持。當滿足SPC要求的時候,如果希望采用SPC優化協議,則需要實現ISinglePhaseNotification接口。如果希望像SQL Server 2005或者SQL Server 2008支持事務提升機製,則需要實現IPromotableSinglePhaseNotification接口。

3、環境事務(Ambient Transaction)

Transaction定義了一個類型為Transaction的Current靜態屬性(可讀可寫),表示當前的事務。作為當前事務的Transaction存儲於當前線程的TLS(Thread Local Storage)中(實際上是定義在一個應用了ThreadStaticAttribute特性的靜態字段上),所以僅對當前線程有效。如果進行異步調用,當前事務並不能自動事先跨線程傳播,將異步操作納入到當前事務,需要使用到另外一個事務:依賴事務。

這種基於當前線程的當前事務又稱環境事務(Ambient Transaction),很多資源管理器都具有對環境事務的感知能力。也就是說,如果我們通過Current屬性設置了環境事務,當對某個具有環境事務感知能力的資源管理器進行訪問的時候,相應的資源管理器會自動登記到當前事務中來。我們將具有這種感知能力的資源管理器稱為System.Transactions資源管理器。

4、事務標識

Transaction具有一個隻讀的TransactionInformation屬性,表示事務一些基本的信息。屬性的類型為TransactionInformation,定義如下:

   1: public class TransactionInformation
   2: {  
   3:     public DateTime CreationTime { get; }    
   4:     public TransactionStatus Status { get; }
   5:  
   6:     public string LocalIdentifier { get; }
   7:     public Guid DistributedIdentifier { get; }
   8: }

TransactionInformation的CreationTime和Status表示創建事務的時間和事務的當前狀態。事務具有活動(Active)、提交(Committed)、中止(Aborted)和未決(In-Doubt)四種狀態,通過TransactionStatus枚舉表示。

   1: public enum TransactionStatus
   2: {
   3:     Active,
   4:     Committed,
   5:     Aborted,
   6:     InDoubt
   7: }

事務具有兩個標識符,一個是本地標識,另一個是分布式標識,分別通過TransactionInformation的隻讀屬性LocalIdentifier和DistributedIdentifier表示。本地標識由兩部分組成:標識為本地應用程序域分配的輕量級事務管理器(LTM)的GUID和一個遞增的整數(表示當前LMT管理的事務序號)。在下麵的代碼中,我們分別打印出三個新創建的可提交事務(CommittableTransaction,為Transaction的子類,我們後麵會詳細介紹)的本地標識。

   1: using System;
   2: using System.Transactions;
   3: class Proggram
   4: {
   5:     static void Main()
   6:     {
   7:         Console.WriteLine(new CommittableTransaction().TransactionInformation.LocalIdentifier);
   8:         Console.WriteLine(new CommittableTransaction().TransactionInformation.LocalIdentifier);
   9:         Console.WriteLine(new CommittableTransaction().TransactionInformation.LocalIdentifier);
  10:     }
  11: }

輸出結果:

AC48F192-4410-45fe-AFDC-8A890A3F5634:1
AC48F192-4410-45fe-AFDC-8A890A3F5634:2
AC48F192-4410-45fe-AFDC-8A890A3F5634:3

一旦本地事務提升到基於DTC的分布式事務,係統會為之生成一個GUID作為其唯一標識。當事務跨邊界執行的時候,分布式事務標識會隨著事務一並被傳播,所以在不同的執行上下文中,你會得到相同的GUID。分布式事務標識通過TransactionInformation的隻讀屬性DistributedIdentifier表示,我經常在審核(Audit)中使用該標識。

對於上麵Transaction的介紹,細心的讀者可能會發現兩個問題:Transaction並沒有提供公有的構造函數,意味著我們不能直接通過new操作符創建Transaction對象;Transaction隻有兩個重載的Rollback方法,並沒有Commit方法,意味著我們直接通過Transaction進行事務提交。

在一個分布式事務中,事務初始化和提交隻能有相同的參與者擔當。也就是說隻有被最初開始的事務才能被提交,我們將這種能被初始化和提交的事務稱作可提交事務(Committable Transaction)。隨著分布式事務參與者逐個登記到事務之中,它們本地的事務實際上依賴著這個最初開始的事務,所以我們稱這種事務為依賴事務(Dependent Transaction)。

二、 可提交事務(CommittableTransaction)

隻有可提交事務才能被直接初始化,對可提交事務的提交驅動著對整個分布式事務的提交。可提交事務通過CommittableTransaction類型表示。照例先來看看CommittableTransaction的定義:

   1: [Serializable]
   2: public sealed class CommittableTransaction : Transaction, IAsyncResult
   3: {   
   4:     public CommittableTransaction();
   5:     public CommittableTransaction(TimeSpan timeout);
   6:     public CommittableTransaction(TransactionOptions options);
   7:  
   8:     public void Commit();
   9:     public IAsyncResult BeginCommit(AsyncCallback asyncCallback, object asyncState);
  10:     public void EndCommit(IAsyncResult asyncResult);
  11:    
  12:     object IAsyncResult.AsyncState { get; }
  13:     WaitHandle IAsyncResult.AsyncWaitHandle { get; }
  14:     bool IAsyncResult.CompletedSynchronously { get; }
  15:     bool IAsyncResult.IsCompleted { get; }
  16: }

1、可提交事務的超時時限和隔離級別

CommittableTransaction直接繼承自Transaction,提供了三個公有的構造函數。通過TimeSpan類型的timeout參數指定事務的超時實現,自被初始化那一刻開始算起,一旦超過了該時限,事務會被中止。通過TransactionOptions類型的options可以同時指定事務的超時時限和隔離級別。TransactionOptions是一個定義在System.Transactions命名空間下的結構(Struct),定義如下,兩個屬性Timeout和IsolationLevel分別代表事務的超時時限和隔離級別。

   1: [StructLayout(LayoutKind.Sequential)]
   2: public struct TransactionOptions
   3: {
   4:     //其他成員
   5:     public TimeSpan Timeout { get; set; }
   6:     public IsolationLevel IsolationLevel { get; set; }
   7: }

如果調用默認無參的構造函數來創建CommittableTransaction對象,意味著采用一個默認的超時時限。這個默認的時間是1分鍾,不過可以它可以通過配置的方式進行指定。事務超時時限相關的參數定義在<system.transactions>配置節中,下麵的XML體現的是默認的配置。從該段配置我們可以看到,我們不但可以通過<defaultSettings>設置事務默認的超時時限,還可以通過<machineSettings>設置最高可被允許的事務超時時限,默認為10分鍾。在對這兩項進行配置的時候,前者的時間必須小於後者,否則將用後者作為事務默認的超時時限。

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:   <system.transactions>
   4:     <defaultSettings timeout="00:01:00"/>
   5:     <machineSettings maxTimeout="00:10:00"/>
   6:   </system.transactions>
   7: </configuration>

作為事務ACID四大屬性之一的隔離性(Isolation),確保事務操作的中間狀態的可見性僅限於事務內部。隔離機製通過對訪問的數據進行加鎖,防止數據被事務的外部程序操作,從而確保了數據的一致性。但是隔離機製在另一方麵又約束了對數據的並發操作,降低數據操作的整體性能。為了權衡著兩個互相矛盾的兩個方麵,我們可以根據具體的情況選擇相應的隔離級別。

在System.Transactions事務體係中,為事務提供了7種不同的隔離級別。這7中隔離級別分別通過System.Transactions.IsolationLevel的7個枚舉項表示。

   1: public enum IsolationLevel
   2: {
   3:     Serializable,
   4:     RepeatableRead,
   5:     ReadCommitted,
   6:     ReadUncommitted,
   7:     Snapshot,
   8:     Chaos,
   9:     Unspecified
  10: }

7個隔離級別之中,Serializable具有最高隔離級別,代表的是一種完全基於序列化(同步)的數據存取方式,這也是System.Transactions事務默認采用的隔離級別。按照隔離級別至高向低,7個不同的隔離級別代表的含義如下:

  • Serializable:可以在事務期間讀取可變數據,但是不可以修改,也不可以添加任何新數據;
  • RepeatableRead:可以在事務期間讀取可變數據,但是不可以修改。可以在事務期間添加新數據;
  • ReadCommitted:不可以在事務期間讀取可變數據,但是可以修改它;
  • ReadUncommitted:可以在事務期間讀取和修改可變數據;
  • Snapshot:可以讀取可變數據。在事務修改數據之前,它驗證在它最初讀取數據之後另一個事務是否更改過這些數據。如果數據已被更新,則會引發錯誤。這樣使事務可獲取先前提交的數據值;
  • Chaos:無法覆蓋隔離級別更高的事務中的掛起的更改;
  • Unspecified:正在使用與指定隔離級別不同的隔離級別,但是無法確定該級別。如果設置了此值,則會引發異常。

2、事務的提交

CommittableTransaction提供了同步(通過Commit方法)和異步(通過BeginCommit|EndCommit方法組合)對事務的提交。此外CommittableTransaction還是實現了IAsyncResult這麼一個接口,如果采用異步的方式調用BeginCommit方法提交事務,方法返回的IAsyncResult對象的各屬性值會反映在CommittableTransaction同名屬性上麵。

前麵我們提到了環境事務已經System.Transactions資源管理器對環境事務的自動感知能力。當創建了CommittableTransaction對象的時候,被創建的事務並不會自動作為環境事務,你需要手工將其指定到Transaction的靜態Current屬性中。接下來,我們將通過一個簡單的例子演示如果通過CommittableTransaction實現一個分布式事務。

3、實例演示:通過CommittableTransaction實現分布式事務

在這個實例演示中,我們沿用介紹事務顯式控製時使用到的銀行轉帳的場景,並且直接使用第一篇中創建的帳戶表(T_ACCOUNT)。一個完整的轉帳操作本質上有兩個子操作完成,提取和存儲,即從一個帳戶中提取相應的金額存入另一個帳戶。為了完成這兩個操作,我寫了如下兩個存儲過程:P_WITHDRAW和P_DEPOSIT。

P_WITHDRAW:

   1: CREATE Procedure P_WITHDRAW
   2:     (
   3:         @id        VARCHAR(50),
   4:         @amount FLOAT
   5:     )
   6: AS
   7: IF NOT EXISTS(SELECT * FROM [dbo].[T_ACCOUNT] WHERE ID = @id)
   8:     BEGIN
   9:         RAISERROR ('帳戶ID不存在',16,1)
  10:         RETURN
  11:     END    
  12: IF NOT EXISTS(SELECT * FROM [dbo].[T_ACCOUNT] WHERE ID = @id AND BALANCE > @amount)
  13:     BEGIN
  14:         RAISERROR ('餘額不足',16,1)
  15:         RETURN
  16:     END
  17:     
  18: UPDATE     [dbo].[T_ACCOUNT] SET Balance = Balance - @amount WHERE Id = @id
  19: GO

P_DEPOSIT:

   1: CREATE Procedure P_DEPOSIT
   2:     (
   3:         @id        VARCHAR(50),
   4:         @amount FLOAT
   5:     )
   6: AS
   7: IF NOT EXISTS(SELECT * FROM [dbo].[T_ACCOUNT] WHERE Id = @id)
   8:     BEGIN
   9:         RAISERROR ('帳戶ID不存在',16,1)
  10:     END
  11: UPDATE     [dbo].[T_ACCOUNT] SET Balance = Balance + @amount WHERE Id = @id
  12: GO

為了確定是否成功轉帳,我們需要提取相應帳戶的當前餘額,我們相應操作實現在下麵一個存儲過程中。

   1: CREATE Procedure P_GET_BALANCE_BY_ID
   2:     (
   3:         @id VARCHAR(50)
   4:     )
   5: AS
   6: IF NOT EXISTS(SELECT * FROM [dbo].[T_ACCOUNT] WHERE Id = @id)
   7:     BEGIN
   8:         RAISERROR ('帳戶ID不存在',16,1)
   9:     END
  10: SELECT BALANCE FROM [dbo].[T_ACCOUNT] WHERE Id = @id
  11: GO

為了執行存儲過程的方便,我寫了一個簡單的工具類DbAccessUtil。ExecuteNonQuery和ExecuteScalar的作用於DbCommand同名方法相同。使用DbAccessUtil的這兩個方法,隻需要以字符串和字典的方式傳入存儲過程名稱和參數即可。由於篇幅所限,關於具有實現不再多做介紹了,又興趣的讀者,可以參考《WCF技術剖析(卷1)》的最後一章,裏麵的DbHelper提供了相似的實現。

   1: public static class DbAccessUtil
   2: {
   3:     public static int ExecuteNonQuery(string procedureName, IDictionary<string, object> parameters);
   4:     public static T ExecuteScalar<T>(string procedureName, IDictionary<string, object> parameters);
   5: }

借助於DbAccessUtil提供的輔助方法,我們定義兩個方法Withdraw和Deposit分別實現提取和存儲的操作,已近獲取某個帳戶當前餘額。

   1: static void Withdraw(string accountId, double amount)
   2: {
   3:     Dictionary<string, object> parameters = new Dictionary<string, object>();
   4:     parameters.Add("id", accountId);
   5:     parameters.Add("amount", amount);
   6:     DbAccessUtil.ExecuteNonQuery("P_DEPOSIT", parameters);
   7: } 
   8: static void Deposite(string accountId, double amount)
   9: {
  10:     Dictionary<string, object> parameters = new Dictionary<string, object>();
  11:     parameters.Add("id", accountId);
  12:     parameters.Add("amount", amount);
  13:     DbAccessUtil.ExecuteNonQuery("P_DEPOSIT", parameters);
  14: }
  15: private static double GetBalance(string accountId)
  16: {
  17:     Dictionary<string, object> parameters = new Dictionary<string, object>();
  18:     parameters.Add("id", accountId);
  19:     return DbAccessUtil.ExecuteScalar<double>("P_GET_BALANCE_BY_ID", parameters);
  20: }

現在假設帳戶表中有一個帳號,它們的ID分別為Foo,餘額為5000。下麵是沒有采用事務機製的轉帳實現(注意:需要轉入的帳戶不存在)。

   1: using System;
   2: using System.Collections.Generic;
   3: namespace Artech.TransactionDemo
   4: {
   5:     class Program
   6:     {
   7:         static void Main(string[] args)
   8:         {
   9:             string accountFoo = "Foo";
  10:             string nonExistentAccount = Guid.NewGuid().ToString();            
  11:             //輸出轉帳之前的餘額
  12:             Console.WriteLine("帳戶\"{0}\"的當前餘額為:¥{1}", accountFoo, GetBalance(accountFoo));
  13:             //開始轉帳    
  14:             try
  15:             {
  16:                 Transfer(accountFoo, nonExistentAccount, 1000);
  17:             }
  18:             catch (Exception ex)
  19:             {
  20:                 Console.WriteLine("轉帳失敗,錯誤信息:{0}", ex.Message);
  21:             }
  22:             //輸出轉帳後的餘額
  23:             Console.WriteLine("帳戶\"{0}\"的當前餘額為:¥{1}", accountFoo, GetBalance(accountFoo));
  24:         }
  25:  
  26:         private static void Transfer(string accountFrom, string accountTo, double amount)
  27:         {
  28:             Withdraw(accountFrom, amount);
  29:             Deposite(accountTo, amount);
  30:         }
  31:     }
  32: }

輸出結果:

帳戶"Foo"的當前餘額為:¥5000
轉帳失敗,錯誤信息:帳戶ID不存在
帳戶"Foo"的當前餘額為:¥4000

由於沒有采用事務,在轉入帳戶根本不存在情況下,款項依然被轉出帳戶提取出來。現在我們通過CommittableTransaction將整個轉帳操作納入同一個事務中,隻需要將Transfer方法進行如下的改寫:

   1: private static void Transfer(string accountFrom, string accountTo, double amount)
   2: {
   3:     Transaction originalTransaction = Transaction.Current;
   4:     CommittableTransaction transaction = new CommittableTransaction();
   5:     try
   6:     {
   7:         Transaction.Current = transaction;
   8:         Withdraw(accountFrom, amount);
   9:         Deposite(accountTo, amount);
  10:         transaction.Commit();
  11:     }
  12:     catch (Exception ex)
  13:     {
  14:         transaction.Rollback(ex);
  15:         throw;
  16:     }
  17:     finally
  18:     {
  19:         Transaction.Current = originalTransaction;
  20:         transaction.Dispose();
  21:     }
  22: }

輸出結果(將餘額恢複成5000):

帳戶"Foo"的當前餘額為:¥5000
轉帳失敗,錯誤信息:帳戶ID不存在
帳戶"Foo"的當前餘額為:¥5000

下一篇中我們將重點介紹DependentTransactionTransactionScope

 

分布式事務係列:
談談分布式事務之一:SOA需要怎樣的事務控製方式
談談分布式事務之二:基於DTC的分布式事務管理模型[上篇]
談談分布式事務之二:基於DTC的分布式事務管理模型[下篇]
談談分布式事務之三: System.Transactions事務詳解[上篇]
談談分布式事務之三: System.Transactions事務詳解[下篇]


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-27 16:33:55

  上一篇:go  談談分布式事務之二:基於DTC的分布式事務管理模型[下篇]
  下一篇:go  快訊丨第五屆“英特爾杯”全國並行應用挑戰賽PAC大賽圓滿落幕