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