谈谈分布式事务之三: System.Transactions事务详解[上篇]

在.NET 1.x中,我们基本是通过ADO.NET实现对不同数据库访问的事务。.NET 2.0为了带来了全新的事务编程模式,由于所有事务组件或者类型均定义在System.Transactions程序集中的System.Transactions命名空间下,我们直接称基于此的事务为System.Transactions事务。System.Transactions事务编程模型使我们可以显式(通过System.Transactions.Transaction)或者隐式(基于System.Transactions.TransactionScope)的方式进行事务编程。我们先来看看,这种全新的事务如何表示。

一、System.Transactions.Transaction

在System.Transactions事务体系下,事务本身通过类型System.Transactions.Transaction类型表示,下面是Transaction的定义:

   1: [Serializable]
   2: public class Transaction : IDisposable, ISerializable
   3: {   
   4:     public event TransactionCompletedEventHandler TransactionCompleted;
   5:     
   6:     public Transaction Clone();
   7:     public DependentTransaction DependentClone(DependentCloneOption cloneOption);
   8:     
   9:     public Enlistment EnlistDurable(Guid resourceManagerIdentifier, IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions);
  10:     public Enlistment EnlistDurable(Guid resourceManagerIdentifier, ISinglePhaseNotification singlePhaseNotification, EnlistmentOptions enlistmentOptions);    
  11:     public bool EnlistPromotableSinglePhase(IPromotableSinglePhaseNotification promotableSinglePhaseNotification);  
  12:     public Enlistment EnlistVolatile(IEnlistmentNotification enlistmentNotification, EnlistmentOptions enlistmentOptions);
  13:     public Enlistment EnlistVolatile(ISinglePhaseNotification singlePhaseNotification, EnlistmentOptions enlistmentOptions);
  14:  
  15:     public void Rollback();
  16:     public void Rollback(Exception e);
  17:  
  18:     void ISerializable.GetObjectData(SerializationInfo serializationInfo, StreamingContext context);
  19:  
  20:     public static Transaction Current { get; set; }   
  21:  
  22:     public IsolationLevel IsolationLevel { get; }
  23:     public TransactionInformation TransactionInformation { get; }
  24: }

1、Transaction是可序列化的

从上面的定义我们可以看到,Transaction类型(在没有特殊说明的情况下,以下的Transaction类型指的就是System.Transactions.Transaction)上面应用的SerializableAttribute特性,并且实现了ISerializable接口,意味着一个Transaction对象是可以被序列化的。Transaction的这一特性在WCF整个分布式事务的实现意义重大,原因很简单:要让事务能够控制整个服务操作,必须实现事务的传播,而传播的前提就是事务可被序列化

2、如何登记事务参与者

Transaction中, 定义了五个EnlistXxx方法用于将涉及到的资源管理器登记到当前事务中。其中EnlistDurable和EnlistVolatile分别实现了 对持久化资源管理器和易失资源管管理器的事务登记,而EnlistPromotableSinglePhase则针对的是可被提升的资源管理器(比如基于 SQL Server 2005和SQL Server 2008)。

事务登记的目的是建立事务提交树,使得处于根节点的事务管理器能够在事务提交的时候能够沿着这棵树将相应的通知发送给所有的事务参与者。这种至上而 下的通知机制依赖于具体采用事务提交协议,或者说某个资源要求参与到当前事务之中,必须满足基于协议需要的接收和处理相应通知的能力。 System.Transactions将不同事务提交协议对参与者的要求定义在相应的接口中。其中IEnlistmentNotificationISinglePhaseNotification分别是基于2PC和SPC(关于2PC和SPC,在上篇中有详细的介绍)。

如果我们需要为相应的资源开发能够参与到System.Transactions事务的资源管理器,需要事先实现IEnlistmentNotification接口,对基本的2PC协议提供支持。当满足SPC要求的时候,如果希望采用SPC优化协议,则需要实现ISinglePhaseNotification接口。如果希望像SQL Server 2005或者SQL Server 2008支持事务提升机制,则需要实现IPromotableSinglePhaseNotification接口。

3、环境事务(Ambient Transaction)

Transaction定义了一个类型为Transaction的Current静态属性(可读可写),表示当前的事务。作为当前事务的Transaction存储于当前线程的TLS(Thread Local Storage)中(实际上是定义在一个应用了ThreadStaticAttribute特性的静态字段上),所以仅对当前线程有效。如果进行异步调用,当前事务并不能自动事先跨线程传播,将异步操作纳入到当前事务,需要使用到另外一个事务:依赖事务。

这种基于当前线程的当前事务又称环境事务(Ambient Transaction),很多资源管理器都具有对 环境事务的感知能力。也就是说,如果我们通过Current属性设置了环境事务,当对某个具有环境事务感知能力的资源管理器进行访问的时候,相应的资源管 理器会自动登记到当前事务中来。我们将具有这种感知能力的资源管理器称为System.Transactions资源管理器。

4、事务标识

Transaction具有一个只读的TransactionInformation属性,表示事务一些基本的信息。属性的类型为TransactionInformation,定义如下:

   1: public class TransactionInformation
   2: {  
   3:     public DateTime CreationTime { get; }    
   4:     public TransactionStatus Status { get; }
   5:  
   6:     public string LocalIdentifier { get; }
   7:     public Guid DistributedIdentifier { get; }
   8: }

TransactionInformation的CreationTime和Status表示创建事务的时间和事务的当前状态。事务具有活动(Active)、提交(Committed)、中止(Aborted)和未决(In-Doubt)四种状态,通过TransactionStatus枚举表示。

   1: public enum TransactionStatus
   2: {
   3:     Active,
   4:     Committed,
   5:     Aborted,
   6:     InDoubt
   7: }

事务具有两个标识符,一个是本地标识,另一个是分布式标识,分别通过TransactionInformation的只读属性 LocalIdentifier和DistributedIdentifier表示。本地标识由两部分组成:标识为本地应用程序域分配的轻量级事务管理器 (LTM)的GUID和一个递增的整数(表示当前LMT管理的事务序号)。在下面的代码中,我们分别打印出三个新创建的可提交事务(CommittableTransaction,为Transaction的子类,我们后面会详细介绍)的本地标识。

   1: using System;
   2: using System.Transactions;
   3: class Proggram
   4: {
   5:     static void Main()
   6:     {
   7:         Console.WriteLine(new CommittableTransaction().TransactionInformation.LocalIdentifier);
   8:         Console.WriteLine(new CommittableTransaction().TransactionInformation.LocalIdentifier);
   9:         Console.WriteLine(new CommittableTransaction().TransactionInformation.LocalIdentifier);
  10:     }
  11: }

输出结果:

AC48F192-4410-45fe-AFDC-8A890A3F5634:1
AC48F192-4410-45fe-AFDC-8A890A3F5634:2
AC48F192-4410-45fe-AFDC-8A890A3F5634:3

一旦本地事务提升到基于DTC的分布式事务,系统会为之生成一个GUID作为其唯一标识。当事务跨边界执行的时候,分布式事务标识会随着事务一并被 传播,所以在不同的执行上下文中,你会得到相同的GUID。分布式事务标识通过TransactionInformation的只读属性 DistributedIdentifier表示,我经常在审核(Audit)中使用该标识。

对于上面Transaction的介绍,细心的读者可能会发现两个问题:Transaction并没有提供公有的构造函数,意味着我们不能直接通过 new操作符创建Transaction对象;Transaction只有两个重载的Rollback方法,并没有Commit方法,意味着我们直接通过 Transaction进行事务提交。

在一个分布式事务中,事务初始化和提交只能有相同的参与者担当。也就是说只有被最初开始的事务才能被提交,我们将这种能被初始化和提交的事务称作可 提交事务(Committable Transaction)。随着分布式事务参与者逐个登记到事务之中,它们本地的事务实际上依赖着这个最初开始的事务,所以我们称这种事务为依赖事务 (Dependent Transaction)。

二、 可提交事务(CommittableTransaction)

只有可提交事务才能被直接初始化,对可提交事务的提交驱动着对整个分布式事务的提交。可提交事务通过CommittableTransaction类型表示。照例先来看看CommittableTransaction的定义:

   1: [Serializable]
   2: public sealed class CommittableTransaction : Transaction, IAsyncResult
   3: {   
   4:     public CommittableTransaction();
   5:     public CommittableTransaction(TimeSpan timeout);
   6:     public CommittableTransaction(TransactionOptions options);
   7:  
   8:     public void Commit();
   9:     public IAsyncResult BeginCommit(AsyncCallback asyncCallback, object asyncState);
  10:     public void EndCommit(IAsyncResult asyncResult);
  11:    
  12:     object IAsyncResult.AsyncState { get; }
  13:     WaitHandle IAsyncResult.AsyncWaitHandle { get; }
  14:     bool IAsyncResult.CompletedSynchronously { get; }
  15:     bool IAsyncResult.IsCompleted { get; }
  16: }

1、可提交事务的超时时限和隔离级别

CommittableTransaction直接继承自Transaction,提供了三个公有的构造函数。通过TimeSpan类型的timeout参数指定事务的超时实现,自被初始化那一刻开始算起,一旦超过了该时限,事务会被中止。通过TransactionOptions类型的options可以同时指定事务的超时时限和隔离级别。TransactionOptions是一个定义在System.Transactions命名空间下的结构(Struct),定义如下,两个属性Timeout和IsolationLevel分别代表事务的超时时限和隔离级别。

   1: [StructLayout(LayoutKind.Sequential)]
   2: public struct TransactionOptions
   3: {
   4:     //其他成员
   5:     public TimeSpan Timeout { get; set; }
   6:     public IsolationLevel IsolationLevel { get; set; }
   7: }

如果调用默认无参的构造函数来创建CommittableTransaction对 象,意味着采用一个默认的超时时限。这个默认的时间是1分钟,不过可以它可以通过配置的方式进行指定。事务超时时限相关的参数定义 在<system.transactions>配置节中,下面的XML体现的是默认的配置。从该段配置我们可以看到,我们不但可以通 过<defaultSettings>设置事务默认的超时时限,还可以通过<machineSettings>设置最高可被允许 的事务超时时限,默认为10分钟。在对这两项进行配置的时候,前者的时间必须小于后者,否则将用后者作为事务默认的超时时限。

   1: <?xml version="1.0" encoding="utf-8" ?>
   2: <configuration>
   3:   <system.transactions>
   4:     <defaultSettings timeout="00:01:00"/>
   5:     <machineSettings maxTimeout="00:10:00"/>
   6:   </system.transactions>
   7: </configuration>

作为事务ACID四大属性之一的隔离性(Isolation),确保事务操作的中间状态的可见性仅限于事务内部。隔离机制通过对访问的数据进行加 锁,防止数据被事务的外部程序操作,从而确保了数据的一致性。但是隔离机制在另一方面又约束了对数据的并发操作,降低数据操作的整体性能。为了权衡着两个 互相矛盾的两个方面,我们可以根据具体的情况选择相应的隔离级别。

在System.Transactions事务体系中,为事务提供了7种不同的隔离级别。这7中隔离级别分别通过System.Transactions.IsolationLevel的7个枚举项表示。

   1: public enum IsolationLevel
   2: {
   3:     Serializable,
   4:     RepeatableRead,
   5:     ReadCommitted,
   6:     ReadUncommitted,
   7:     Snapshot,
   8:     Chaos,
   9:     Unspecified
  10: }

7个隔离级别之中,Serializable具有最高隔离级别,代表的是一种完全基于序列化(同步)的数据存取方式,这也是System.Transactions事务默认采用的隔离级别。按照隔离级别至高向低,7个不同的隔离级别代表的含义如下:

  • Serializable:可以在事务期间读取可变数据,但是不可以修改,也不可以添加任何新数据;
  • RepeatableRead:可以在事务期间读取可变数据,但是不可以修改。可以在事务期间添加新数据;
  • ReadCommitted:不可以在事务期间读取可变数据,但是可以修改它;
  • ReadUncommitted:可以在事务期间读取和修改可变数据;
  • Snapshot:可以读取可变数据。在事务修改数据之前,它验证在它最初读取数据之后另一个事务是否更改过这些数据。如果数据已被更新,则会引发错误。这样使事务可获取先前提交的数据值;
  • Chaos:无法覆盖隔离级别更高的事务中的挂起的更改;
  • Unspecified:正在使用与指定隔离级别不同的隔离级别,但是无法确定该级别。如果设置了此值,则会引发异常。

2、事务的提交

CommittableTransaction提供了同步(通过Commit方法)和异步(通过BeginCommit|EndCommit方法组合)对事务的提交。此外CommittableTransaction还是实现了IAsyncResult这么一个接口,如果采用异步的方式调用BeginCommit方法提交事务,方法返回的IAsyncResult对象的各属性值会反映在CommittableTransaction同名属性上面。

前面我们提到了环境事务已经System.Transactions资源管理器对环境事务的自动感知能力。当创建了CommittableTransaction对象的时候,被创建的事务并不会自动作为环境事务,你需要手工将其指定到Transaction的静态Current属性中。接下来,我们将通过一个简单的例子演示如果通过CommittableTransaction实现一个分布式事务。

3、实例演示:通过CommittableTransaction实现分布式事务

在这个实例演示中,我们沿用介绍事务显式控制时使用到的银行转帐的场景,并且直接使用第一篇中创建的帐户表(T_ACCOUNT)。一个完整的转帐操作本质上有两个子操作完成,提取和存储,即从一个帐户中提取相应的金额存入另一个帐户。为了完成这两个操作,我写了如下两个存储过程:P_WITHDRAW和P_DEPOSIT。

P_WITHDRAW:

   1: CREATE Procedure P_WITHDRAW
   2:     (
   3:         @id        VARCHAR(50),
   4:         @amount FLOAT
   5:     )
   6: AS
   7: IF NOT EXISTS(SELECT * FROM [dbo].[T_ACCOUNT] WHERE ID = @id)
   8:     BEGIN
   9:         RAISERROR (‘帐户ID不存在‘,16,1)
  10:         RETURN
  11:     END    
  12: IF NOT EXISTS(SELECT * FROM [dbo].[T_ACCOUNT] WHERE ID = @id AND BALANCE > @amount)
  13:     BEGIN
  14:         RAISERROR (‘余额不足‘,16,1)
  15:         RETURN
  16:     END
  17:     
  18: UPDATE     [dbo].[T_ACCOUNT] SET Balance = Balance - @amount WHERE Id = @id
  19: GO

P_DEPOSIT:

   1: CREATE Procedure P_DEPOSIT
   2:     (
   3:         @id        VARCHAR(50),
   4:         @amount FLOAT
   5:     )
   6: AS
   7: IF NOT EXISTS(SELECT * FROM [dbo].[T_ACCOUNT] WHERE Id = @id)
   8:     BEGIN
   9:         RAISERROR (‘帐户ID不存在‘,16,1)
  10:     END
  11: UPDATE     [dbo].[T_ACCOUNT] SET Balance = Balance + @amount WHERE Id = @id
  12: GO

为了确定是否成功转帐,我们需要提取相应帐户的当前余额,我们相应操作实现在下面一个存储过程中。

   1: CREATE Procedure P_GET_BALANCE_BY_ID
   2:     (
   3:         @id VARCHAR(50)
   4:     )
   5: AS
   6: IF NOT EXISTS(SELECT * FROM [dbo].[T_ACCOUNT] WHERE Id = @id)
   7:     BEGIN
   8:         RAISERROR (‘帐户ID不存在‘,16,1)
   9:     END
  10: SELECT BALANCE FROM [dbo].[T_ACCOUNT] WHERE Id = @id
  11: GO

为了执行存储过程的方便,我写了一个简单的工具类DbAccessUtil。ExecuteNonQuery和ExecuteScalar的作用于 DbCommand同名方法相同。使用DbAccessUtil的这两个方法,只需要以字符串和字典的方式传入存储过程名称和参数即可。由于篇幅所限,关 于具有实现不再多做介绍了,又兴趣的读者,可以参考《WCF技术剖析(卷1)》的最后一章,里面的DbHelper提供了相似的实现。

   1: public static class DbAccessUtil
   2: {
   3:     public static int ExecuteNonQuery(string procedureName, IDictionary<string, object> parameters);
   4:     public static T ExecuteScalar<T>(string procedureName, IDictionary<string, object> parameters);
   5: }

借助于DbAccessUtil提供的辅助方法,我们定义两个方法Withdraw和Deposit分别实现提取和存储的操作,已近获取某个帐户当前余额。

   1: static void Withdraw(string accountId, double amount)
   2: {
   3:     Dictionary<string, object> parameters = new Dictionary<string, object>();
   4:     parameters.Add("id", accountId);
   5:     parameters.Add("amount", amount);
   6:     DbAccessUtil.ExecuteNonQuery("P_DEPOSIT", parameters);
   7: } 
   8: static void Deposite(string accountId, double amount)
   9: {
  10:     Dictionary<string, object> parameters = new Dictionary<string, object>();
  11:     parameters.Add("id", accountId);
  12:     parameters.Add("amount", amount);
  13:     DbAccessUtil.ExecuteNonQuery("P_DEPOSIT", parameters);
  14: }
  15: private static double GetBalance(string accountId)
  16: {
  17:     Dictionary<string, object> parameters = new Dictionary<string, object>();
  18:     parameters.Add("id", accountId);
  19:     return DbAccessUtil.ExecuteScalar<double>("P_GET_BALANCE_BY_ID", parameters);
  20: }

现在假设帐户表中有一个帐号,它们的ID分别为Foo,余额为5000。下面是没有采用事务机制的转帐实现(注意:需要转入的帐户不存在)。

   1: using System;
   2: using System.Collections.Generic;
   3: namespace Artech.TransactionDemo
   4: {
   5:     class Program
   6:     {
   7:         static void Main(string[] args)
   8:         {
   9:             string accountFoo = "Foo";
  10:             string nonExistentAccount = Guid.NewGuid().ToString();            
  11:             //输出转帐之前的余额
  12:             Console.WriteLine("帐户\"{0}\"的当前余额为:¥{1}", accountFoo, GetBalance(accountFoo));
  13:             //开始转帐    
  14:             try
  15:             {
  16:                 Transfer(accountFoo, nonExistentAccount, 1000);
  17:             }
  18:             catch (Exception ex)
  19:             {
  20:                 Console.WriteLine("转帐失败,错误信息:{0}", ex.Message);
  21:             }
  22:             //输出转帐后的余额
  23:             Console.WriteLine("帐户\"{0}\"的当前余额为:¥{1}", accountFoo, GetBalance(accountFoo));
  24:         }
  25:  
  26:         private static void Transfer(string accountFrom, string accountTo, double amount)
  27:         {
  28:             Withdraw(accountFrom, amount);
  29:             Deposite(accountTo, amount);
  30:         }
  31:     }
  32: }

输出结果:

帐户"Foo"的当前余额为:¥5000
转帐失败,错误信息:帐户ID不存在
帐户"Foo"的当前余额为:¥4000

由于没有采用事务,在转入帐户根本不存在情况下,款项依然被转出帐户提取出来。现在我们通过CommittableTransaction将整个转帐操作纳入同一个事务中,只需要将Transfer方法进行如下的改写:

   1: private static void Transfer(string accountFrom, string accountTo, double amount)
   2: {
   3:     Transaction originalTransaction = Transaction.Current;
   4:     CommittableTransaction transaction = new CommittableTransaction();
   5:     try
   6:     {
   7:         Transaction.Current = transaction;
   8:         Withdraw(accountFrom, amount);
   9:         Deposite(accountTo, amount);
  10:         transaction.Commit();
  11:     }
  12:     catch (Exception ex)
  13:     {
  14:         transaction.Rollback(ex);
  15:         throw;
  16:     }
  17:     finally
  18:     {
  19:         Transaction.Current = originalTransaction;
  20:         transaction.Dispose();
  21:     }
  22: }

输出结果(将余额恢复成5000):

帐户"Foo"的当前余额为:¥5000
转帐失败,错误信息:帐户ID不存在
帐户"Foo"的当前余额为:¥5000

下一篇中我们将重点介绍DependentTransactionTransactionScope

时间: 2024-10-26 19:44:22

谈谈分布式事务之三: System.Transactions事务详解[上篇]的相关文章

谈谈分布式事务之三: System.Transactions事务详解[下篇]

在前面一篇给出的Transaction的定义中,信息的读者应该看到了一个叫做DepedentClone的方法.该方法对用于创建基于现有Transaction对 象的“依赖事务(DependentTransaction)”.不像可提交事务是一个独立的事务对象,依赖事务依附于现有的某个事务(可能是可提交事 务,也可能是依赖事务).依赖事务可以帮助我们很容易地编写一些事务型操作,当环境事务不存的时候,可以确保操作在一个独立的事务中执行:当环境事务存在 的时候,则自动加入其中. 一.依赖事务(Depen

C语言中的system函数参数详解

http://blog.csdn.net/pipisorry/article/details/33024727 函数名: system 功   能: 发出一个DOS命令 用   法: int system(char *command); system函数已经被收录在标准c库中,可以直接调用 system()函数用于向操作系统传递控制台命令行,以WINDOWS系统为例,通过system()函数执行命令和在DOS窗口中执行命令的效果是一样的,所以只要在运行窗口中可以使用的命令都可以用SYSTEM()

java中System.getProperty()方法详解

java中System.getProperty()方法详解,如下: System.out.println("java版本号:" + System.getProperty("java.version")); // java版本号 System.out.println("Java提供商名称:" + System.getProperty("java.vendor")); // Java提供商名称 System.out.println

System.Transactions 事务超时属性

System.Transactions 有2个超时属性(timeout 与 maxTimeout),可以通过配置文件来进行设置. 1. timeout System.Transactions 默认的timeout值为1分钟,可以通过app.config/web.config/machine.config来进行设置(对于应用中具体的事务还可以通过调用具体对象TransactionScope或CommittableTransaction的构造函数进行设置).以下配置样例代码将其设置为30秒: <co

System.Transactions事务超时设置

System.Transactions 有2个超时属性(timeout 与 maxTimeout),可以通过配置文件来进行设置. 1. timeout System.Transactions 默认的timeout值为1分钟,可以通过app.config/web.config/machine.config来进行设置(对于应用中具体的事务还可以通过调用具体对象TransactionScope或CommittableTransaction的构造函数进行设置).以下配置样例代码将其设置为30秒: <co

OSPF详解之三:OSPF LSA详解

OSPF LSA详解 OSPF V2版本中常用的主要有6类LSA,分别是Router-LSA.Network-LSA.Network-summary-LSA.ASBR-summary-LSA.AS-External-LSA.NSSA-LSA,接下来我将一步一步为大家解析. Type 1:Router-LSA 每个设备都会产生,描述了设备的链路状态和开销,在所属的区域内传播. 谈到1类LSA,大家必然会想到它的链路类型,链路类型分为4类:P2P.Stub.Transit.Vritual link.

《招一个靠谱的移动开发》iOS面试题及详解(上篇)

多线程.特别是NSOperation 和 GCD 的内部原理. 运行时机制的原理和运用场景. SDWebImage的原理.实现机制.如何解决TableView卡的问题. block和代理的,通知的区别.block的用法需要注意些什么. strong,weak,retain,assign,copy nomatic 等的区别. 设计模式,mvc,单利,工厂,代理等的应用场景. 单利的写法.在单利中创建数组应该注意些什么. NSString 的时候用copy和strong的区别. 响应值链. NSTi

RocketMQ事务消费和顺序消费详解

一.RocketMq有3中消息类型 1.普通消费 2. 顺序消费 3.事务消费 顺序消费场景 在网购的时候,我们需要下单,那么下单需要假如有三个顺序,第一.创建订单 ,第二:订单付款,第三:订单完成.也就是这个三个环节要有顺序,这个订单才有意义.RocketMQ可以保证顺序消费. rocketMq实现顺序消费的原理 produce在发送消息的时候,把消息发到同一个队列(queue)中,消费者注册消息监听器为MessageListenerOrderly,这样就可以保证消费端只有一个线程去消费消息

Spring事务管理--(二)嵌套事物详解

一.前言 最近开发程序的时候,出现数据库自增id跳数字情况,无奈之下dba遍查操作日志,没有delete记录.才开始慢慢来查询事物问题.多久以来欠下的账,今天该还给spring事物. 希望大家有所收获.2016年07月19日22:32:38 二.spring嵌套事物 1.展示项目代码--简单测springboot项目 整体项目就这么简单,为了方便.这里就只有biz层与service层,主要作为两层嵌套,大家只要看看大概就ok.后面会给出git项目地址,下载下来看一看就明白,力求最简单. 下面我们