当在分布式系统中工作时,事务有时必须要跨越服务边界。例如,如果一个服务管理客户信息而另一个服务管理订单,一个客户提交一个订单并想产品可以发送到一个新的地址,系统将需要调用每个服务上的操作。如果事务完成,用户将会期待两个系统上的信息都被合适的更新。
如果基础架构支持一个原子事务协议,服务可以像刚才描述的那样被组合到一个复合事务中。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 一个单一食物中的两个合作的事务服务的输出