程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 談談分布式事務之三 System.Transactions事務詳解[上篇]

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

編輯:關於.NET

在.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將不同事務提交協議對參與者的要求定義在相應的接口中。其中 IEnlistmentNotification和ISinglePhaseNotification分別是基於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

下一篇中我們將重點介紹DependentTransaction和TransactionScope。

出處:http://artech.cnblogs.com

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved