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


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

前麵一篇給出的Transaction的定義中,信息的讀者應該看到了一個叫做DepedentClone的方法。該方法對用於創建基於現有Transaction對象的“依賴事務(DependentTransaction)”。不像可提交事務是一個獨立的事務對象,依賴事務依附於現有的某個事務(可能是可提交事務,也可能是依賴事務)。依賴事務可以幫助我們很容易地編寫一些事務型操作,當環境事務不存的時候,可以確保操作在一個獨立的事務中執行;當環境事務存在的時候,則自動加入其中。

一、依賴事務(Dependent Transaction)

依賴事務通過DependentTransaction類型表示,DependentTransaction定義如下。和CommittableTransaction一樣,DependentTransaction也是Transaction的子類。既然DependentTransaction依賴於現有的Transaction對象而存在,相當於被依賴事務的子事務,所以無法執行對事務的提交,也自然不會定義Commit方法。但是,DependentTransaction具有一個唯一的方法成員:Complete。調用這個方法意味著向被依賴事務發送通知,表明所有與依賴事務相關的操作已經完成。

   1: [Serializable]
   2: public sealed class DependentTransaction : Transaction
   3: {  
   4:     public void Complete();
   5: }

1、通過DependentTransaction將異步操所納入現有事務

通過Transaction的靜態屬性Current表示的環境事務保存在TLS(Thread Local Storage)中,所以環境事務是基於當前線程的。這就意味著,即使環境事務存在,通過異步調用的操作也不可能自動加入到當前事務之中,因為在異步線程中感知不到環境事務的存在。在這種情況下,我們需要做的就是手工將當前事務傳遞到另一個線程中,作為它的環境事務。通過依賴事務我們很容易實現這一點。

DependentTransaction通過Transaction的DependentClone方法創建,該方法具有一個DependentCloneOption枚舉類型的參數,體現了被依賴的事務再上尚未接受到依賴事務的通知(調用Complete或者Rollback方法)得情況下,提交或者完成所采取的事務控製行為。DependentCloneOption提供了兩個選項,BlockCommitUntilComplete表示被依賴事務會一直等待接收到依賴事務的通知或者超過事務設定的超時時限;而RollbackIfNotComplete則會直接將被依賴的事務回滾,並拋出TransactionAbortedException異常。

   1: [Serializable]
   2: public class Transaction : IDisposable, ISerializable
   3: {       
   4:     //其他成員
   5:     public DependentTransaction DependentClone(DependentCloneOption cloneOption);
   6: }
   7: public enum DependentCloneOption
   8: {
   9:     BlockCommitUntilComplete,
  10:     RollbackIfNotComplete
  11: }

下麵的代碼演示了如果通過依賴事務,采用異步的方式進行銀行轉賬操作。借助於組件ThreadPool,將主線程環境事務的依賴事務傳遞給異步操作代理,開始異步操作的時候將此依賴事務作為當前的環境事務,那麼之後的操作將自動在當前事務下進行。

   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:         ThreadPool.QueueUserWorkItem(state =>
   9:         {
  10:             Transaction.Current = state as DependentTransaction;
  11:             try
  12:             {
  13:                 Withdraw(accountFrom, amount);
  14:                 Deposite(accountTo, amount);
  15:                 (state as DependentTransaction).Complete();
  16:             }
  17:             catch (Exception ex)
  18:             {
  19:                 Transaction.Current.Rollback(ex);
  20:             }
  21:             finally
  22:             {
  23:                 (state as IDisposable).Dispose();
  24:                 Transaction.Current = null;
  25:             }
  26:         }, Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete));
  27:         //其他操作
  28:         transaction.Commit();
  29:     }
  30:     catch (TransactionAbortedException ex)
  31:     {
  32:         transaction.Rollback(ex);
  33:         Console.WriteLine("轉帳失敗,錯誤信息:{0}", ex.InnerException.Message);
  34:     }
  35:     catch (Exception ex)
  36:     {
  37:         transaction.Rollback(ex);
  38:         throw;
  39:     }
  40:     finally
  41:     {
  42:         Transaction.Current = originalTransaction;
  43:         transaction.Dispose();
  44:     }
  45: }

由於在調用DependentClone方法創建依賴事務時指定的參數為DependentCloneOption.BlockCommitUntilComplete,所以主線程在調用Commit方法提交事務的時候,由於依賴事務尚未結束(調用Complete或者Rollback方法),在這裏會一直等待。如果依賴事務的Complete或者Rollback一直沒有調用,那麼被依賴的事務會一直等到超出事務設置的超時時限。所以,對於基於BlockCommitUntilComplete選項創建的依賴事務來說,應該及時地調用Complete或者Rollback方法。

2、通過DependentTransaction實現事務型方法

這裏所說的事務型方法是指方法的執行總是在事務中執行。具體來講,有兩種不同的事務應用場景:如果當前不存在環境事務,那麼方法的執行將在一個獨立的事務中執行;反之,如果存在環境事務,在方法執行會自動加入到環境事務之中。

比如說,存儲(Deposit)和提取(Withdraw)就是典型的事務型操作。對於單純的存取款的場景,應該創建一個新的事務來控製存儲和提取操作的執行,以確保單一帳戶款項的數據一致性。如果在轉賬的場景中,應在在轉賬開始之前就創建一個新的事務,讓提取和存儲的操作自動加入到這個事務之中。

我們現在就結合可提交事務和依賴事務將Deposit和Withdraw兩個方法定義成事務型方法,為了相同代碼的重複,在這裏把事務控製部分定義在如下一個InvokeInTransaction靜態方法中:

   1: static void InvokeInTransaction(Action action)
   2: {
   3:     Transaction originalTransaction = Transaction.Current;
   4:     CommittableTransaction committableTransaction = null;
   5:     DependentTransaction dependentTransaction = null;
   6:     if (null == Transaction.Current)
   7:     {
   8:         committableTransaction = new CommittableTransaction();
   9:         Transaction.Current = committableTransaction;
  10:     }
  11:     else
  12:     {
  13:         dependentTransaction = Transaction.Current.DependentClone(DependentCloneOption.RollbackIfNotComplete);
  14:         Transaction.Current = dependentTransaction;
  15:     } 
  16:  
  17:     try
  18:     {
  19:         action();
  20:         if (null != committableTransaction)
  21:         {
  22:             committableTransaction.Commit();
  23:         } 
  24:  
  25:         if (null != dependentTransaction)
  26:         {
  27:             dependentTransaction.Complete();
  28:         }
  29:     }
  30:     catch (Exception ex)
  31:     {
  32:         Transaction.Current.Rollback(ex);
  33:         throw;
  34:     }
  35:     finally
  36:     {
  37:         Transaction transaction = Transaction.Current;
  38:         Transaction.Current = originalTransaction;
  39:         transaction.Dispose();
  40:     }
  41: }

InvokeInTransaction方法的參數是一個Action類型的代理(Delegate),表示具體的業務操作。在開始的時候記錄下當前的環境事務,當整個操作結束之後應該環境事務恢複成該值。如果存在環境事務,則創建環境事務的依賴事務,反之直接創建可提交事務。並將新創建的依賴事務或者可提交事務作為當前的環境事務。將目標操作的執行(action)放在try/catch中,當目標操作順利執行後,調用依賴事務的Complete方法或者可提交事務的Commit方法。如果拋出異常,則調用環境事務的Rollback進行回滾。在finally塊中將環境事務恢複到之前的狀態,並調用Dispose方法對創建的事務進行回收。

借助於InvokeInTransaction這個輔助方法,我們以事務型方法的形式定義了如下的兩個方法: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:     InvokeInTransaction(() => DbAccessUtil.ExecuteNonQuery("P_WITHDRAW", parameters));
   7: }       
   8:  
   9: static void Deposit(string accountId, double amount)
  10: {
  11:     Dictionary<string, object> parameters = new Dictionary<string, object>();
  12:     parameters.Add("id", accountId);
  13:     parameters.Add("amount", amount);
  14:     InvokeInTransaction(() => DbAccessUtil.ExecuteNonQuery("P_DEPOSIT", parameters));
  15: }

二、TransactionScope

在上麵一節,我結合可提交事務和依賴事務,以及環境事務的機製提供了對事務型操作的實現。實際上,如果借助TransactionScope,相應的代碼將會變得非常簡單。下麵的代碼中,通過TransactionScope對InvokeInTransaction進行了改寫,從執行效果來看這和原來的代碼完全一致。

   1: static void InvokeInTransaction(Action action)
   2: {
   3:     using (TransactionScope transactionScope = new TransactionScope())
   4:     {
   5:         action();
   6:         transactionScope.Complete();
   7:     }
   8: }

通過InvokeInTransaction方法前後代碼的對比,我們可以明顯看到TransactionScope確實能夠使我們的事務控製變得非常的簡單。實際上,在利用System.Transactions事務進行編程的時候,我們一般不會使用到可提交事務,對於依賴事務也隻有在異步調用的時候會使用到,基於TransactionScope的事務編程方式才是我們推薦的。

正如其名稱所表現的一樣,TransactionScope就是為一組事務型操作創建一個執行範圍,而這個範圍始於TransactionScope創建之時,結束於TransactionScope被回收(調用Dispose方法)。在對TransactionScope進行深入介紹之前,照例先來看看它的定義:

   1: public sealed class TransactionScope : IDisposable
   2: {
   3:     public TransactionScope();
   4:     public TransactionScope(Transaction transactionToUse);
   5:     public TransactionScope(TransactionScopeOption scopeOption);
   6:     public TransactionScope(Transaction transactionToUse, TimeSpan scopeTimeout);
   7:     public TransactionScope(TransactionScopeOption scopeOption, TimeSpan scopeTimeout);
   8:     public TransactionScope(TransactionScopeOption scopeOption, TransactionOptions transactionOptions);
   9:     public TransactionScope(Transaction transactionToUse, TimeSpan scopeTimeout, EnterpriseServicesInteropOption interopOption);
  10:     public TransactionScope(TransactionScopeOption scopeOption, TransactionOptions transactionOptions, EnterpriseServicesInteropOption interopOption);
  11:     
  12:     public void Complete();
  13:     public void Dispose();    
  14: }

我們可以看到TransactionScope實現了IDisposable接口,除了Dispose方法之外,僅僅具有一個唯一的方法:Complete。但是TransactionScope卻有一組豐富的構造函數。我們先來看看這些構造函數相應的參數如何影響TransactionScope對事務控製的行為。

1、TransactionScopeOption

實際上前麵一節中提供的InvokeInTransaction方法基本上體現了TransactionScope的內部實現。也就是說,TransactionScope也是通過創建可提交事務或者依賴事務,並將其作為事務範圍內的環境事務,從而將範圍的所有操作納入到一個事務之中。

通過在構造函數中指定TransactionScopeOption類型的scopeOption參數,控製TransactionScope當環境事務存在的時候應該采取怎樣的方式執行事務範圍內的操作。具有來講,具有三種不同的方式:

  • 如果已經存在環境事務,則使用該環境事務。否則,在進入範圍之前創建新的事務;
  • 總是為該範圍創建新事務;
  • 環境事務上下文在創建範圍時被取消。範圍中的所有操作都在無環境事務上下文的情況下完成。

TransactionScopeOption是一個枚舉,三個枚舉值Required、RequiresNew和Suppress依次對應上麵的三種行為。

   1: public enum TransactionScopeOption
   2: {
   3:     Required,
   4:     RequiresNew,
   5:     Suppress
   6: }

對於Required選項,如果當前存在環境事務TransactionScope會創建環境事務的依賴事務,負責創建可提交事務,然後將創建的環境事務或者可提交事務作為事務範圍的環境事務。如對於RequiresNew選項,TransactionScope總是會創建可提交事務並將其作為事務範圍的環境事務,意味著控製事務範圍內操作的事務也當前的環境事務已經沒有任何關係。如果Suppress選項,TransactionScope會將事務範圍內的環境事務設為空,意味著事務範圍內的操作並不受事務的控製。

Required是默認選項,意味著事務範圍內的事務將會作為當前環境事務的一部分。如果你不希望某個操作被納入當前的環境事務,但是相應的操作也需要事務的控製以確保所操作數據的一致性。比如,當業務邏輯失敗導致異常拋出,需要對相應的錯誤信息進行日誌記錄。對於日記的操作就可以放入基於RequiresNew選項創建TransactionScope中。對於一些不重要的操作(操作的錯誤可被忽略),並且不需要通過事務來控製的操作,比如發送一些不太重要的通知,就可以采用Suppress選項。

2、TransactionOptions和EnterpriseServicesInteropOption

TransactionOptions在前麵已經提及,用於控製事務的超時時限和隔離級別。對於超時時限,你也可以選擇TransactionScope相應能夠的構造函數以TimeSpan的形式指定。而對於事務的隔離級別,需要著重強調一點:當選擇TransactionScopeOption.Required選項時,TransactionScope指定的隔離級別必須與環境事務(如果有)相匹配。

比如下麵的例子中,我定義兩個嵌套的TransactionScope,外部的TransactionScope采用默認的隔離級別,內部在采用ReadCommitted隔離級別,當執行這段代碼的時候,會拋出如圖1所示的ArgumentException異常。

   1: using (TransactionScope outerScope = new TransactionScope())
   2: {
   3:     TransactionOptions transactionOptions = new TransactionOptions() { IsolationLevel = IsolationLevel.ReadCommitted };
   4:     using (TransactionScope innerScope = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
   5:     {
   6:         //事務型操作
   7:         innerScope.Complete();
   8:     }
   9:     //事務型操作
  10:     outerScope.Complete();
  11: }

image

圖1 隔離級別不一致導致的異常

實際上在System.Transactions事務機製被引入之前,像Enterprise Service主要依賴於基於COM+的分布式事務。TransactionScope通過EnterpriseServicesInteropOption控製System.Transactions事務如何與COM+的分布式事務進行互操作。具有來講具有如下三種互操作選項,分別和EnterpriseServicesInteropOption三個枚舉值相對應:

  • None:Transaction 和 Current 之間不同步;
  • Automatic:搜索現有的 COM+ 上下文並與之同步(如該上下文存在);
  • Full:System.EnterpriseServices 上下文(可通過調用 ContextUtil 類的靜態方法 Transaction 來檢索)和 System.Transactions 環境事務(可通過調用 Transaction 類的靜態方法 Current 來檢索)始終保持同步。這將引入性能損失,因為可能需要創建新的 System.EnterpriseServices 上下文。
   1: public enum EnterpriseServicesInteropOption
   2: {
   3:     None,
   4:     Automatic,
   5:     Full
   6: }

3、事務提交和回滾

對於事務範圍中的事務,無論是事務的提交(對於可提交事務)、完成(依賴事務)和回滾都是在Dispose方法中執行的。TransactionScope中定一個個私有的布爾類型字段(complete)表示事務是否正常結束。該成員的默認值為False,當調用TransactionScope的Complete方法的時候會將此字段設置成True。當Dispose執行的時候,如果該字段的值為False,會調用事務的Rollback方法對該事務實施回滾;否則會調用Commit方法(對於可提交事務)對事務進行提交或者調用Complete方法(依賴事務)通知被依賴的事務本地事務已經正常完成。

除了執行事務的提交、完成或者回滾之外,TransactionScope的Dispose方法還負責將環境事務回複到事務範圍開始之前的狀態。在調用Complete和Dispose之前,環境事務處於不可用的狀態,如果此時試圖獲取環境事務,會拋出異常。比如在下麵的代碼中,在事務範圍內部調用Complete方法後,通過Transaction的Current靜態屬性獲取當前環境事務,會拋出圖2所示的InvalidOpertionException異常。

   1: using (TransactionScope transactionScope = new TransactionScope())
   2: {
   3:     //其他事務操作
   4:     transactionScope.Complete();
   5:     Transaction ambientTransaction = Transaction.Current;
   6: }  

image 圖2 在TransactionScope完成之後獲取環境事務導致的異常

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


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

最後更新:2017-10-27 16:05:11

  上一篇:go  快訊丨第五屆“英特爾杯”全國並行應用挑戰賽PAC大賽圓滿落幕
  下一篇:go  談談分布式事務(Distributed Transaction)[共5篇]