当在分布式系统中工作时,事务有时必须要跨越服务边界。例如,如果一个服务管理客户信息而另一个服务管理订单,一个客户提交一个订单并想产品可以发送到一个新的地址,系统将需要调用每个服务上的操作。如果事务完成,用户将会期待两个系统上的信息都被合适的更新。
如果基础架构支持一个原子事务协议,服务可以像刚才描述的那样被组合到一个复合事务中。WS-AT(网络服务原子事务)提供在参与的服务间共享信息的平台来实现ACID事务必须的两步语义提交。在WCF中,在服务边界间的流事务信息被称作事务流。
为了在服务边界间十万事务流转的语义,下面的5步必须实现:
1. (服务契约) SessionMode.Required. 服务契约必须要求会话,因为这是信息如何在合作者和服务组成部分间共享消息的方式。
2. (操作行为) TransactionScopeRequired=true. 操作行为必须要求一个事务范围。如果没有事务存在,那么将会按照要求创建一个新的事务。
3.(操作契约) TransactionFlowOption.Allowed. 操作契约必须允许事务信息在消息头中流转。
4.(绑定定义) TransactionFlow=true. 绑定必须使能事务流以便于信道可以将事务信息加到SOAP消息头中。也要注意绑定必须支持会话因为wsHttpBinding支持但是basicHttpBinding不支持。
5.(客户端)TransactionScope. 这部分初始化事务,一般对客户端来说,当调用服务操作时必须使用一个事务范围。它也必须调用TransactionScope.Close() 来执行改变。
图片5.12 扩展服务边界的事务
关于TransactionScopeRequired 属性的.NET 3.5 文档包含了下面的表来描述这些元素间的关系。为了方便我们在这里重述一遍。
TransactionScopeRequired | 允许事务流的绑定 | 调用事务流 | 结果 |
False | False | No | 方法不在事务内执行。 |
True | False | No | 方法在一个新的事务中创建执行。 |
True or False | False | Yes | 对这个事务头会返回一个SOAP错误。 |
False | True | Yes | 方法不在事务内执行。 |
True | True | Yes | 方法在事务内执行。 |
列表5.18 描述了如何使用这些元素。代码与列表5.15 中显示的类似,5.15 中的代码是确定一个服务操作的事务实现,而5.18代码使用TransactionFlowOption 属性显示跨服务的事务实现。注意几个点。
首先,ServiceContract被标记为需要会话。为了实现这个需求,必须使用一个支持会话的协议,比如wsHttpBinding或者netTcpBinding。
其次,为了例证的目的,TransactionAutoComplete设置成false 同时方法的最后一行是SetTransactionComplete.如果执行达不到SetTransactionComplete,事务将自动回滚。
第三,TransactionFlowOption.Allowed 在每一个OperationContract 上设置来允许跨服务的事务调用。
列表5.18 跨边界的事务流上下文
001 | [ServiceContract(SessionMode=SessionMode.Required)] |
002 | public interface IBankService |
005 | double GetBalance( string accountName); |
008 | void Transfer( string from, string to, double amount); |
010 | public class BankService : IBankService |
012 | [OperationBehavior(TransactionScopeRequired = false )] |
013 | public double GetBalance( string accountName) |
015 | DBAccess dbAccess = new DBAccess(); |
016 | double amount = dbAccess.GetBalance(accountName); |
017 | dbAccess.Audit(accountName, "Query" , amount); |
020 | [OperationBehavior(TransactionScopeRequired = true , TransactionAutoComplete= true )] |
021 | public void Transfer( string from, string to, double amount) |
025 | Withdraw(from, amount); |
033 | [OperationBehavior(TransactionAutoComplete = false , TransactionScopeRequired = true )] |
034 | [TransactionFlow(TransactionFlowOption.Allowed)] |
035 | private void Withdraw( string accountName, double amount) |
037 | DBAccess dbAccess = new DBAccess(); |
038 | dbAccess.Withdraw(accountName, amount); |
039 | dbAccess.Audit(accountName, "Withdraw" , amount); |
040 | OperationContext.Current.SetTransactionComplete(); |
042 | [OperationBehavior(TransactionAutoComplete= false , TransactionScopeRequired= true )] |
043 | private void Deposit( string accountName, double amount) |
045 | DBAccess dbAccess = new DBAccess(); |
046 | dbAccess.Deposit(accountName, amount); |
047 | dbAccess.Audit(accountName, "Deposit" , amount); |
048 | OperationContext.Current.SetTransactionComplete(); |
054 | private SqlConnection conn; |
057 | string cs = ConfigurationManager.ConnectionStrings[ "sampleDB" ].ConnectionString; |
058 | conn = new SqlConnection(cs); |
061 | public void Deposit( string accountName, double amount) |
063 | string sql = string .Format( "Deposit {0}, {1}" , accountName, amount); |
064 | SqlCommand cmd = new SqlCommand(sql, conn); |
065 | cmd.ExecuteNonQuery(); |
067 | public void Withdraw( string accountName, double amount) |
069 | string sql = string .Format( "Withdraw {0}, {1}" , accountName, amount); |
070 | SqlCommand cmd = new SqlCommand(sql, conn); |
071 | cmd.ExecuteNonQuery(); |
073 | public double GetBalance( string accountName) |
075 | SqlParameter[] paras = new SqlParameter[2]; |
076 | paras[0] = new SqlParameter( "@accountName" , accountName); |
077 | paras[1] = new SqlParameter( "@sum" , System.Data.SqlDbType.Float); |
078 | paras[1].Direction = System.Data.ParameterDirection.Output; |
080 | SqlCommand cmd = new SqlCommand(); |
081 | cmd.Connection = conn; |
082 | cmd.CommandType = System.Data.CommandType.StoredProcedure; |
083 | cmd.CommandText = "GetBalance" ; |
085 | for ( int i = 0; i < paras.Length; i++) |
087 | cmd.Parameters.Add(paras[i]); |
090 | int n = cmd.ExecuteNonQuery(); |
091 | object o = cmd.Parameters[ "@sum" ].Value; |
092 | return Convert.ToDouble(o); |
094 | public void Audit( string accountName, string action, double amount) |
096 | Transaction txn = Transaction.Current; |
099 | Console.WriteLine( "{0} | {1} Audit:{2}" , |
100 | txn.TransactionInformation.DistributedIdentifier, |
101 | txn.TransactionInformation.LocalIdentifier, action); |
105 | Console.WriteLine( "<no transaction> Audit:{0}" , action); |
107 | string sql = string .Format( "Audit {0}, {1}, {2}" , |
108 | accountName, action, amount); |
109 | SqlCommand cmd = new SqlCommand(sql, conn); |
110 | cmd.ExecuteNonQuery(); |
列表5.19 显示了配置文件。注意绑定是支持会话的wsHttpBinding。因为代码在服务契约中声明了SessionMode.Required 所以这是必须的。也要注意transactionFlow=”true”在绑定配置部分定义。
列表5.19 在配置文件中使能事务流
01 | <?xml version= "1.0" encoding= "utf-8" ?> |
04 | <!--Change connectionString refer to your environment--> |
05 | <add connectionString= "Data Source=SQL2K8CLUSTER\SQL2K8CLUSTER;Initial Catalog=BankService;Integrated Security=True" name= "sampleDB" /> |
10 | <binding name= "transactions" transactionFlow= "true" > |
13 | <extendedProtectionPolicy policyEnforcement= "Never" /> |
21 | <behavior name= "metadata" > |
22 | <serviceMetadata httpGetEnabled= "true" /> |
27 | <service behaviorConfiguration= "metadata" name= "Services.BankService" > |
28 | <endpoint address= "" binding= "wsHttpBinding" bindingConfiguration= "transactions" |
29 | contract= "Services.IBankService" /> |
37 | </system.serviceModel> |
列表5.20 显示了将两个服务的工作合并到一个单独事务的客户端代码。创建了三个代理,两个指向一个服务,第三个指向另外一个服务。两个查询操作和一个 Withdraw 操作在proxy1上调用,然后在proxy2上调用Deposit。如果在那些服务操作内所有事情都很顺利,它们每个都会执行自己的 SetTransactionComplete().在两个操作都返回后,客户端调用scope.Complete()来完成事务。只有事务中所有部分都 执行它们的SetTransactionComplete()方法,事务才会被提交;如果它们中有没有成功的,整个事务将会被回滚。最后,proxy3会 调用两个查询操作来确定经过事务处理以后改变是一致的。
列表5.20 在一个客户端合作完成一个分布式事务
01 | using (TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew)) |
03 | BankServiceClient proxy1 = new BankServiceClient(); |
04 | BankServiceClient proxy2 = new BankServiceClient(); |
05 | Console.WriteLine( "{0}: Before - savings:{1}, checking {2}" , |
07 | proxy1.GetBalance( "savings" ), |
08 | proxy2.GetBalance( "checking" )); |
09 | proxy1.Withdraw( "savings" , 100); |
10 | proxy2.Deposit( "checking" , 100); |
16 | BankServiceClient proxy3 = new BankServiceClient(); |
17 | Console.WriteLine( "{0}: After - savings:{1}, checking {2}" , |
19 | proxy3.GetBalance( "savings" ), |
20 | proxy3.GetBalance( "checking" )); |
图片5.13 显示了一个客户端和两个服务端的输出。左边的客户端打印了savings账户的总额并在转账前后检查。两个服务端在右边。最上面的服务被Proxy1和 Proxy3访问;底下的被Proxy2访问。上面的服务执行了两个查询操作,一个Withdraw操作和另外两个查询操作。下面的服务执行了一个 Deposit操作。注意分布式事务身份id 在两个服务端都是一致的,意味着它们都是同一个事务的一部分。
图片5.13 一个单一食物中的两个合作的事务服务的输出