事务化的服务操作只能作为一个整体成功或失败。它们以一个整体被初始化,假设结果将会是一致的,无论操作最终是成功还是失败。图片5.9 使用伪代码描述这个行为。客户端打开一个到服务端的连接然后调用它的Transfer 方法。Transfer 执行一个借入,一个存入,然后标记事务完成。客户端在事务语义中不涉及。
为了在WCF中实现这个行为,服务操作必须使用[OperationBehavior(TransactionScopedRequired=true)]属性来标记为是可事务化的。这指导WCF创建一个新的事务并在将控制权给那个方法前把执行线程入列。如果操作在它完成前失败了,所有在事务中进行的对事务资源的部分更新都将被回滚。
如果TransactionScopedRequired=false 被设置(默认设置),操作就会在非事务下执行。在那个情况,操作将不支持ACID 属性。如果操作更新了一个表然后在更新第二个表时失败了,那么第一个表的更新将会保持,也就意味着ACID属性被破坏了。
你可以指示一个操作通过隐式或显式方式完成。通过使用[OperationBehavior(TransactionAutoComplete=false)]行为并在方法返回前显式调用OperationContext.Current.SetTransactionComplete()。如果你使用显式方法,你也需要在通信信道中使用一个基于会话的绑定元素,同时你需要在服务契约中使用[ServiceContract(SessionMode=SessionMode.Allowed)]来支持会话。
列表5.15 显示了一个服务,BankService,它暴露了两个服务操作。第一个服务操作,GetBalance,不是事务化的。它从数据库中读取数据并返回结果。OpeartionBehavior 的TransactionScopedRequired=false 用来说明它不需要一个事务。第二个操作,Transfer,是事务化的而且在操作行为上添加了TransactionScopedRequired=true.它调用两个内部方法,Withdraw和Deposit,每一个方法都通过DBAccess来更新数据库。传输操作隐含使用TransactionAutoComplete=true属性来标记事务的完成。如果Withdraw或者Deposit都没有抛出异常,两个操作的改变都被标记为完成。
BankService 服务使用内部类DBAccess来访问所有数据库。注意它的构造函数打开一个到数据库的连接。当DBAccess超出范围而且没有请求或者事务是活跃的,垃圾回收器将会关闭连接。主动尝试在一个析构函数中关闭连接将导致一个错误因为当超出类的范围时事务仍然可能是活跃的。
列表5.15 事务化操作
namespace Services { [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); } [OperationBehavior(TransactionAutoComplete=false, TransactionScopeRequired=true)] private void Deposit(string accountName, double amount) { DBAccess dbAccess = new DBAccess(); dbAccess.Withdraw(accountName, amount); dbAccess.Audit(accountName, "Deposit", amount); } } 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.16中显示。服务端的事务对客户端是透明的。
列表5.16 客户端调用一个事务化服务
BankServiceClient proxy = new BankServiceClient(); Console.WriteLine("{0}: Before - savings:{1}, checking {2}", DateTime.Now, proxy.GetBalance("savings"), proxy.GetBalance("checking")); proxy.Transfer("savings", "checking", 100); Console.WriteLine("{0}: After - savings:{1}, checking {2}", DateTime.Now, proxy.GetBalance("savings"), proxy.GetBalance("checking")); proxy.Close();
因为两个内部方法,Withdraw 和Deposit,每个都创建了一个新的DBAccess类,它们每个都独立的打开一个到数据库的连接。当Withdraw在事务中打开第一个连接时,事务是一个本地事务的一部分而不是一个分布式事务的一部分。当它打开第二个连接时,事务扩大到一个分布式事务所以工作可以在两个连接之间合作。DBAccess.Audit方法打印出事务的LocalIdentifier和DistributedIdentifier, 在图片5.10中显示。注意Withdraw没有在一个分布式事务中执行,因为它是在那个时间里唯一一个打开的连接。当Deposit执行时,它创建了一个分布式事务因为它是在那个事务范围内第二个打开的连接。逐步扩大自动发生同时有一个很明显的对性能的不利影响。
列表5.17 显示了完整的代码,当传输操作通过DBAccess打开一个连接并把这个连接同时发送给Withdraw和Deposit以便于只有一个连接被使用。
列表5.17 避免分布式事务的事务操作的完整代码
[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete=true)] public void Transfer(string from, string to, double amount) { try { DBAccess dbAccess = new DBAccess(); Withdraw(from, amount, dbAccess); Deposit(to, amount, dbAccess); } catch(Exception ex) { throw ex; } } [OperationBehavior(TransactionAutoComplete = false, TransactionScopeRequired = true)] [TransactionFlow(TransactionFlowOption.Allowed)] private void Withdraw(string accountName, double amount, DBAccess dbAccess) { dbAccess.Withdraw(accountName, amount); dbAccess.Audit(accountName, "Withdraw", amount); } [OperationBehavior(TransactionAutoComplete=false, TransactionScopeRequired=true)] private void Deposit(string accountName, double amount, DBAccess dbAccess) { dbAccess.Withdraw(accountName, amount); dbAccess.Audit(accountName, "Deposit", amount); }
图片5.11显示了完整服务的输出结果。注意分布式事务ID总是为0,意味着不存在分布式事务。