当在分布式系统中工作时,事务有时必须要跨越服务边界。例如,如果一个服务管理客户信息而另一个服务管理订单,一个客户提交一个订单并想产品可以发送到一个新的地址,系统将需要调用每个服务上的操作。如果事务完成,用户将会期待两个系统上的信息都被合适的更新。
如果基础架构支持一个原子事务协议,服务可以像刚才描述的那样被组合到一个复合事务中。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 跨边界的事务流上下文
[ServiceContract(SessionMode=SessionMode.Required)] public interface IBankService { [OperationContract] double GetBalance(string accountName); [OperationContract] void Transfer(string from, string to, double amount); } public class BankService : IBankService { [OperationBehavior(TransactionScopeRequired = false)] public double GetBalance(string accountName) { DBAccess dbAccess = new DBAccess(); double amount = dbAccess.GetBalance(accountName); dbAccess.Audit(accountName, "Query", amount); return amount; } [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete=true)] public void Transfer(string from, string to, double amount) { try { Withdraw(from, amount); Deposit(to, amount); } catch(Exception ex) { throw ex; } } [OperationBehavior(TransactionAutoComplete = false, TransactionScopeRequired = true)] [TransactionFlow(TransactionFlowOption.Allowed)] private void Withdraw(string accountName, double amount) { DBAccess dbAccess = new DBAccess(); dbAccess.Withdraw(accountName, amount); dbAccess.Audit(accountName, "Withdraw", amount); OperationContext.Current.SetTransactionComplete(); } [OperationBehavior(TransactionAutoComplete=false, TransactionScopeRequired=true)] private void Deposit(string accountName, double amount) { DBAccess dbAccess = new DBAccess(); dbAccess.Deposit(accountName, amount); dbAccess.Audit(accountName, "Deposit", amount); OperationContext.Current.SetTransactionComplete(); } } class DBAccess { private SqlConnection conn; public DBAccess() { string cs = ConfigurationManager.ConnectionStrings["sampleDB"].ConnectionString; conn = new SqlConnection(cs); conn.Open(); } public void Deposit(string accountName, double amount) { string sql = string.Format("Deposit {0}, {1}", accountName, amount); SqlCommand cmd = new SqlCommand(sql, conn); cmd.ExecuteNonQuery(); } public void Withdraw(string accountName, double amount) { string sql = string.Format("Withdraw {0}, {1}", accountName, amount); SqlCommand cmd = new SqlCommand(sql, conn); cmd.ExecuteNonQuery(); } public double GetBalance(string accountName) { SqlParameter[] paras = new SqlParameter[2]; paras[0] = new SqlParameter("@accountName", accountName); paras[1] = new SqlParameter("@sum", System.Data.SqlDbType.Float); paras[1].Direction = System.Data.ParameterDirection.Output; SqlCommand cmd = new SqlCommand(); cmd.Connection = conn; cmd.CommandType = System.Data.CommandType.StoredProcedure; cmd.CommandText = "GetBalance"; for (int i = 0; i < paras.Length; i++) { cmd.Parameters.Add(paras[i]); } int n = cmd.ExecuteNonQuery(); object o = cmd.Parameters["@sum"].Value; return Convert.ToDouble(o); } public void Audit(string accountName, string action, double amount) { Transaction txn = Transaction.Current; if (txn != null) { Console.WriteLine("{0} | {1} Audit:{2}", txn.TransactionInformation.DistributedIdentifier, txn.TransactionInformation.LocalIdentifier, action); } else { Console.WriteLine("<no transaction> Audit:{0}", action); } string sql = string.Format("Audit {0}, {1}, {2}", accountName, action, amount); SqlCommand cmd = new SqlCommand(sql, conn); cmd.ExecuteNonQuery(); } }
列表5.19 显示了配置文件。注意绑定是支持会话的wsHttpBinding。因为代码在服务契约中声明了SessionMode.Required 所以这是必须的。也要注意transactionFlow=”true”在绑定配置部分定义。
列表5.19 在配置文件中使能事务流
<?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <!--Change connectionString refer to your environment--> <add connectionString="Data Source=SQL2K8CLUSTER\SQL2K8CLUSTER;Initial Catalog=BankService;Integrated Security=True" name="sampleDB"/> </connectionStrings> <system.serviceModel> <bindings> <wsHttpBinding> <binding name="transactions" transactionFlow="true"> <security> <transport> <extendedProtectionPolicy policyEnforcement="Never" /> </transport> </security> </binding> </wsHttpBinding> </bindings> <behaviors> <serviceBehaviors> <behavior name="metadata"> <serviceMetadata httpGetEnabled="true" /> </behavior> </serviceBehaviors> </behaviors> <services> <service behaviorConfiguration="metadata" name="Services.BankService"> <endpoint address="" binding="wsHttpBinding" bindingConfiguration="transactions" contract="Services.IBankService" /> <host> <baseAddresses> <add baseAddress="http://localhost:8000/EssentialWCF" /> </baseAddresses> </host> </service> </services> </system.serviceModel> </configuration>
列表5.20 显示了将两个服务的工作合并到一个单独事务的客户端代码。创建了三个代理,两个指向一个服务,第三个指向另外一个服务。两个查询操作和一个Withdraw 操作在proxy1上调用,然后在proxy2上调用Deposit。如果在那些服务操作内所有事情都很顺利,它们每个都会执行自己的SetTransactionComplete().在两个操作都返回后,客户端调用scope.Complete()来完成事务。只有事务中所有部分都执行它们的SetTransactionComplete()方法,事务才会被提交;如果它们中有没有成功的,整个事务将会被回滚。最后,proxy3会调用两个查询操作来确定经过事务处理以后改变是一致的。
列表5.20 在一个客户端合作完成一个分布式事务
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew)) { BankServiceClient proxy1 = new BankServiceClient(); BankServiceClient proxy2 = new BankServiceClient(); Console.WriteLine("{0}: Before - savings:{1}, checking {2}", DateTime.Now, proxy1.GetBalance("savings"), proxy2.GetBalance("checking")); proxy1.Withdraw("savings", 100); proxy2.Deposit("checking", 100); scope.Complete(); proxy1.Close(); proxy2.Close(); } BankServiceClient proxy3 = new BankServiceClient(); Console.WriteLine("{0}: After - savings:{1}, checking {2}", DateTime.Now, proxy3.GetBalance("savings"), proxy3.GetBalance("checking")); proxy3.Close();
图片5.13 显示了一个客户端和两个服务端的输出。左边的客户端打印了savings账户的总额并在转账前后检查。两个服务端在右边。最上面的服务被Proxy1和Proxy3访问;底下的被Proxy2访问。上面的服务执行了两个查询操作,一个Withdraw操作和另外两个查询操作。下面的服务执行了一个Deposit操作。注意分布式事务身份id 在两个服务端都是一致的,意味着它们都是同一个事务的一部分。
图片5.13 一个单一食物中的两个合作的事务服务的输出