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个例子中,有一个使用事务进行文件拷贝的例子。那里面有比较深入的实现。如果你还没有看过,推荐去研究一下,相信你读过此篇随笔,研究它应当不再是个难题。