在.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