【WCF--初入江湖】07 分布式事务
07 分布式事务
一、前言
二、事务
三、TransactionFlow 特性
[TransactionFlow(TransactionFlowOption.Mandatory)] int serviceMethod(int value) { }
四、[ServiceBehavior]中的事务属性
【1】TransactionAutoCompleteOnSessionClose
如果想要确保关闭会话后待处理的消息仍然可以完成,应该使用该属性。
根据其属性值,事务将会在会话关闭后提交或回滚。
TransactionIsolationLevel用于指示事务隔离级别。
IsolationLevel枚举如下:
(1)ReadUncommitted:读取未提交数据,该方式在读取数据时保持共享锁定以避免读取已修改的数据,但在事务结束前可以更改这些数据,这导致非可重复读取或幻读。
(2)ReadCommitted:读取提交数据, 发出共享锁定并允许非独占方式的锁定。该方式与读取未提交数据相相似,这种方式看似和读取未提交数据相似,但有一个区别,事务的只读锁在移到下一行的时候,会解锁,而写入锁却只有在事务完成或者被中止后才解锁,事务要等待所有写入锁解锁。
(3)RepeatableRead:可重复性读取,与读取提交数据相似,在查询中使用的所有数据上放置锁,以防止其他用户更新这些数据。防止非可重复读取,但幻读行仍有可能发生。该方式是只读锁也要等到事务结束或者中止才解除
(4)Serializable:在完成事务前防止更新或插入。
ReadUncommitted是最低的隔离级别
Serializable是最高的隔离级别
---------------
理解事务隔离级别:
用于指示事务的超时时间,默认为TimeSpan.Zero,表示不会受超时时间的限制。
示例:
ServiceBehavior(TransactionAutoCompleteOnSessionClose=true,
TransactionIsolationLevel=IsolationLevel.ReadCommitted,
TransactionTimeout="00:00:30")] public Class ServiceClass:IServiceClass { }
五、实例
设置步骤:
Create database WCFTransactionDb; use WCFTransactionDb; Create table Account ( Id int not null primary key, --账号 Balance float --余额 ) insert into Account values(1000, 3000.0); insert into Account values(1001, 2000.0);
更新和查看:
update Account set Balance=Balance-200
where Id=1000;
update Account set Balance=Balance+200
where Id=1001;
SELECT TOP 1000 [Id]
,[Balance]
FROM [WCFTransactionDb].[dbo].[Account]
【ServiceBehavior】的有关属性 及其要实现事务的方法添加[OperationBehavior(TransactionScopeRequired = true ]
IService1.cs
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; namespace Keasy5.WCF.Transaction.WCFService { [ServiceContract] public interface IService1 { [OperationContract] [TransactionFlow(TransactionFlowOption.Mandatory/*强制事务必须成为流*/)] void OutMoney(int id, double money); [OperationContract] [TransactionFlow(TransactionFlowOption.Mandatory/*强制事务必须成为流*/)] void IntoMoney(int id, double money); } }
Service1.cs
namespace Keasy5.WCF.Transaction.WCFService { [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single/*对象用于所有传入呼叫,并且在调用后不回收。如果服务对象不存在,则创建一个。*/ ,ReleaseServiceInstanceOnTransactionComplete = false /*获取或设置一个值,该值指定在完成当前事务后是否释放服务对象*/ )] public class Service1 : IService1 { [OperationBehavior(TransactionScopeRequired = true /*该值指示方法在执行时是否需要事务范围*/)] public void OutMoney(double money) { } [OperationBehavior(TransactionScopeRequired = true /*该值指示方法在执行时是否需要事务范围*/)] public void IntoMoney(double money) { } } }
using System; using System.Collections.Generic; using System.Data; using System.Data.SqlClient; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; namespace Keasy5.WCF.Transaction.WCFService { [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single/*对象用于所有传入呼叫,并且在调用后不回收。如果服务对象不存在,则创建一个。*/ ,ReleaseServiceInstanceOnTransactionComplete = false /*获取或设置一个值,该值指定在完成当前事务后是否释放服务对象*/ )] public class Service1 : IService1 { private const string ConnectString = "Data Source=.;Initial Catalog=WCFTransactionDb;Integrated Security=True"; [OperationBehavior(TransactionScopeRequired = true /*该值指示方法在执行时是否需要事务范围*/)] public void OutMoney(int id, double money) { ChangeMoneyToDb(id, money, false); } [OperationBehavior(TransactionScopeRequired = true /*该值指示方法在执行时是否需要事务范围*/)] public void IntoMoney(int id, double money) { ChangeMoneyToDb(id, money, true); } /// <summary> /// /// </summary> /// <param name="id"></param> /// <param name="money"></param> /// <param name="inMoney">inMoney=true表示转入,否则表示转出</param> private static void ChangeMoneyToDb(int id, double money, bool inMoney) { using (SqlConnection sqlConnection = new SqlConnection(ConnectString)) { sqlConnection.Open(); using (SqlCommand command = sqlConnection.CreateCommand()) { string updateSql = null; if (inMoney) { updateSql = "update Account set Balance=Balance+@Money where Id=@Id;"; //updateSql = string.Format("update Account set Balance=Balance+{0} where Id={1}", money, id)+";"; } else { updateSql = "update Account set Balance=Balance-@Money where Id=@Id;"; //updateSql = string.Format("update Account set Balance=Balance-{0} where Id={1}", money, id)+";"; } command.CommandText = updateSql; command.CommandType = CommandType.Text; command.Parameters.Add(new SqlParameter("@Id", id)); command.Parameters.Add(new SqlParameter("@Money", money)); command.ExecuteNonQuery(); } } } } }
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.web> <compilation debug="true" /> </system.web> <system.serviceModel> <bindings> <wsHttpBinding> <binding name="wsHttpBinding_translationBind" transactionFlow="true"></binding> </wsHttpBinding> </bindings> <services> <service name="Keasy5.WCF.Transaction.WCFService.Service1"> <host> <baseAddresses> <add baseAddress = "http://localhost:8733/Design_Time_Addresses/Keasy5.WCF.Transaction.WCFService/Service1/" /> </baseAddresses> </host> <endpoint address="bank" binding="wsHttpBinding" bindingConfiguration="wsHttpBinding_translationBind" contract="Keasy5.WCF.Transaction.WCFService.IService1"></endpoint> </service> </services> <behaviors> <serviceBehaviors> <behavior> <serviceMetadata httpGetEnabled="True"/> <serviceDebug includeExceptionDetailInFaults="False" /> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel> </configuration>
System.InvalidOperationException: “Service1”协定上至少有一个操作配置为将 TransactionFlowAttribute 特性设置为“强制”,但是通道的绑定“BasicHttpBinding”未使用 TransactionFlowBindingElement 进行配置。没有 TransactionFlowBindingElement,无法使用设置为“强制”的 TransactionFlowAttribute 特性。
在 System.ServiceModel.Dispatcher.TransactionValidationBehavior.ValidateTransactionFlowRequired(String resource, String name, ServiceEndpoint endpoint)
在 System.ServiceModel.Dispatcher.TransactionValidationBehavior.System.ServiceModel.Description.IServiceBehavior.Validate(ServiceDescription service, ServiceHostBase serviceHostBase)
在 System.ServiceModel.Description.DispatcherBuilder.ValidateDescription(ServiceDescription description, ServiceHostBase serviceHost)
在 System.ServiceModel.Description.DispatcherBuilder.InitializeServiceHost(ServiceDescription description, ServiceHostBase serviceHost)
在 System.ServiceModel.ServiceHostBase.InitializeRuntime()
在 System.ServiceModel.ServiceHostBase.OnOpen(TimeSpan timeout)
在 System.ServiceModel.Channels.CommunicationObject.Open(TimeSpan timeout)
在 Microsoft.Tools.SvcHost.ServiceHostHelper.OpenService(ServiceInfo info)
解决方法是:为endpoint 配置一个transactionFlow="true"的Binding。
<bindings> <wsHttpBinding> <binding name="wsHttpBinding_translationBind" transactionFlow="true"></binding> </wsHttpBinding> </bindings> <endpoint address="bank" 。。。 bindingConfiguration="wsHttpBinding_translationBind" 。。。。 ></endpoint>
<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <bindings> <wsHttpBinding> <binding name="WSHttpBinding_IService1" transactionFlow="true" /> </wsHttpBinding> </bindings> <client> <endpoint address="http://localhost:8733/Design_Time_Addresses/Keasy5.WCF.Transaction.WCFService/Service1/bank" binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IService1" contract="TransactionWCFService.IService1" name="Transaction_IService1"> </endpoint> </client> </system.serviceModel> </configuration>
private void buttonChangedAccount_Click(object sender, EventArgs e) { int outId = Convert.ToInt32(this.textBoxOutId.Text); int inId = Convert.ToInt32(this.textBoxInId.Text); double money = Convert.ToDouble(this.textBoxMoney.Text); Service1Client client = new Service1Client("Transaction_IService1"); using (System.Transactions.TransactionScope transactionScope = new TransactionScope()) { client.OutMoney(outId, money); client.IntoMoney(inId, money); transactionScope.Complete(); } }
【3】测试事务
到目前,转账功能完成,
为测试事务是否成功,服务端或客户端故意中抛出一个异常。
private void buttonChangedAccount_Click(object sender, EventArgs e) { int outId = Convert.ToInt32(this.textBoxOutId.Text); int inId = Convert.ToInt32(this.textBoxInId.Text); double money = Convert.ToDouble(this.textBoxMoney.Text); Service1Client client = new Service1Client("Transaction_IService1"); using (System.Transactions.TransactionScope transactionScope = new TransactionScope()) { try { client.OutMoney(outId, money); client.IntoMoney(inId, money); throw new FaultException("发生故障,转账失败"); transactionScope.Complete(); } catch (FaultException faultException) { System.Transactions.Transaction.Current.Rollback(); MessageBox.Show(faultException.Message); } }
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single/*对象用于所有传入呼叫,并且在调用后不回收。如果服务对象不存在,则创建一个。*/ ,ReleaseServiceInstanceOnTransactionComplete = false /*获取或设置一个值,该值指定在完成当前事务后是否释放服务对象*/ )] public class Service1 : IService1 { 。。。。。 [OperationBehavior(TransactionScopeRequired = true /*该值指示方法在执行时是否需要事务范围*/)] public void IntoMoney(int id, double money) { ChangeMoneyToDb(id, money, true); throw new FaultException("发生故障,转账失败"); } 。。。。。。。。。
出现异常后,两个账号的余额均没有改变。事务起效了。
如果调用不支持事务的服务方法,会是什么样子的
为了进行探究:在IService和Service1,添加两个不支持事务的两个方法:
OutMoneyNoTransaction和IntoMoneyNoTransaction
IService1.cs
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; namespace Keasy5.WCF.Transaction.WCFService { [ServiceContract] public interface IService1 { [OperationContract] [TransactionFlow(TransactionFlowOption.Mandatory/*强制事务必须成为流*/)] void OutMoney(int id, double money); [OperationContract] [TransactionFlow(TransactionFlowOption.Mandatory/*强制事务必须成为流*/)] void IntoMoney(int id, double money); [OperationContract] void OutMoneyNoTransaction(int id, double money); [OperationContract] void IntoMoneyNoTransaction(int id, double money); } }
Service1.cs
using System; using System.Collections.Generic; using System.Data; using System.Data.SqlClient; using System.Linq; using System.Runtime.Serialization; using System.ServiceModel; using System.Text; namespace Keasy5.WCF.Transaction.WCFService { [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single/*对象用于所有传入呼叫,并且在调用后不回收。如果服务对象不存在,则创建一个。*/ ,ReleaseServiceInstanceOnTransactionComplete = false /*获取或设置一个值,该值指定在完成当前事务后是否释放服务对象*/ )] public class Service1 : IService1 { private const string ConnectString = "Data Source=.;Initial Catalog=WCFTransactionDb;Integrated Security=True"; 。。。。 /// <summary> /// /// </summary> /// <param name="id"></param> /// <param name="money"></param> /// <param name="inMoney">inMoney=true表示转入,否则表示转出</param> private static void ChangeMoneyToDb(int id, double money, bool inMoney) { using (SqlConnection sqlConnection = new SqlConnection(ConnectString)) { sqlConnection.Open(); using (SqlCommand command = sqlConnection.CreateCommand()) { string updateSql = null; if (inMoney) { updateSql = "update Account set Balance=Balance+@Money where Id=@Id;"; //updateSql = string.Format("update Account set Balance=Balance+{0} where Id={1}", money, id)+";"; } else { updateSql = "update Account set Balance=Balance-@Money where Id=@Id;"; //updateSql = string.Format("update Account set Balance=Balance-{0} where Id={1}", money, id)+";"; } command.CommandText = updateSql; command.CommandType = CommandType.Text; command.Parameters.Add(new SqlParameter("@Id", id)); command.Parameters.Add(new SqlParameter("@Money", money)); command.ExecuteNonQuery(); } } } public void OutMoneyNoTransaction(int id, double money) { ChangeMoneyToDb(id, money, false); } public void IntoMoneyNoTransaction(int id, double money) { ChangeMoneyToDb(id, money, true); throw new FaultException("发生故障,转账失败"); } } }
客户端和服务端的配置不需要改变。
客户端的调用,如下:
private void ChangeAccountNoTrasacion_Click(object sender, EventArgs e) { int outId = Convert.ToInt32(this.textBoxOutId.Text); int inId = Convert.ToInt32(this.textBoxInId.Text); double money = Convert.ToDouble(this.textBoxMoney.Text); Service1Client client = new Service1Client("Transaction_IService1"); using (System.Transactions.TransactionScope transactionScope = new TransactionScope()) { try { client.OutMoneyNoTransaction(outId, money); client.IntoMoneyNoTransaction(inId, money); transactionScope.Complete(); } catch (FaultException faultException) { System.Transactions.Transaction.Current.Rollback(); MessageBox.Show(faultException.Message); } } }
虽然调用服务的两个不支持事务的方法被放在:
transactionScope = new TransactionScope()中
using (System.Transactions.TransactionScope transactionScope = new TransactionScope()) { 。。。。 client.OutMoneyNoTransaction(outId, money); client.IntoMoneyNoTransaction(inId, money); 。。。。
但是两个账号的余额还是发生了改变。事务没有起效。
其他事务
【1】SQL中的事务处理
我们可以通过如下三个SQL语句实现事务的启动、提交与回滚:
using (DbTransaction transaction = connection.BeginTransaction()) { }
示例:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data; using System.Data.SqlClient; namespace WindowsFormsApplication1 { class Class1 { SqlConnection conn; //连接对象 SqlTransaction tran; //事务对象 public Class1() { conn = new SqlConnection("server=.;uid=sa;pwd=sa;database=master"); } //转出 private void OutMoney(int m) { SqlCommand cmd = new SqlCommand("update account set balance=balance-" + m + " where ID='A'", conn); cmd.Transaction = tran; cmd.ExecuteNonQuery(); } //转入 private void intoMoney(int m) { SqlCommand cmd = new SqlCommand("update account set balance=balance+" + m + " where ID='B'", conn); cmd.Transaction = tran; cmd.ExecuteNonQuery(); } //公开转帐 public void TransferMoney(int m) { conn.Open(); tran = conn.BeginTransaction(); //开启事务 try { OutMoney(m); intoMoney(m); tran.Commit(); //提交事务 } catch (Exception err) { tran.Rollback(); //回滚事务 } finally { conn.Close(); } } } }
源码下载
链接: http://pan.baidu.com/s/1pJPqg7h 密码: 6yjh