.net 2.0 framework 中新增了 System.Transactions 命名空間,其中提供的一系列接口和類使得在.net 2.0 中使用事務比起從前要方便了許多。有關在 .net 2.0 下操作數據庫事務的文章已經有了很多,這裡只提一下如何設計自定義事務操作。
一、事務使用基礎 先看一段使用事務的代碼:
1using (TransactionScope ts= new TransactionScope())
2{
3 //自定義操作
4 ts.Complete();
5}
這裡使用 using 語句定義了一段隱性事務。如果我們在該語句塊中加入一段對 SQL Server 操作的代碼,那麼它們將會自動加入這個事務。可以看出,這種事務的使用方式是極其方便的。
那麼,有沒有可能在該語句塊中加入我們自己定義的事務操作,並且該操作能夠隨著整個事務塊的成功而提交,隨其失敗而回滾呢?答案當然是可以的,否則我就不會寫這篇隨筆了。
二、實現自定義事務操作 根據事務的特性,我們可以推想:這個操作必須有實現提交和回滾之類動作的方法。沒錯,這就是 System.Transactions 命名空間中的 IEnlistmentNotification 接口。我們先寫一個最簡單的實現:
1class SampleEnlistment1 : IEnlistmentNotification
2{
3 void IEnlistmentNotification.Commit(Enlistment enlistment)
4 {
5 Console.WriteLine("提交!");
6 enlistment.Done();
7 }
8
9 void IEnlistmentNotification.InDoubt(Enlistment enlistment)
10 {
11 throw new Exception("The method or operation is not implemented.");
12 }
13
14 void IEnlistmentNotification.Prepare(PreparingEnlistment preparingEnlistment)
15 {
16 Console.WriteLine("准備!");
17 preparingEnlistment.Prepared();
18 }
19
20 void IEnlistmentNotification.Rollback(Enlistment enlistment)
21 {
22 Console.WriteLine("回滾!");
23 enlistment.Done();
24 }
25}
26
27
好,定義完之後,還需要向事務管理器進行注冊,把它加入到當前事務中去:
1using (TransactionScope ts= new TransactionScope())
2{
3 SampleEnlistment1 myEnlistment1 = new SampleEnlistment1();
4 Transaction.Current.EnlistVolatile(myEnlistment1, EnlistmentOptions.None);
5 ts.Complete();
6}
執行這一段代碼,我們可以得到以下的輸出:
准備!
提交!
先解釋一下,當調用 ts.Complete() 方法的時候,表示事務已成功執行。隨後,事務管理器就會尋找當前所有已注冊的條目,也就是 IEnlistmentNotification 的每一個實現,依次調用它們的 Prepare 方法,即通知每個條目做好提交准備,當所有條目都調用了 Prepared() 表示自己已經准備妥當之後,再依次調用它們的 Commit 方法進行提交。如果其中有一個沒有調用 Prepared 而是調用了 ForceRollback 的話,整個事務都將回滾,此時事務管理器再調用每個條目的 Rollback 方法。
而如果我們將前面的 ts.Complete() 行注釋掉,顯然執行結果就將變為:
回滾!
三、一個實現賦值的自定義操作 考慮一下,我們要實現一個事務賦值操作。該如何做法?以下是一個例子:
1class SampleEnlistment2 : IEnlistmentNotification
2{
3 public SampleEnlistment2(AssignTransactionDemo var, int newValue)
4 {
5 _var = var;
6 _oldValue = var.i;
7 _newValue = newValue;
8 }
9
10 private AssignTransactionDemo _var;
11 private int _oldValue;
12 private int _newValue;
13
14 void IEnlistmentNotification.Commit(Enlistment enlistment)
15 {
16 _var.i = _newValue;
17 Console.WriteLine("提交!i的值變為:" + _var.i.ToString());
18 enlistment.Done();
19 }
20
21 void IEnlistmentNotification.InDoubt(Enlistment enlistment)
22 {
23 throw new Exception("The method or operation is not implemented.");
24 }
25
26 void IEnlistmentNotification.Prepare(PreparingEnlistment preparingEnlistment)
27 {
28 preparingEnlistment.Prepared();
29 }
30
31 void IEnlistmentNotification.Rollback(Enlistment enlistment)
32 {
33 _var.i = _oldValue;
34 Console.WriteLine("回滾!i的值變為:" + _var.i.ToString());
35 enlistment.Done();
36 }
37}
38
39class AssignTransactionDemo
40{
41 public int i;
42
43 public void AssignIntVarValue(int newValue)
44 {
45 SampleEnlistment2 myEnlistment2 = new SampleEnlistment2(this, newValue);
46 Guid guid = new Guid("{3456789A-7654-2345-ABCD-098765434567}");
47 Transaction.Current.EnlistDurable(guid, myEnlistment2, EnlistmentOptions.None);
48 }
49}
50
51
然後,這樣來使用:
1AssignTransactionDemo atd = new AssignTransactionDemo();
2atd.i = 0;
3using (TransactionScope scope1 = new TransactionScope())
4{
5 atd.AssignIntVarValue(1);
6 Console.WriteLine("事務完成!");
7 scope1.Complete();
8 Console.WriteLine("退出區域之前,i的值為:" + atd.i.ToString());
9}
10Thread.Sleep(1000);
11Console.WriteLine("退出區域之後,i的值為:" + atd.i.ToString());
運行這一段代碼,我們可以看到如下結果:
事務完成!
退出區域之前,i的值為:0
提交!i的值變為:1
退出區域之後,i的值為:1
從輸出結果來看,賦值操作被成功執行了。可是有沒有感覺有些奇怪?先做個討論:
1、如果前面沒有 Thread.Sleep(1000) 這一行,那麼我們多半會看到最後一行的輸出中,i 的值依然會是 0!為什麼?想想就容易明白,這裡對 Commit 方法是采用的異步調用,如同另開了一個線程。如果主線程不作等待的話,當輸出的時候事務的 Commit 方法多半還沒有被執行,輸出的結果當然就會不對。
2、這個例子中,賦值操作是在 Commit 方法中才實際執行的。但實際上就本例而言,我們也可以做個調整:將賦值操作放在 AssignIntVarValue 方法的最後去執行,然後把 Commit 方法中的賦值操作去掉。相關的代碼變化如下:
1class SampleEnlistment2 : IEnlistmentNotification
2{
3 void IEnlistmentNotification.Commit(Enlistment enlistment)
4 {
5 enlistment.Done();
6 }
7 //其它略
8}
9
10class AssignTransactionDemo
11{
12 public int i;
13
14 public void AssignIntVarValue(int newValue)
15 {
16 SampleEnlistment2 myEnlistment2 = new SampleEnlistment2(this, newValue);
17 Guid guid = new Guid("{3456789A-7654-2345-ABCD-098765434567}");
18 Transaction.Current.EnlistDurable(guid, myEnlistment2, EnlistmentOptions.None);
19 i = newValue;
20 Console.WriteLine("提交前改變!i的值為:" + i.ToString());
21 }
22}
23
24
這樣,執行結果將會變為:
提交前改變!i的值為:1
事務完成!
退出區域之前,i的值為:1
退出區域之後,i的值為:1
3、在前面的基礎上,當把調用的地方作如下改動,使事務失敗:
1using (TransactionScope scope1 = new TransactionScope())
2{
3 atd.AssignIntVarValue(1);
4 Console.WriteLine("事務失敗!");
5 //scope1.Complete();
6 Console.WriteLine("退出區域之前,i的值為:" + atd.i.ToString());
7}
此時的執行結果將變為:
提交前改變!i的值為:1
事務失敗!
退出區域之前,i的值為:1
回滾!i的值變為:0
退出區域之後,i的值為:0
可見,事務已成功回滾。
四、進一步的討論 前面我們都是只進行了一次賦值操作,如果我們需要進行兩次呢?
1using (TransactionScope scope1 = new TransactionScope())
2{
3 atd.AssignIntVarValue(1);
4 atd.AssignIntVarValue(2);
5 Console.WriteLine("事務失敗!");
6 //scope1.Complete();
7 Console.WriteLine("退出區域之前,i的值為:" + atd.i.ToString());
8}
這時的執行結果將會是如何?我們當然是希望回滾的時候,i 的值能先變回為 1,再變回為 0。但是實際結果呢?
提交前改變!i的值為:1
提交前改變!i的值為:2
事務失敗!
退出區域之前,i的值為:2
回滾!i的值變為:0
回滾!i的值變為:1
退出區域之後,i的值為:1
顯然,事務的回滾並沒有按照我們希望的順序來,是何原因?分析一下機制就能知道,事務管理器向每個條目發出回滾命令的時候只是發出了一個異步調用,並且很可能還是按登記的順序來發出的,這樣一來,Rollback 方法的調用順序顯然就不能保證了。
這時,如果將 Rollback 方法作一個小調整:
1void IEnlistmentNotification.Rollback(Enlistment enlistment)
2{
3 while (_var.i != _newValue)
4 {
5 Thread.Sleep(500);
6 }
7 _var.i = _oldValue;
8 Console.WriteLine("回滾!i的值變為:" + _oldValue.ToString());
9 enlistment.Done();
10}
再次運行之,結果就對了:
提交前改變!i的值為:1
提交前改變!i的值為:2
事務失敗!
退出區域之前,i的值為:2
回滾!i的值變為:1
回滾!i的值變為:0
結果的正確其實並不是調用的順序就對了,只是 Rollback 方法在執行的時候先檢查一下 _newValue 的值是否與當前 i 的值一致,不一致的話就等上一會兒。在等待的過程中,另一個實例的 Rollback 方法被執行,而它檢查發現是匹配的,所以就會回滾到 1。第一個 Rollback 等待結束後再檢查發現匹配了,於是就回滾為 0。
當然實際應用中,這種方法是極不可取的。且不說執行順序依然會有很大的風險,光是設計方式就有大問題。那麼在實際應用中我們應當如何去做呢?這裡只提供一下設計思想,具體的實現代碼不再列出了。
在前面的例子中,兩次賦值共進行了兩次登記,這一點是引發不穩定性的起因。我們應當考慮,兩次賦值依然只登記一次,在第一次賦值的時候,建立一個 SampleEnlistment2 的實例並在 AssignTransactDemo 中保存下來,並且 SampleEnlistment2 需要記錄當前的操作。下一次賦值時,仍然使用這個實例,只進行操作記錄即可。這樣,當回滾的時候,它根據記錄的反順序執行回滾操作就可以了。
再進一步呢?如果說有多個 Transaction 需要進行賦值操作呢?這時我們可以在 AssignTransactionDemo 類中加入一個 Dictionary<Transaction, SampleEnlistment2>,使用的時候根據 Transaction 去尋找相應的條目即可。
本文討論暫到此為止。在微軟的101個例子中,有一個使用事務進行文件拷貝的例子。那裡面有比較深入的實現。如果你還沒有看過,推薦去研究一下,相信你讀過此篇隨筆,研究它應當不再是個難題。