引言
其实事务在数据层、服务层、业务逻辑层多处地方都会使用到,在本篇文章将会为大家一一细说。
其中前面四节是事务的基础,后面的三节是事务的重点,对事务有基础的朋友可以跳过前面四节。
文章有错漏的地方欢迎各位点评。
目录
一、事务的定义
所谓事务,它是一个操作集合,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。典型的例子就像从网上银行系统的帐户A转帐到帐户B,它经过两个阶段:1.从帐户A取出款项。2.把款项放入帐户B中。这两个过程要么同时成功,要么同时失败,这一系列的操作就被称为事务性(Transactional)操作。
在一个事务性操作的环境下,操作有着以下的4种特性,被称为ACID特性
原子性(Atomicity) | 当事务结束,它对所有资源状态的改变都被视为一个操作,这些操作要不同时成功,要不同时失败 |
一致性(Consistency) | 操作完成后,所有数据必须符合业务规则,否则事务必须中止 |
隔离性(Isolation) | 事务以相互隔离的方式执行,事务以外的实体无法知道事务过程中的中间状态 |
持久性(Durable) | 事务提交后,数据必须以一种持久性方式存储起来 |
二、事务管理器
在软件系统当中可以看到无论在数据库、Web服务、WCF、文件系统都存在着数据参与到事务运作当中,我们把管理这些数据的工具称为资源管理器RM(Resources Manager)。而事务管理器TM(Transaction Manager)就是协调多个资源管理器的工作,保证数据完整性的工具。
由上图可以看到事务的管理流程,系统通知事务管理器TM来启动事务,事务管理器TM控制向多个资源管理器RM并协调RM之间的事务操作。图中存在两个持久化RM,分别管理数据库和文件系统,这些事务操作要不同时成功,要不同时失败。
事务管理器一般分为三类:轻量级事务管理器(LTM)、核心事务管理器(KTM)、分布式事务协调器(DTC)
1. 轻量级事务管理器 (LTM)
它是包括在System.Transactions 命名空间内的一个事务管理框架,它只能管理单个应用程序域内的事务。LTM 可以管理多个易变的RM,但只能管理一个持久化RM。若事务试图加入第二个持久化RM,那轻量级事务管理器LTM将提升级别。LTM是性能最高的事务管理器,在可选择的情况下应该尽可能地使用 LTM 事务管理器。
这里易变RM是指它参与会引发 “未确定状态” 的2PC事务时候,不需要恢复服务,更多时候,易变RM的数据只存储在内存当中。
而持久化RM是指它参与会引发 “未确定状态” 的2PC事务时候,它需要恢复服务,持久化RM管理的数据是在于硬盘当中。所以,参与2PC事务的的持久RM必须有新旧两个版本,如果事务引发 “未确定状态” 的时候,那么它就会联系持久化RM,恢复到其中一个版本。
2PC 是2 Phase Commit的缩写,代表事务的2阶段提交验证算法:在数据提交时,第一阶段:应用程序记录每个数据源并执行更新请求,TM通知每个RM来执行分布式事 务,然后每个RM都对数据执行本地的事务,在事务将提交前,TM会与各个RM进行信息交换,以获知更新是否成功。第二阶段,如果其中任何一个RM表示更新 失败,TM就会通知所有的RM事务操作失败,实现数据回滚。如果所有RM的操作都成功,那么整个TM事务就宣告成功。
2. 核心事务管理器 (KTM)
KTM是用于Windows Vista和Windows Server 2008 系统中的轻量级事务管理器,与LTM相像,它可以管理多个易变的RM,但只能管理一个持久化RM。
3. 分布式事务协调器(DTC)
分布式事务协调器DTC(Distributed Transaction Coordinator)能管理多个持久化RM中的事务,事务可以跨越应用程序域、进程、硬件、域等所有的边界。在Windows Server 2008当中,DTC支持OleDB、XA、WS-AtomicTransaction、WSCoordination、WS-BusinessActivity等多个协议。由于分布式事务需要在多个参与方之间实现多次通讯,所以是一种巨大的开销,因此,在可以使用LTM和KTM的时候,应该尽量避免使用DTC。在上面图片中的事务同时启动了两个RM分别处理数据库数据与文件数据,当中启动的就是DTC分布式事务。
4.事务类 System.Transactioins.Transaction
Transaction是由Framework 2.0 就开始引入,用于显示管理事务的一个类。通过Transaction可以直接管理事务的中止、释放,也可以获取、克隆当前的环境事务类。
- Transaction的公用属性
其中Transaction.Current 比较常用,它可以指向一个当前运行环境中的事务,如果环境事务不存在,系统将返回一个null
Transaction transaction=Transaction.Current;
属性 | 说明 |
---|---|
Current | 获取或设置环境事务。 |
IsolationLevel | 获取事务的隔离级别。 |
TransactionInformation | 检索有关某个事务的附加信息。 |
- Transaction的常用公用方法
其中Rollback、Dispose方法可以控制事务中止、释放,而Clone、DependentClone方法在多线程操作中经常用到,在 “异步事务” 一节中将详细说明
方法 | 说明 |
---|---|
Rollback | 中止事务、回滚。 |
Dispose | 释放事务对象。 |
Clone | 创建事务克隆 |
DependentClone | 创建事务的依赖克隆。 |
- Transaction的事件
在事务完成后,会触发TransactionCompleted事件,开发人员可以在此事件的过程监测其状态
事件 | 说明 |
---|---|
TransactionCompleted | 在事务完成后执行 |
5. 事务状态 TransactionInformation
上面讲解过事务分为本地事务与分布式事务,而Transaction类的TransactionInformation是事务状态的记录,它可以跟踪事务动作,分辨事务现处的状态,记录本地事务与分布式事务的Guid。
TransactionInformation有两个重要成员
1 public class TransactionInformation 2 { 3 //返回分布式事务标识符 4 public Guid DistributedIdentifier 5 {get;} 6 7 //返回本地事务标识符 8 public string LocalIdentifier 9 {get;} 10 }
LocalIndentifier是本地事务的标识符,它可以获取本地事务管理器(LTM)的ID,并且注意只要事务存在,它的值就永远不会是null。它包含两个部分,一个是LTM的Guid,它是应用程序中的唯一值,代表了现存应用程序域分配的LTM。另一部分是一个可变量,代表了当时该应用程序域中的事务数量。
例如:3427dec9-4abc-34cc-9edf-30ad835c33k3:3
其中3427dec9-4abc-34cc-9edf-30ad835c33k3是此本地事务管理器的Guid,在事务启动后,此值都是不变的,而 “3” 代表此刻该应用程序域中存在 “3” 个本地事务。
DistributedIndentifier是分布式事务的标识符,在普通情况下DistributedIndentifier的值都为Guid.Empty。但当LTM或KTM事务被提升到分布式事务时,DistributedIndentifier就会产生。最重的是,在同一个分布式事务管理器当中,即使事务跨越服务边界,分布式ID都是一致的。DistributedIndentifier是分布式事务的唯一标识符,它的使用方法在后面
“事务的传播” 一节将详细介绍。
在TransactionManager类中,还提供了一个事件DistributedTransactionStarted专门用于测试分布式事务的变化。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 using (TransactionScope scope = new TransactionScope()) 6 { 7 TransactionManager.DistributedTransactionStarted += OnDistributedTransactionStarted; 8 ............ 9 scope.Complete(); 10 } 11 Console.ReadKey(); 12 } 13 14 //当执行分布式事务是就会启动此方法显示事务信息 15 static void OnDistributedTransactionStarted(object sender, TransactionEventArgs args) 16 { 17 Transaction transaction = args.Transaction; 18 Console.WriteLine("Distributed Transaction Started!\n DistributedIndentifier:" 19 +transaction.TransactionInformation.DistributedIdentifier); 20 } 21 }
基础知识就先讲到这里,下面开始介绍一下事务的具体用法。
三、在ADO.NET中实现事务
1. ADO.NET事务的主要成员
需要使用事务管理的情况很多,在数据层使用得特别广泛,几乎每一个系统的数据层都会实现事务。数据层的事务都是继承自DBTransaction,派生自IDbTransaction的。下面介绍一下IDbTransaction的基本成员:
1 public interface IDbTransaction:IDisposable 2 { 3 IDbConnection Connection {get;} //返回Connection对象 4 IsolationLevel IsolationLevel{get;} 5 void Commit(); //数据提交,把所有改变数据保存到持久化数据库 6 void Rollback(); //数据回滚,把所有数据恢复原值 7 }
其中Connection属性是返回初始化此事务时所引用的连接对象的。Commit()方法应该在完成所有数据操作后才调用,调用该方法后,已经改变的数据将会保存到持久化数据库当中。而Rollback()是出现错误时调用的,调用后数据将返回初始值。IsolationLevel是指定遇到其它并行事务时的处理方式。
ADO.NET当中有多个子类都继续自DBTransaction,其中SqlTransaction是比较常用的,SqlTransaction中还定义了一个Save()方法,这个方法允许开发人员把失败的事务回滚到上一个保存点而不回滚整个事务。而在DataContext类里面,Transaction属性会返回DBTransaction对象。
2.开发实例
在传统的ADO.NET中使用事务,方法如下:
1 private static void Execute(string connectionString) 2 { 3 using (SqlConnection connection = new SqlConnection(connectionString)) 4 { 5 connection.Open(); 6 7 SqlCommand command = connection.CreateCommand(); 8 SqlTransaction transaction; 9 10 //启动事务 11 transaction = connection.BeginTransaction("SampleTransaction"); 12 13 //设定SqlCommand的事务和连接对象 14 command.Connection = connection; 15 command.Transaction = transaction; 16 17 try 18 { 19 command.CommandText ="Insert into ......"; 20 command.ExecuteNonQuery(); 21 22 // 完成提交 23 transaction.Commit(); 24 ...... 25 } 26 catch (Exception ex) 27 { 28 //数据回滚 29 transaction.Rollback(); 30 ..... 31 } 32 } 33 }
在DataContext中使用事务,方法极其相似,不同的是SqlCommand中事务为SqlTransaction,在DataContext中事务为DbTransaction
1 using(MyDataContext context=new MyDataContext()) 2 { 3 try 4 { 5 context.Connection.Open(); 6 context.Transaction=context.Connection.BeginTransaction(); 7 //更新数据 8 ......... 9 context.SubmitChanges(); 10 //事务提交 11 context.Transaction.Commit(); 12 } 13 catch(Excetion ex) 14 { 15 //数据回滚 16 context.Transaction.Rollback(); 17 //错误处理 18 ......... 19 } 20 }
四、隐式事务 TransactionScope
1. TransactionScope的概念
TransactionScope存在于System.Transactions 命名空间中, 它是从Framework 2.0开始引入的一个事务管理类,它也是微软推荐使用的一个事务管理类。在TransactionScope的构造函数中会自动创建了一个新的LTM(轻量级事务管理器),并通过Transaction.Current 隐式把它设置为环境事务。在使用隐式事务时,事务完成前程序应该调用TransactionScope的Complete()方法,把事务提交,最后利用Dispose()释放事务对象。若执行期间出现错误,事务将自动回滚。
1 public class TransactionScope:IDisposable 2 { 3 //多个构造函数 4 public TransactionScope(); 5 public TransactionScope(Transaction) 6 public TransactionScope(TransactionScopeOption) 7 ...... 8 public void Complete(); //提交事务 9 public void Dispose(); //释放事务对象 10 } 11 12 //调用方式 13 using(TransactionScope scope=new TransactionScope()) 14 { 15 //执行事务型工作 16 ............ 17 scope.Complete(); 18 }
2. TransactionScope的构造函数 TransactionScope(transactionScopeOption)
TransactionScopeOption 是枚举的一个实例,它主要用于TransactionScope的构造函数内,定义事务生成的状态要求。
在MSDN里面可以找到它的定义:
http://msdn.microsoft.com/zh-cn/library/system.transactions.transactionscopeoption.aspx
成员名称 | 说明 |
---|---|
Required | 该范围需要一个事务。 如果已经存在环境事务,则使用该环境事务。 否则,在进入范围之前创建新的事务。 这是默认值。 |
RequiresNew | 总是为该范围创建新事务。 |
Suppress | 环境事务上下文在创建范围时被取消。 范围中的所有操作都在无环境事务上下文的情况下完成。 |
这里Suppress有点特别,当使用Suppress范围内,所有的操作都将在无事务的上下文中执行,即当中的程序不再受到事务的保护,这大多数在嵌套式事务中使用。
1 void DoWork() 2 { 3 using(TransactionScope scope=new TransactionScope()) 4 { 5 //在事务环境中执行操作 6 ...... 7 NoTransaction(); 8 scope.Complete(); 9 } 10 } 11 12 void NoTransaction() 13 { 14 //在无事务环境中执行操作 15 using(TransactionScope scope=new TransactionScope(TransactionScopeOption.Suppress)) 16 { 17 ...... 18 } 19 }
3. 应用实例,在Entity Framework中使用TransactionScope
1 public Order GetOrder(int id) 2 { 3 BusinessContext _context = new BusinessContext(); 4 Order order = null; 5 try 6 { 7 using (TransactionScope scope = new TransactionScope()) 8 { 9 //数据操作
10 var list = _context.Order.Include("OrderItem") 11 .Where(x => x.ID == id); 12 if (list.Count() > 0) 13 order = list.First(); 14 else 15 order = new Order(); 16 scope.Complete(); //事务提交 17 } 18 } 19 catch (Exception ex) 20 { 21 ...... //出错处理,并返回一个空对象 22 order=new Order(); 23 } 24 _context.Dispose(); 25 return order; 26 }
五、在WCF中实现事务
1. WCF服务中事务的边界
把WCF的事务边界独立成一节,是想大家注意这一点,WCF服务中,事务是以方法为边界的,每个WCF服务的方法可以有独立事务的执行模式。而事务可以在多个服务中传播,也可以在服务端与客户端之间传播,介时事务管理器的级数将会晋升。
2.简单的事务使用方式
TransactionScopeRequired与TransactionAutoComplete是WCF事务的基本元素。
当TransactionScopeRequired等于true时,代表在此WCF服务的方法中启动事务。反之,当此值为false时代表此方法不执行事务。
当TransactionAutoComplete等于true时,代表该方法使用隐式事务,这也是微软推荐使用的方法。即当该方法在运行过程中没有抛出Exception,操作就默认为完成,事务将自动提交。如果期间出现任何异常,事务就会自动回滚。如果TransactionAutoComplete等于false时,该方法即为显式事务,即需要在方法完成时利用OperationContext.Current.SetTransactionComplete () 显式提交事务。
1 [ServiceContract(SessionMode=SessionMode.Required)] 2 public interface IService 3 { 4 [OperationContract] 5 void Method1(); 6 7 [OperationContract] 8 void Method2(); 9 } 10 11 public class Service : IService 12 { 13 //隐式事务 14 [OperationBehavior(TransactionScopeRequired=true,TransactionAutoComplete=true)] 15 public void Method1() 16 { 17 ........... 18 } 19 20 //显式事务 21 [OperationBehavior(TransactionScopeRequired=true,TransactionAutoComplete=false)] 22 public void Method2() 23 { 24 ........... 25 OperationContext.Current.SetTransactionComplete(); 26 } 27 }
3. 事务的传播
在同一个应用程序域中,事务默认能相互传播,在上面的例子当中,当方法Method1()直接调用Mehtod2()的时候,事务也能够成功流转。
事务也能够在服务端与客户端之间传播,还能跨越服务边界,在多个系统当中流转,在WCF里把服务中的事务传播称作事务流(Transaction Flow)。如果事务流需要在服务端和客户端成功传播或使用分布式事务,必须具备以下条件:
- 绑定必须支持事务,在WCF内并非所有绑定都支持事务,像常用的BasicHttpBinding就不支持事务的传播。只有以下几个绑定才能支持事务流的运转:NetTcpBinding、WSHttpBinding、WSDualHttpBinding、WSFederationHttpBinding、NetNamedPipeBinding。
- 服务方法必须设置好TransactionScopeRequired与TransactionAutoComplete两个事务的基本元素,要成功启动事务,这是基础条件。
- 把TransactionFlow设置为true,这代表启动事务流,允许在SOAP头部放置事务的信息。一般情况下TransactionFlow的默认值为false ,这表示事务只能在服务器的同一应用程序域内流转,而不能实现服务端与客户端之间的传播。
- 把服务契约的TransactionFlowOption设置为Allowed,这代表允许客户端的事务传播到服务端。
- 客户端必须启动一个事务,在最后使用TransactionScope.Complete ( ) 提交事务。
TransactionFlowOption有三个选项:
一为NotAllowed,这代表了禁止客户端传播事务流到服务端,即使客户端启动了事务,该事务也会被忽略;
二为Allowed,这代表允许客户端的事务传播到服务端,但服务器端不一定会引用到此事务;
三为Mandatory,这代表服务端与客户端必须同时启动事务流,否则就会抛出InvalidOperationException异常。
下面举几个例子来讲解一下事务流的使用方式。
3.1 在服务端与客户端之间传播事务
这是事务流的基本使用方式,首先在服务端使用wsHttpBinding绑定建立一个服务契约,在方法中利用TransactionInformation对象检测一下事务的状态。然后设置好TransactionScopeRequired与TransactionAutoComplete属性来启动事务,在*.config中把TransactionFlow设置为true。再把服务的TransactionFlowOption设置为Allowed,最后在客户端通过TransactionScope.Complete()的方法提交事务。
服务端:
1 namespace Example 2 { 3 [ServiceContract] 4 public interface IExampleService 5 { 6 [OperationContract] 7 void Method1(); 8 } 9 10 public class ExampleService : IExampleService 11 { 12 //使用隐式事务,并把TransactionFlowOption设置为Allowed打开事务流 13 [OperationBehavior(TransactionAutoComplete=true,TransactionScopeRequired=true)] 14 [TransactionFlow(TransactionFlowOption.Allowed)] 15 public void Method1() 16 { 17 //通过TransactionInformation检测事务状态 18 Transaction transaction = Transaction.Current; 19 string info=string.Format(" DistributedIndentifier:{0} \n LocalIndentifier:{1}", 20 transaction.TransactionInformation.DistributedIdentifier, 21 transaction.TransactionInformation.LocalIdentifier); 22 Console.WriteLine("Method1: \n"+info); 23 } 24 25 static void Main(string[] args) 26 { 27 //启动服务 28 ServiceHost exampleHost = new ServiceHost(typeof(Example.ExampleService)); 29 30 exampleHost.Open(); 31 32 Console.WriteLine("service start"); 33 Console.ReadKey(); 34 Console.WriteLine("service end"); 35 36 exampleHost.Close(); 37 } 38 } 39 } 40 41 <configuration> 42 <system.serviceModel> 43 <bindings> 44 <wsHttpBinding> 45 <!--启动事务流--> 46 <binding name="defaultWSHttpBinding" transactionFlow="true" /> 47 </wsHttpBinding> 48 </bindings> 49 <behaviors> 50 <serviceBehaviors> 51 <behavior name=""> 52 <serviceMetadata httpGetEnabled="true" /> 53 <serviceDebug includeExceptionDetailInFaults="false" /> 54 </behavior> 55 </serviceBehaviors> 56 </behaviors> 57 <services> 58 <service name="Example.ExampleService"> 59 <!--使用支持事务流的wsHttpBinding绑定--> 60 <endpoint address="" binding="wsHttpBinding" bindingConfiguration="defaultWSHttpBinding" 61 contract="Example.IExampleService"> 62 <identity> 63 <dns value="localhost" /> 64 </identity> 65 </endpoint> 66 <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> 67 <host> 68 <baseAddresses> 69 <add baseAddress="http://localhost:7200/Example/ExampleService/" /> 70 </baseAddresses> 71 </host> 72 </service> 73 </services> 74 </system.serviceModel> 75 </configuration>
客户端:
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required)) 6 { 7 ShowTransactionMessage("start"); 8 9 ExampleServiceReference.ExampleServiceClient exampleService1 = new 10 ExampleServiceReference.ExampleServiceClient(); 11 exampleService1.Method1(); 12 ShowTransactionMessage("exampleService started"); 13 exampleService1.Close(); 14 //事务提交 15 scope.Complete(); 16 } 17 Console.ReadKey(); 18 } 19 20 //检查事务的状态 21 static void ShowTransactionMessage(string data) 22 { 23 if (Transaction.Current != null) 24 { 25 Transaction transaction = Transaction.Current; 26 string info = string.Format(" DistributedIndentifier:{0} \n LocalIndentifier:{1}", 27 transaction.TransactionInformation.DistributedIdentifier, 28 transaction.TransactionInformation.LocalIdentifier); 29 Console.WriteLine(data+" \n" + info); 30 } 31 } 32 }
图中上面代表服务端,下面代表客户端。可以看到,在客户端刚启动事务时,事务只一般的LTM轻量级事务,DistributedIndentifier为空值。当调用ExampleService服务后,事务的级别便提升为分布式事务。而服务端与客户端的事务正是通过DistributedIndentifier为标识的。
3.2使用分布式事务协调多个服务端的操作
在分布式系统当中,单个客户端可能引用多个服务,分布式事务能协调多方的操作。多个系统中的操作要不同时成功,要不同时失败。下面的例子中,客户端同时引用了ExampleService服务和ExtensionService服务,并启动了分布式事务。而在客户端与两个服务端的事务都是通过DistributedIndentifier 作为事务的标识的。
服务端:
1 //*******************************ExampleService*************************************************************************// 2 namespace Example 3 { 4 [ServiceContract] 5 public interface IExampleService 6 { 7 [OperationContract] 8 void Method1(); 9 } 10 11 public class ExampleService : IExampleService 12 { 13 //使用隐式事务,并把TransactionFlowOption设置为Allowed 14 [OperationBehavior(TransactionAutoComplete=true,TransactionScopeRequired=true)] 15 [TransactionFlow(TransactionFlowOption.Allowed)] 16 public void Method1() 17 { 18 //通过TransactionInformation检测事务状态 19 Transaction transaction = Transaction.Current; 20 string info=string.Format(" DistributedIndentifier:{0} \n LocalIndentifier:{1}", 21 transaction.TransactionInformation.DistributedIdentifier, 22 transaction.TransactionInformation.LocalIdentifier); 23 Console.WriteLine("Method1: \n"+info); 24 } 25 26 static void Main(string[] args) 27 { 28 //启动服务 29 ServiceHost exampleHost = new ServiceHost(typeof(Example.ExampleService)); 30 31 exampleHost.Open(); 32 33 Console.WriteLine("service start"); 34 Console.ReadKey(); 35 Console.WriteLine("service end"); 36 37 exampleHost.Close(); 38 } 39 } 40 } 41 42 <configuration> 43 <system.serviceModel> 44 <bindings> 45 <wsHttpBinding> 46 <!--启动事务流--> 47 <binding name="defaultWSHttpBinding" transactionFlow="true" /> 48 </wsHttpBinding> 49 </bindings> 50 <behaviors> 51 <serviceBehaviors> 52 <behavior name=""> 53 <serviceMetadata httpGetEnabled="true" /> 54 <serviceDebug includeExceptionDetailInFaults="false" /> 55 </behavior> 56 </serviceBehaviors> 57 </behaviors> 58 <services> 59 <service name="Example.ExampleService"> 60 <!--使用支持事务流的wsHttpBinding绑定--> 61 <endpoint address="" binding="wsHttpBinding" contract="Example.IExampleService" 62 bindingConfiguration="defaultWSHttpBinding" > 63 <identity> 64 <dns value="localhost" /> 65 </identity> 66 </endpoint> 67 <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> 68 <host> 69 <baseAddresses> 70 <add baseAddress="http://localhost:7200/Example/ExampleService/" /> 71 </baseAddresses> 72 </host> 73 </service> 74 </services> 75 </system.serviceModel> 76 </configuration> 77 78 //*************************************Extension**************************************************************// 79 namespace Extension 80 { 81 [ServiceContract] 82 public interface IExtensionService 83 { 84 [OperationContract] 85 void DoWork(); 86 } 87 88 public class ExtensionService : IExtensionService 89 { 90 [OperationBehavior(TransactionAutoComplete=true,TransactionScopeRequired=true)] 91 [TransactionFlow(TransactionFlowOption.Allowed)] 92 public void DoWork() 93 { 94 Transaction transaction = Transaction.Current; 95 string info = string.Format(" DistributedIndentifier:{0} \n LocalIndentifier:{1}", 96 transaction.TransactionInformation.DistributedIdentifier, 97 transaction.TransactionInformation.LocalIdentifier); 98 Console.WriteLine("DoWork: \n" + info); 99 } 100 101 static void Main(string[] args) 102 { 103 Console.WriteLine("extension service start"); 104 ServiceHost host = new ServiceHost(typeof(ExtensionService)); 105 host.Open(); 106 Console.ReadKey(); 107 host.Close(); 108 } 109 } 110 } 111 <configuration> 112 <!--略--> 113 ................... 114 </configuration>
客户端
1 namespace Test 2 { 3 class Program 4 { 5 static void Main(string[] args) 6 { 7 using (TransactionScope scope = new TransactionScope(TransactionScopeOption.RequiresNew)) 8 { 9 ShowTransactionMessage("start"); 10 11 ExampleServiceReference.ExampleServiceClient exampleService1 = new 12 ExampleServiceReference.ExampleServiceClient(); 13 exampleService1.Method1(); 14 ShowTransactionMessage("exampleService started"); 15 ExtensionServiceReference.ExtensionServiceClient extensionService = new 16 ExtensionServiceReference.ExtensionServiceClient(); 17 extensionService.DoWork(); 18 ShowTransactionMessage("extensionService started"); 19 20 exampleService1.Close(); 21 extensionService.Close(); 22 23 scope.Complete(); 24 } 25 26 Console.ReadKey(); 27 } 28 29 //检查事务的状态 30 static void ShowTransactionMessage(string data) 31 { 32 if (Transaction.Current != null) 33 { 34 Transaction transaction = Transaction.Current; 35 string info = string.Format(" DistributedIndentifier:{0} \n LocalIndentifier:{1}", 36 transaction.TransactionInformation.DistributedIdentifier, 37 transaction.TransactionInformation.LocalIdentifier); 38 Console.WriteLine(data+" \n" + info); 39 } 40 41 } 42 } 43 }
从测试结果可以看到在多个不同的服务端与客户端之间,都是通过DistributedIndentifier分布式事务ID来进行信息沟通的。一旦任何一出现问题,事务都会共同回滚,这对分布式开发是十分重要的。
值得注意的一点,事务必须由客户端提交,当客户端调用无事务状态时,两个服务端的事务则无法进行辨认,即其中一方出错,事务出现回滚,另一方也无法感知,这样容易引起逻辑性错误。试着把客户端代码改为 using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Suppress)){...},再运作一下,可以看到以下结果。事务都是由两个服务端分别管理,系统并无启动分布式事务。
应该注意:分布式事务会耗费较大的资源,在没必要的情况下,应该尽量使用LTM级量级事务管理器,而避免使用DTC分布式事务管理。
4.事务的的隔离性
事务的隔离性是通过TransactionIsolationLevel来定义的,它存在以下几个级别:
Unspecified | |
ReadUncommitted | 在读取数据时保持共享锁以避免读取已修改的数据,但在事务结束前这些数据可能已更改,因此会导致不可重复的读取和虚假数据。 |
ReadCommitted | 发出共享锁定并允许非独占方式的锁定。 |
RepeatableRead | 在查询中使用的所有数据上放置锁,以防止其他用户更新这些数据。这防止了不可重复的读取,但仍有可能产生虚假行。 |
Serializable | 默认级别,也是最高级别。表示事务完成前禁止外界更新数据 |
Chaos | 不使用隔离 |
Snapshot |
需要注意服务端与客户端必须使用同一级别的隔离模式,否则系统将会抛出FaultException异常。
服务类必须在至少一个方法开启了事务后才可以设置隔离模式
1 public interface IService 2 { 3 [OperationContract] 4 void Method(); 5 } 6 7 [ServiceBehavior(TransasctionIsolationLevel=IsolationLevel.ReadCommitted)] 8 public class Service : IService 9 { 10 //隐式事务 11 [OperationBehavior(TransactionScopeRequired=true,TransactionAutoComplete=true)] 12 public void Method() 13 {..........} 14 }
5.事务的超时
当事务实现隔离时,资源将会被锁定,如果一些事务长期占有资源,那将容易造成死锁,为了避免这个问题,事务有一个超时限制,这个超时默认值为60s。如果事务超过此时间,即使没有发生异常,也会自动中止。
超时时候可以通过特性设置,也可使用*.config文件设置。下面的两段代码有着相同的效果,就是把超时时间设置为10s。
1 [ServiceBehavior(TransactionTimeout="00:00:10")] 2 public class Service:IService 3 {......} 4 5 <configuration> 6 ........ 7 <system.serviceModel> 8 ........ 9 <services> 10 <service name="MyService" behaviorConfiguration="myBehavior"> 11 ...... 12 </service> 13 </services> 14 <behaviors> 15 <serviceBehaviors> 16 <behavior name="myBehavior" transactionTimeout="00:00:10"/> 17 </serviceBehaviors> 18 </behaviors> 19 </system.serviceModel> 20 </configuration>
六、嵌套式事务
嵌套式事务经常会出现在项目中,但往往容易被大家忽略,下面介绍一下 嵌套式事务的用法:
1 using (TransactionScope scope1 = new TransactionScope()) 2 { 3 .............. 4 using (TransactionScope scope2=new TransactionScope(TransactionScopeOption.RequiresNew)) 5 { 6 .............. 7 scope2.Complete(); //只完成嵌套式的内部事务,但事务并未正式提交 8 } 9 scope1.Complete(); //代表完成所有事务,事务正式提交 10 }
一般项目中,大家都只会把事务用在DAL层,用于管理数据的CRUD,但其实在一些操作中,某些数据的操作必须具有一致性。比如在订单管理中,当插入一条OrderItem时,Order表内的总体价格,商品数量等也会随之改变。很多人把两个表的操作合成一个方法,放在OrderDAL中完成。但其实这样做违返设计的原则,因为计算Order的总体价格时可能会包含商品优惠、客户等级、客户积分等等业务逻辑,而在DAL层不应该包含任何的业务逻辑存在的,所以这样操作应该放在业务层完成。这时候,业务层的方法内就需要同时调用OrderItemDAL的AddOrderItem(OrderItem) 方法和OrderDAL的UpdateOrder(Order)方法,为了保证数据的一致性更新,就需要使用嵌套式事务。但这往往容易被开发人员所忽略,当Order表的更新成功而OrderItem表的插入失败时,系统不能保证数据的同步回滚,那就会造成数据的逻辑性错误。
下面的例子就是为了保证数据一致性更新而使用的嵌套式事务,在使用嵌套式事务的时候要应该注意及其把对象释放,避免做成死锁。
1 public class OrderDAL 2 { 3 public void UpdateOrder(Order order) 4 { 5 using (TransactionScope scope = new TransactionScope()) 6 { 7 ...... 8 scope.Complete(); 9 } 10 } 11 } 12 13 public class OrderItemDAL 14 { 15 public void AddOrderItem(OrderItem orderItem) 16 { 17 using (TransactionScope scope = new TransactionScope()) 18 { 19 ...... 20 scope.Complete(); 21 } 22 } 23 } 24 25 public class OrderManager 26 { 27 public void AddOrderItem(OrderItem item) 28 { 29 using (TransactionScope scope = new TransactionScope()) 30 { 31 OrderItemDAL orderItemDAL=new OrderItemDAL(); 32 orderItemDAL.AddOrderItem(item); 33 OrderDAL orderDAL=new OrderDAL(); 34 ........ 35 orderDAL.UpdateOrder(order); 36 scope.Complete(); 37 } 38 } 39 }
七、异步事务
记得在第二节的时候曾经提起过事务类Transaction的方法中包含方法
public DependentTransaction DependentClone(DependentCloneOption)
此方法作用是克隆当前的事务,它在多线程调用同一事务的情况下使用经常使用。其中DependentCloneOption包含有两个选项:
一为BlockCommitUntilComplete,这表示在依赖事务未完成前,事务将处于阻塞状态,只有在所有依赖事务完成后,事务才能执行提交;
二为RollbackInNotComplete,这表示依赖事务必须在事务完成前调用Complete(),否则事务会被视为失败。
在普通情况下,事务都会通过Transaction.Current 来获取,但此方法只能获取当前线程下的事务对象,在异步方法当中,这只会返回一个空值 null 。此时就需要使用DependentClone 方法获取依赖事务对象 DependentTransaction ,再把此对象作用异步参数传递到回调方法中。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Method(); 6 Console.ReadKey(); 7 } 8 9 static void Method() 10 { 11 using (TransactionScope scope = new TransactionScope()) 12 { 13 ShowMessage("Main Thread"); 14 15 //获取一个依赖事务,把依赖事务作为回调参数传到回调函数中 16 DependentTransaction dependentTransaction= 17 Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete); 18 ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncThread), dependentTransaction); 19 ........ 20 scope.Complete(); //完成主线程事务,在依赖事务完成前,事务提交将处于阻塞状态 21 } 22 } 23 24 static void AsyncThread(object transaction) 25 { 26 //获取依赖事务,利用TransactionScope(Transaction)构造函数生成隐式事务 27 DependentTransaction dependentTransaction = (DependentTransaction)transaction; 28 using (TransactionScope scope = new TransactionScope(dependentTransaction)) 29 { 30 ShowMessage("AsyncThread"); 31 .......... 32 scope.Complete(); //完成异步事务 33 } 34 //完成依赖事务 35 dependentTransaction.Complete(); 36 } 37 38 static void ShowMessage(string data) 39 { 40 if (Transaction.Current != null) 41 { 42 Transaction transaction = Transaction.Current; 43 string info = string.Format("{0}:{1}\nTransaction:\n DistributedIndentifier:{2} \n LocalIndentifier:{3}\n", 44 data,Thread.CurrentThread.ManagedThreadId.ToString(), 45 transaction.TransactionInformation.DistributedIdentifier, 46 transaction.TransactionInformation.LocalIdentifier); 47 Console.WriteLine(info); 48 } 49 } 50 }
首先在主线程中利用Transaction.DependentClone(DependentCloneOption.BlockCommitUntilComplete) 方法生成一个依赖事务,注意方法使用了BlockCommitUntilComplete的方式生成,即事务将在所有依赖事务使用Complete()后才能执行提交。
然后利用ThreadPool.QueueUserWorkItem(WaitCallback,Object)方法把依赖事务作为回调参数传到回调函数中。
最后在回调函数中使用TransactionScope(Transaction)构造函数生成函数事务,这代表把参数Transaction作用当前的环境事务对象。观察下面的运行结果,两个线程中的事务都是同一个事务。
结束语
事务是在多个层次都会使用到的,但很多项目当中往往会忽略了这一点而只在数据层使用,在大型的系统当中这样可能会影响到系统的一致性。特别是在分布式系统当中,操作往往同时存在于多个不同的系统当中,事务的处理更显示出其重要性。