谈谈分布式事务之三: System.Transactions事务详解[下篇]
在前面一篇给出的Transaction的定义中,信息的读者应该看到了一个叫做DepedentClone的方法。该方法对用于创建基于现有Transaction对 象的“依赖事务(DependentTransaction)”。不像可提交事务是一个独立的事务对象,依赖事务依附于现有的某个事务(可能是可提交事 务,也可能是依赖事务)。依赖事务可以帮助我们很容易地编写一些事务型操作,当环境事务不存的时候,可以确保操作在一个独立的事务中执行;当环境事务存在 的时候,则自动加入其中。
一、依赖事务(Dependent Transaction)
依赖事务通过DependentTransaction类型表示,DependentTransaction定义如下。和CommittableTransaction一样,DependentTransaction也是Transaction的子类。既然DependentTransaction依赖于现有的Transaction对象而存在,相当于被依赖事务的子事务,所以无法执行对事务的提交,也自然不会定义Commit方法。但是,DependentTransaction具有一个唯一的方法成员:Complete。调用这个方法意味着向被依赖事务发送通知,表明所有与依赖事务相关的操作已经完成。
1: [Serializable]
2: public sealed class DependentTransaction : Transaction
3: {
4: public void Complete();
5: }
1、通过DependentTransaction将异步操所纳入现有事务
通过Transaction的 静态属性Current表示的环境事务保存在TLS(Thread Local Storage)中,所以环境事务是基于当前线程的。这就意味着,即使环境事务存在,通过异步调用的操作也不可能自动加入到当前事务之中,因为在异步线程 中感知不到环境事务的存在。在这种情况下,我们需要做的就是手工将当前事务传递到另一个线程中,作为它的环境事务。通过依赖事务我们很容易实现这一点。
DependentTransaction通过Transaction的DependentClone方法创建,该方法具有一个DependentCloneOption枚举类型的参数,体现了被依赖的事务再上尚未接受到依赖事务的通知(调用Complete或者Rollback方法)得情况下,提交或者完成所采取的事务控制行为。DependentCloneOption提供了两个选项,BlockCommitUntilComplete表示被依赖事务会一直等待接收到依赖事务的通知或者超过事务设定的超时时限;而RollbackIfNotComplete则会直接将被依赖的事务回滚,并抛出TransactionAbortedException异常。
1: [Serializable]
2: public class Transaction : IDisposable, ISerializable
3: {
4: //其他成员
5: public DependentTransaction DependentClone(DependentCloneOption cloneOption);
6: }
7: public enum DependentCloneOption
8: {
9: BlockCommitUntilComplete,
10: RollbackIfNotComplete
11: }
下面的代码演示了如果通过依赖事务,采用异步的方式进行银行转账操作。借助于组件ThreadPool,将主线程环境事务的依赖事务传递给异步操作代理,开始异步操作的时候将此依赖事务作为当前的环境事务,那么之后的操作将自动在当前事务下进行。
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: ThreadPool.QueueUserWorkItem(state =>
9: {
10: Transaction.Current = state as DependentTransaction;
11: try
12: {
13: Withdraw(accountFrom, amount);
14: Deposite(accountTo, amount);
15: (state as DependentTransaction).Complete();
16: }
17: catch (Exception ex)
18: {
19: Transaction.Current.Rollback(ex);
20: }
21: finally
22: {
23: (state as IDisposable).Dispose();
24: Transaction.Current = null;
25: }
26: }, Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete));
27: //其他操作
28: transaction.Commit();
29: }
30: catch (TransactionAbortedException ex)
31: {
32: transaction.Rollback(ex);
33: Console.WriteLine("转帐失败,错误信息:{0}", ex.InnerException.Message);
34: }
35: catch (Exception ex)
36: {
37: transaction.Rollback(ex);
38: throw;
39: }
40: finally
41: {
42: Transaction.Current = originalTransaction;
43: transaction.Dispose();
44: }
45: }
由于在调用DependentClone方法创建依赖事务时指定的参数为 DependentCloneOption.BlockCommitUntilComplete,所以主线程在调用Commit方法提交事务的时候,由于 依赖事务尚未结束(调用Complete或者Rollback方法),在这里会一直等待。如果依赖事务的Complete或者Rollback一直没有调 用,那么被依赖的事务会一直等到超出事务设置的超时时限。所以,对于基于BlockCommitUntilComplete选项创建的依赖事务来说,应该 及时地调用Complete或者Rollback方法。
2、通过DependentTransaction实现事务型方法
这里所说的事务型方法是指方法的执行总是在事务中执行。具体来讲,有两种不同的事务应用场景:如果当前不存在环境事务,那么方法的执行将在一个独立的事务中执行;反之,如果存在环境事务,在方法执行会自动加入到环境事务之中。
比如说,存储(Deposit)和提取(Withdraw)就是典型的事务型操作。对于单纯的存取款的场景,应该创建一个新的事务来控制存储和提取 操作的执行,以确保单一帐户款项的数据一致性。如果在转账的场景中,应在在转账开始之前就创建一个新的事务,让提取和存储的操作自动加入到这个事务之中。
我们现在就结合可提交事务和依赖事务将Deposit和Withdraw两个方法定义成事务型方法,为了相同代码的重复,在这里把事务控制部分定义在如下一个InvokeInTransaction静态方法中:
1: static void InvokeInTransaction(Action action)
2: {
3: Transaction originalTransaction = Transaction.Current;
4: CommittableTransaction committableTransaction = null;
5: DependentTransaction dependentTransaction = null;
6: if (null == Transaction.Current)
7: {
8: committableTransaction = new CommittableTransaction();
9: Transaction.Current = committableTransaction;
10: }
11: else
12: {
13: dependentTransaction = Transaction.Current.DependentClone(DependentCloneOption.RollbackIfNotComplete);
14: Transaction.Current = dependentTransaction;
15: }
16:
17: try
18: {
19: action();
20: if (null != committableTransaction)
21: {
22: committableTransaction.Commit();
23: }
24:
25: if (null != dependentTransaction)
26: {
27: dependentTransaction.Complete();
28: }
29: }
30: catch (Exception ex)
31: {
32: Transaction.Current.Rollback(ex);
33: throw;
34: }
35: finally
36: {
37: Transaction transaction = Transaction.Current;
38: Transaction.Current = originalTransaction;
39: transaction.Dispose();
40: }
41: }
InvokeInTransaction方法的参数是一个Action类型的代理(Delegate),表示具体的业务操作。在开始的时候记录下当 前的环境事务,当整个操作结束之后应该环境事务恢复成该值。如果存在环境事务,则创建环境事务的依赖事务,反之直接创建可提交事务。并将新创建的依赖事务 或者可提交事务作为当前的环境事务。将目标操作的执行(action)放在try/catch中,当目标操作顺利执行后,调用依赖事务的Complete 方法或者可提交事务的Commit方法。如果抛出异常,则调用环境事务的Rollback进行回滚。在finally块中将环境事务恢复到之前的状态,并 调用Dispose方法对创建的事务进行回收。
借助于InvokeInTransaction这个辅助方法,我们以事务型方法的形式定义了如下的两个方法: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: InvokeInTransaction(() => DbAccessUtil.ExecuteNonQuery("P_WITHDRAW", parameters));
7: }
8:
9: static void Deposit(string accountId, double amount)
10: {
11: Dictionary<string, object> parameters = new Dictionary<string, object>();
12: parameters.Add("id", accountId);
13: parameters.Add("amount", amount);
14: InvokeInTransaction(() => DbAccessUtil.ExecuteNonQuery("P_DEPOSIT", parameters));
15: }
二、TransactionScope
在上面一节,我结合可提交事务和依赖事务,以及环境事务的机制提供了对事务型操作的实现。实际上,如果借助TransactionScope,相应的代码将会变得非常简单。下面的代码中,通过TransactionScope对InvokeInTransaction进行了改写,从执行效果来看这和原来的代码完全一致。
1: static void InvokeInTransaction(Action action)
2: {
3: using (TransactionScope transactionScope = new TransactionScope())
4: {
5: action();
6: transactionScope.Complete();
7: }
8: }
通过InvokeInTransaction方法前后代码的对比,我们可以明显看到TransactionScope确实能够使我们的事务控制变得非常的简单。实际上,在利用System.Transactions事务进行编程的时候,我们一般不会使用到可提交事务,对于依赖事务也只有在异步调用的时候会使用到,基于TransactionScope的事务编程方式才是我们推荐的。
正如其名称所表现的一样,TransactionScope就是为一组事务型操作创建一个执行范围,而这个范围始于TransactionScope创建之时,结束于TransactionScope被回收(调用Dispose方法)。在对TransactionScope进行深入介绍之前,照例先来看看它的定义:
1: public sealed class TransactionScope : IDisposable
2: {
3: public TransactionScope();
4: public TransactionScope(Transaction transactionToUse);
5: public TransactionScope(TransactionScopeOption scopeOption);
6: public TransactionScope(Transaction transactionToUse, TimeSpan scopeTimeout);
7: public TransactionScope(TransactionScopeOption scopeOption, TimeSpan scopeTimeout);
8: public TransactionScope(TransactionScopeOption scopeOption, TransactionOptions transactionOptions);
9: public TransactionScope(Transaction transactionToUse, TimeSpan scopeTimeout, EnterpriseServicesInteropOption interopOption);
10: public TransactionScope(TransactionScopeOption scopeOption, TransactionOptions transactionOptions, EnterpriseServicesInteropOption interopOption);
11:
12: public void Complete();
13: public void Dispose();
14: }
我们可以看到TransactionScope实现了IDisposable接口,除了Dispose方法之外,仅仅具有一个唯一的方法:Complete。但是TransactionScope却有一组丰富的构造函数。我们先来看看这些构造函数相应的参数如何影响TransactionScope对事务控制的行为。
1、TransactionScopeOption
实际上前面一节中提供的InvokeInTransaction方法基本上体现了TransactionScope的内部实现。也就是说,TransactionScope也是通过创建可提交事务或者依赖事务,并将其作为事务范围内的环境事务,从而将范围的所有操作纳入到一个事务之中。
通过在构造函数中指定TransactionScopeOption类型的scopeOption参数,控制TransactionScope当环境事务存在的时候应该采取怎样的方式执行事务范围内的操作。具有来讲,具有三种不同的方式:
- 如果已经存在环境事务,则使用该环境事务。否则,在进入范围之前创建新的事务;
- 总是为该范围创建新事务;
- 环境事务上下文在创建范围时被取消。范围中的所有操作都在无环境事务上下文的情况下完成。
TransactionScopeOption是一个枚举,三个枚举值Required、RequiresNew和Suppress依次对应上面的三种行为。
1: public enum TransactionScopeOption
2: {
3: Required,
4: RequiresNew,
5: Suppress
6: }
对于Required选项,如果当前存在环境事务TransactionScope会 创建环境事务的依赖事务,负责创建可提交事务,然后将创建的环境事务或者可提交事务作为事务范围的环境事务。如对于RequiresNew选 项,TransactionScope总是会创建可提交事务并将其作为事务范围的环境事务,意味着控制事务范围内操作的事务也当前的环境事务已经没有任何 关系。如果Suppress选项,TransactionScope会将事务范围内的环境事务设为空,意味着事务范围内的操作并不受事务的控制。
Required是默认选项,意味着事务范围内的事务将会作为当前环境事务的一部分。如果你不希望某个操作被纳入当前的环境事务,但是相应的操作也 需要事务的控制以确保所操作数据的一致性。比如,当业务逻辑失败导致异常抛出,需要对相应的错误信息进行日志记录。对于日记的操作就可以放入基于RequiresNew选项创建TransactionScope中。对于一些不重要的操作(操作的错误可被忽略),并且不需要通过事务来控制的操作,比如发送一些不太重要的通知,就可以采用Suppress选项。
2、TransactionOptions和EnterpriseServicesInteropOption
TransactionOptions在前面已经提及,用于控制事务的超时时限和隔离级别。对于超时时限,你也可以选择 TransactionScope相应能够的构造函数以TimeSpan的形式指定。而对于事务的隔离级别,需要着重强调一点:当选择 TransactionScopeOption.Required选项时,TransactionScope指定的隔离级别必须与环境事务(如果有)相匹 配。
比如下面的例子中,我定义两个嵌套的TransactionScope,外部的TransactionScope采用默认的隔离级别,内部在采用ReadCommitted隔离级别,当执行这段代码的时候,会抛出如图1所示的ArgumentException异常。
1: using (TransactionScope outerScope = new TransactionScope())
2: {
3: TransactionOptions transactionOptions = new TransactionOptions() { IsolationLevel = IsolationLevel.ReadCommitted };
4: using (TransactionScope innerScope = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
5: {
6: //事务型操作
7: innerScope.Complete();
8: }
9: //事务型操作
10: outerScope.Complete();
11: }
图1 隔离级别不一致导致的异常
实际上在System.Transactions事务机制被引入之前,像Enterprise Service主要依赖于基于COM+的分布式事务。TransactionScope通过 EnterpriseServicesInteropOption控制System.Transactions事务如何与COM+的分布式事务进行互操 作。具有来讲具有如下三种互操作选项,分别和EnterpriseServicesInteropOption三个枚举值相对应:
- None:Transaction 和 Current 之间不同步;
- Automatic:搜索现有的 COM+ 上下文并与之同步(如该上下文存在);
- Full:System.EnterpriseServices 上下文(可通过调用 ContextUtil 类的静态方法 Transaction 来检索)和 System.Transactions 环境事务(可通过调用 Transaction 类的静态方法 Current 来检索)始终保持同步。这将引入性能损失,因为可能需要创建新的 System.EnterpriseServices 上下文。
1: public enum EnterpriseServicesInteropOption
2: {
3: None,
4: Automatic,
5: Full
6: }
3、事务提交和回滚
对于事务范围中的事务,无论是事务的提交(对于可提交事务)、完成(依赖事务)和回滚都是在Dispose方法中执行的。 TransactionScope中定一个个私有的布尔类型字段(complete)表示事务是否正常结束。该成员的默认值为False,当调用 TransactionScope的Complete方法的时候会将此字段设置成True。当Dispose执行的时候,如果该字段的值为False,会 调用事务的Rollback方法对该事务实施回滚;否则会调用Commit方法(对于可提交事务)对事务进行提交或者调用Complete方法(依赖事 务)通知被依赖的事务本地事务已经正常完成。
除了执行事务的提交、完成或者回滚之外,TransactionScope的Dispose方法还负责将环境事务回复到事务范围开始之前的状态。在 调用Complete和Dispose之前,环境事务处于不可用的状态,如果此时试图获取环境事务,会抛出异常。比如在下面的代码中,在事务范围内部调用 Complete方法后,通过Transaction的Current静态属性获取当前环境事务,会抛出图2所示的InvalidOpertionException异常。
1: using (TransactionScope transactionScope = new TransactionScope())
2: {
3: //其他事务操作
4: transactionScope.Complete();
5: Transaction ambientTransaction = Transaction.Current;
6: }