|
一、事務(wù)使用基礎(chǔ)
先看一段使用事務(wù)的代碼:
1using (TransactionScope ts= new TransactionScope())
2{
3 //自定義操作
4 ts.Complete();
5}
這里使用 using 語句定義了一段隱性事務(wù)。如果我們在該語句塊中加入一段對 SQL Server 操作的代碼,那么它們將會自動加入這個事務(wù)。可以看出,這種事務(wù)的使用方式是極其方便的。
那么,有沒有可能在該語句塊中加入我們自己定義的事務(wù)操作,并且該操作能夠隨著整個事務(wù)塊的成功而提交,隨其失敗而回滾呢?答案當(dāng)然是可以的,否則我就不會寫這篇隨筆了。
二、實現(xiàn)自定義事務(wù)操作
根據(jù)事務(wù)的特性,我們可以推想:這個操作必須有實現(xiàn)提交和回滾之類動作的方法。沒錯,這就是 System.Transactions 命名空間中的 IEnlistmentNotification 接口。我們先寫一個最簡單的實現(xiàn):
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("準(zhǔn)備!");
17 preparingEnlistment.Prepared();
18 }
19
20 void IEnlistmentNotification.Rollback(Enlistment enlistment)
21 {
22 Console.WriteLine("回滾!");
23 enlistment.Done();
24 }
25}
26
27
好,定義完之后,還需要向事務(wù)管理器進行注冊,把它加入到當(dāng)前事務(wù)中去:
1using (TransactionScope ts= new TransactionScope())
2{
3 SampleEnlistment1 myEnlistment1 = new SampleEnlistment1();
4 Transaction.Current.EnlistVolatile(myEnlistment1, EnlistmentOptions.None);
5 ts.Complete();
6}
執(zhí)行這一段代碼,我們可以得到以下的輸出:
準(zhǔn)備!
提交!
先解釋一下,當(dāng)調(diào)用 ts.Complete() 方法的時候,表示事務(wù)已成功執(zhí)行。隨后,事務(wù)管理器就會尋找當(dāng)前所有已注冊的條目,也就是 IEnlistmentNotification 的每一個實現(xiàn),依次調(diào)用它們的 Prepare 方法,即通知每個條目做好提交準(zhǔn)備,當(dāng)所有條目都調(diào)用了 Prepared() 表示自己已經(jīng)準(zhǔn)備妥當(dāng)之后,再依次調(diào)用它們的 Commit 方法進行提交。如果其中有一個沒有調(diào)用 Prepared 而是調(diào)用了 ForceRollback 的話,整個事務(wù)都將回滾,此時事務(wù)管理器再調(diào)用每個條目的 Rollback 方法。
而如果我們將前面的 ts.Complete() 行注釋掉,顯然執(zhí)行結(jié)果就將變?yōu)椋?br>
回滾!
三、一個實現(xiàn)賦值的自定義操作
考慮一下,我們要實現(xiàn)一個事務(wù)賦值操作。該如何做法?以下是一個例子:
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的值變?yōu)椋? + _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的值變?yōu)椋? + _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("事務(wù)完成!");
7 scope1.Complete();
8 Console.WriteLine("退出區(qū)域之前,i的值為:" + atd.i.ToString());
9}
10Thread.Sleep(1000);
11Console.WriteLine("退出區(qū)域之后,i的值為:" + atd.i.ToString());
運行這一段代碼,我們可以看到如下結(jié)果:
事務(wù)完成!
退出區(qū)域之前,i的值為:0
提交!i的值變?yōu)椋?
退出區(qū)域之后,i的值為:1
從輸出結(jié)果來看,賦值操作被成功執(zhí)行了。可是有沒有感覺有些奇怪?先做個討論:
1、如果前面沒有 Thread.Sleep(1000) 這一行,那么我們多半會看到最后一行的輸出中,i 的值依然會是 0!為什么?想想就容易明白,這里對 Commit 方法是采用的異步調(diào)用,如同另開了一個線程。如果主線程不作等待的話,當(dāng)輸出的時候事務(wù)的 Commit 方法多半還沒有被執(zhí)行,輸出的結(jié)果當(dāng)然就會不對。
2、這個例子中,賦值操作是在 Commit 方法中才實際執(zhí)行的。但實際上就本例而言,我們也可以做個調(diào)整:將賦值操作放在 AssignIntVarValue 方法的最后去執(zhí)行,然后把 Commit 方法中的賦值操作去掉。相關(guān)的代碼變化如下:
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
這樣,執(zhí)行結(jié)果將會變?yōu)椋?br>
提交前改變!i的值為:1
事務(wù)完成!
退出區(qū)域之前,i的值為:1
退出區(qū)域之后,i的值為:1
3、在前面的基礎(chǔ)上,當(dāng)把調(diào)用的地方作如下改動,使事務(wù)失敗:
1using (TransactionScope scope1 = new TransactionScope())
2{
3 atd.AssignIntVarValue(1);
4 Console.WriteLine("事務(wù)失敗!");
5 //scope1.Complete();
6 Console.WriteLine("退出區(qū)域之前,i的值為:" + atd.i.ToString());
7}
此時的執(zhí)行結(jié)果將變?yōu)椋?br>
提交前改變!i的值為:1
事務(wù)失敗!
退出區(qū)域之前,i的值為:1
回滾!i的值變?yōu)椋?
退出區(qū)域之后,i的值為:0
可見,事務(wù)已成功回滾。
四、進一步的討論
前面我們都是只進行了一次賦值操作,如果我們需要進行兩次呢?
1using (TransactionScope scope1 = new TransactionScope())
2{
3 atd.AssignIntVarValue(1);
4 atd.AssignIntVarValue(2);
5 Console.WriteLine("事務(wù)失敗!");
6 //scope1.Complete();
7 Console.WriteLine("退出區(qū)域之前,i的值為:" + atd.i.ToString());
8}
這時的執(zhí)行結(jié)果將會是如何?我們當(dāng)然是希望回滾的時候,i 的值能先變回為 1,再變回為 0。但是實際結(jié)果呢?
提交前改變!i的值為:1
提交前改變!i的值為:2
事務(wù)失敗!
退出區(qū)域之前,i的值為:2
回滾!i的值變?yōu)椋?
回滾!i的值變?yōu)椋?
退出區(qū)域之后,i的值為:1
顯然,事務(wù)的回滾并沒有按照我們希望的順序來,是何原因?分析一下機制就能知道,事務(wù)管理器向每個條目發(fā)出回滾命令的時候只是發(fā)出了一個異步調(diào)用,并且很可能還是按登記的順序來發(fā)出的,這樣一來,Rollback 方法的調(diào)用順序顯然就不能保證了。
這時,如果將 Rollback 方法作一個小調(diào)整:
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的值變?yōu)椋? + _oldValue.ToString());
9 enlistment.Done();
10}
再次運行之,結(jié)果就對了:
提交前改變!i的值為:1
提交前改變!i的值為:2
事務(wù)失敗!
退出區(qū)域之前,i的值為:2
回滾!i的值變?yōu)椋?
回滾!i的值變?yōu)椋?
結(jié)果的正確其實并不是調(diào)用的順序就對了,只是 Rollback 方法在執(zhí)行的時候先檢查一下 _newValue 的值是否與當(dāng)前 i 的值一致,不一致的話就等上一會兒。在等待的過程中,另一個實例的 Rollback 方法被執(zhí)行,而它檢查發(fā)現(xiàn)是匹配的,所以就會回滾到 1。第一個 Rollback 等待結(jié)束后再檢查發(fā)現(xiàn)匹配了,于是就回滾為 0。
當(dāng)然實際應(yīng)用中,這種方法是極不可取的。且不說執(zhí)行順序依然會有很大的風(fēng)險,光是設(shè)計方式就有大問題。那么在實際應(yīng)用中我們應(yīng)當(dāng)如何去做呢?這里只提供一下設(shè)計思想,具體的實現(xiàn)代碼不再列出了。
在前面的例子中,兩次賦值共進行了兩次登記,這一點是引發(fā)不穩(wěn)定性的起因。我們應(yīng)當(dāng)考慮,兩次賦值依然只登記一次,在第一次賦值的時候,建立一個 SampleEnlistment2 的實例并在 AssignTransactDemo 中保存下來,并且 SampleEnlistment2 需要記錄當(dāng)前的操作。下一次賦值時,仍然使用這個實例,只進行操作記錄即可。這樣,當(dāng)回滾的時候,它根據(jù)記錄的反順序執(zhí)行回滾操作就可以了。
再進一步呢?如果說有多個 Transaction 需要進行賦值操作呢?這時我們可以在 AssignTransactionDemo 類中加入一個 Dictionary<Transaction, SampleEnlistment2>,使用的時候根據(jù) Transaction 去尋找相應(yīng)的條目即可。
本文討論暫到此為止。在微軟的101個例子中,有一個使用事務(wù)進行文件拷貝的例子。那里面有比較深入的實現(xiàn)。如果你還沒有看過,推薦去研究一下,相信你讀過此篇隨筆,研究它應(yīng)當(dāng)不再是個難題。
AspNet技術(shù):在.NET2.0中使用自定義事務(wù)操作,轉(zhuǎn)載需保留來源!
鄭重聲明:本文版權(quán)歸原作者所有,轉(zhuǎn)載文章僅為傳播更多信息之目的,如作者信息標(biāo)記有誤,請第一時間聯(lián)系我們修改或刪除,多謝。