如何创建一个简单的基于OleTx协议的WCF事务实例
由于工作需要,最近研究了一下WCF分布式事务方面的技术,自己做了一个基于OleTx协议的WCF事务实例,供大家参考。
一、实例背景
本实例以大家熟悉的银行转账业务为背景,在几台不同的机器上部署一套基于OleTx协议的WCF分布式服务。
本例需要A、B、C、D四台计算机,其中A表示SqlServer服务器,B部署取款服务,C部署存款服务,D部署客户端程序(调用存款和取款服务),D同时直接A进行日志写入。一次转账操作分为三个部分:存款、取款、写日志,这三部分组成一个转账事务,要么全部成功,要么全部失败。
二、WCF服务端代码
创建一个WCF服务项目OleTxService。
1、契约
取款服务契约如下:
1 using System.Net.Security; 2 using System.ServiceModel; 3 using OleTxService.Models; 4 5 namespace OleTxService 6 { 7 /// <summary> 8 /// 取款服务接口 9 /// </summary> 10 [ServiceContract(SessionMode = SessionMode.Required)] 11 public interface IWithDrawService 12 { 13 /// <summary> 14 /// 取款操作 15 /// </summary> 16 /// <param name="accountId">取款账号</param> 17 /// <param name="amount">取款金额</param> 18 [OperationContract] 19 [TransactionFlow(TransactionFlowOption.Mandatory)] 20 [FaultContract(typeof(MyFault), ProtectionLevel = ProtectionLevel.None)] 21 void Withdraw(string accountId, double amount); 22 } 23 }
存款服务契约如下:
1 using System.Net.Security; 2 using System.ServiceModel; 3 using OleTxService.Models; 4 5 namespace OleTxService 6 { 7 /// <summary> 8 /// 存款服务接口 9 /// </summary> 10 [ServiceContract(SessionMode = SessionMode.Required)] 11 public interface IDepositService 12 { 13 /// <summary> 14 /// 存款操作 15 /// </summary> 16 /// <param name="accountId">存款账号</param> 17 /// <param name="amount">存款金额</param> 18 [OperationContract] 19 [FaultContract(typeof(MyFault), ProtectionLevel = ProtectionLevel.None)] 20 [TransactionFlow(TransactionFlowOption.Mandatory)] 21 void Deposit(string accountId, double amount); 22 } 23 }
由于后面要设置事务操作行为的TransactionAutoComplete属性为false,所以要求其中契约的Session要设置为SessionMode.Required。
在操作上要增加一个TransactionFlow特性,允许事务在客户端到服务端之间流转。
2、服务实现
取款服务如下:
1 using System; 2 using System.Configuration; 3 using System.Data; 4 using System.Data.SqlClient; 5 using System.ServiceModel; 6 using OleTxService.Helper; 7 using OleTxService.Models; 8 9 namespace OleTxService 10 { 11 /// <summary> 12 /// 取款服务 13 /// </summary> 14 public class WithDrawService : IWithDrawService 15 { 16 /// <summary> 17 /// 取款操作 18 /// </summary> 19 /// <param name="accountId">取款账号</param> 20 /// <param name="amount">取款金额</param> 21 [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)] 22 public void Withdraw(string accountId, double amount) 23 { 24 string connString = ConfigurationManager.ConnectionStrings["MyBank"].ConnectionString; 25 try 26 { 27 var _parms = new SqlParameter[] 28 { 29 SQLServerHelper.BuildInParameter("id", SqlDbType.VarChar, accountId), 30 SQLServerHelper.BuildInParameter("amount", SqlDbType.Float, amount) 31 }; 32 SQLServerHelper.ExecuteNonQuery(connString, CommandType.StoredProcedure, "P_WITHDRAW", _parms); 33 OperationContext.Current.SetTransactionComplete(); 34 } 35 catch (Exception ex) 36 { 37 throw new FaultException<MyFault>(new MyFault(string.Format("取款异常:{0}", ex.Message)), "取款出错"); 38 } 39 } 40 } 41 }
存款服务如下:
1 using System; 2 using System.Configuration; 3 using System.Data; 4 using System.Data.SqlClient; 5 using System.ServiceModel; 6 using OleTxService.Helper; 7 using OleTxService.Models; 8 9 namespace OleTxService 10 { 11 /// <summary> 12 /// 存款服务 13 /// </summary> 14 public class DepositService : IDepositService 15 { 16 /// <summary> 17 /// 存款操作 18 /// </summary> 19 /// <param name="accountId">存款账号</param> 20 /// <param name="amount">存款金额</param> 21 [OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)] 22 public void Deposit(string accountId, double amount) 23 { 24 string connString = ConfigurationManager.ConnectionStrings["MyBank"].ConnectionString; 25 try 26 { 27 var _parms = new SqlParameter[] 28 { 29 SQLServerHelper.BuildInParameter("id", SqlDbType.VarChar, accountId), 30 SQLServerHelper.BuildInParameter("amount", SqlDbType.Float, amount) 31 }; 32 SQLServerHelper.ExecuteNonQuery(connString, CommandType.StoredProcedure, "P_DEPOSIT", _parms); 33 OperationContext.Current.SetTransactionComplete(); 34 } 35 catch (Exception ex) 36 { 37 throw new FaultException<MyFault>(new MyFault(string.Format("存款异常:{0}", ex.Message)), "存款出错"); 38 } 39 } 40 } 41 }
其中,取款和存款的具体代码未贴出,读者可以自己实现。具体操作:从银行账户表的一个账号(一行数据)减少金额,另一个账号增加金额。
注意,在服务操作上增加了一个操作行为:[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = false)]。TransactionScopeRequired=true表示该服务操作需要在一个显示的TransactionScope中。TransactionAutoComplete=false表示必须手工完成该事务,其方法是在要完成事务的地方写上:OperationContext.Current.SetTransactionComplete();
3、配置WCF服务
在配置文件中配置这两个服务,有关WCF服务的配置代码如下:
<system.serviceModel> <bindings> <netTcpBinding> <binding name="transactionBinding" transactionFlow="true" transactionProtocol="OleTransactions" /> </netTcpBinding> </bindings> <services> <service name="OleTxService.DepositService"> <endpoint address="deposit" binding="netTcpBinding" bindingConfiguration="transactionBinding" contract="OleTxService.IDepositService" /> </service> <service name="OleTxService.WithDrawService"> <endpoint address="withdraw" binding="netTcpBinding" bindingConfiguration="transactionBinding" contract="OleTxService.IWithDrawService" /> </service> </services> <behaviors> <serviceBehaviors> <behavior> <!-- To avoid disclosing metadata information, set the values below to false before deployment --> <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/> <!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information --> <serviceDebug includeExceptionDetailInFaults="false"/> </behavior> </serviceBehaviors> </behaviors> <protocolMapping> <add binding="basicHttpsBinding" scheme="https" /> </protocolMapping> <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" /> </system.serviceModel>
我们将服务配置成netTcpBinding,并在绑定配置中加上允许事务流转、指定事务协议的配置项:
<bindings> <netTcpBinding> <binding name="transactionBinding" transactionFlow="true" transactionProtocol="OleTransactions" /> </netTcpBinding> </bindings>
三、在IIS中部署netTcpBinding的WCF服务
由于我们的服务是采用netTcpBinding,在IIS中寄宿时需要有些注意的地方。
1、部署网站
在IIS中新增一个网站oletransaction,用来部署本例中的服务。在本例中,服务终结点中的地址配置的是相对地址,也可以为空。
2、编辑绑定
在IIS中寄宿非HTTP协议的WCF服务时,需要手动为网站编辑相应的绑定。在本例中,需要为服务增加一个net.tcp绑定,并在其中指定绑定端口。
在IIS中选中网站,右击“编辑绑定(Edit Bindings...)”,在弹出的“站点绑定(Site Bindings)”对话框中添加一个net.tcp绑定。
其中,绑定信息“8060:*”表示设置TCP的监听端口为8060。
3、添加net.tcp协议支持
需要手动在网站中添加对net.tcp协议的支持。
选中网站,点击“高级设置(Advanced Setting...)”。
在弹出的高级设置窗口中找到“启用的协议(Enabled Protocols)”,添加net.tcp,协议之间以逗号分隔。
4、浏览服务
在完成编辑绑定、添加协议支持后,即可浏览服务:
如何不能访问,请检查防火墙是否关闭,Net Tcp监听服务是否打开。
照此方式在B、C服务器中分别部署取款服务、存款服务。
四、WCF客户端代码
1、引用服务
根据服务的WSDL地址分别引用取款服务、取款服务。
2、编写转账代码
转账的总体代码如下:
using (var scope = new TransactionScope()) { try { Transfer(fromAccountId, toAccountId, amount); var logContent = string.Format("完成一条转账操作,转出账号:{0},转入账号:{1},转账金额:{2}", fromAccountId, toAccountId, amount); WriteLog(logContent, remark); scope.Complete(); Console.WriteLine("转账成功!"); } catch (FaultException<withdraw.MyFault> ex) { Console.WriteLine("转账失败:{0},错误详情:{1}", ex.Message, ex.Detail.ErrorMessage); } catch (FaultException<deposit.MyFault> ex) { Console.WriteLine("转账失败:{0},错误详情:{1}", ex.Message, ex.Detail.ErrorMessage); } catch (Exception ex) { Console.WriteLine("转账失败:{0}", ex.Message); } }
其中转账方法Transfer定义如下:
/// <summary> /// 转账操作 /// </summary> /// <param name="fromAccountId">转出账号</param> /// <param name="toAccountId">转入账号</param> /// <param name="amount">转账金额</param> static void Transfer(string fromAccountId, string toAccountId, double amount) { withdraw.WithDrawServiceClient clientWithdraw = new WithDrawServiceClient("NetTcpBinding_IWithDrawService"); clientWithdraw.Withdraw(fromAccountId, amount); deposit.DepositServiceClient clientDeposit = new DepositServiceClient("NetTcpBinding_IDepositService"); clientDeposit.Deposit(toAccountId, amount); }
写日志的代码如下:
/// <summary> /// 记录日志 /// </summary> /// <param name="content">日志内容</param> /// <param name="remark">备注</param> static void WriteLog(string content, string remark) { var sql = ConfigurationManager.AppSettings["InsertBankRecord"]; var _params = new SqlParameter[] { SQLServerHelper.BuildInParameter("ID", SqlDbType.UniqueIdentifier, Guid.NewGuid()), SQLServerHelper.BuildInParameter("Content", SqlDbType.NVarChar, content), SQLServerHelper.BuildInParameter("Remark", SqlDbType.NVarChar, remark ?? string.Empty), SQLServerHelper.BuildInParameter("UpdateTime", SqlDbType.DateTime, DateTime.Now) }; SQLServerHelper.ExecuteNonQuery(_myConnString, CommandType.Text, sql, _params); }
在转账的总体代码中,将调用取款服务、存款服务、写日志这三个操作放到一个TransactionScope变量scope中。只有当三个操作本身正常完成,并且调用scope.Complete()后,事务成功,否则事务失败。
3、部署客户端
将客户端部署到D计算机,并配置好服务地址、数据库连接串。
五、配置MSDTC
由于本例采用的是基于OleTx协议的WCF事务,用到了微软的MSDTC服务,因此,需要配置MSDTC服务。
1、配置MSDTC
在“管理工具(Administrative Tools)”中打开“组件服务(Component Services)”。
找到我的计算机节点:“组件服务(Component Services)”=>“计算机(Computers)”=>“我的计算机(My Computer)”,右键点击“属性(Properties)”。
在“MSDTC”选项卡中,勾选“使用本地协调器(Use local coordinator)”选项。
2、配置Local DTC
还需要配置“本地DTC(Local DTC)”。
在组件服务中找到本地DTC节点:组件服务(Component Services)=>计算机(Computers)=>我的计算机(My Computer)=>分布式事务协调器(Distributed Transaction Coordinator),右击“属性(Properties)”。
在弹出的本地DTC属性窗口中,点击“安全(Security)”选项卡,配置成下面的值。
六、验证事务
部署好整个WCF分布式事务系统后,运行程序,验证事务正确性:
- 正常输入情况下应该完成整个事务
- 调用服务错误情况下应该回滚整个事务
- 本地写日志出错情况下应该回滚整个事务
七、总结
本文完成了一个基于OleTx协议的WCF分布式事务的代码编写和系统部署的过程。总共有三大要点:
- WCF事务编程,主要注意契约和操作行为中的一些关于事务的特性。
- 在IIS中部署基于netTcpBinding的WCF服务,主要关注编辑站点绑定、添加net.tcp协议支持。
- 配置MSDTC和本地DTC,这是部署WCF分布式事务的必要条件。