构建面向对象的应用软件系统框架 第8章
第8章 事务处理
8.1 事务的基本概念
事务处理在应用系统开发中起着重要的作用。简单地来说,事务就是由若干步处理组成的工作单元,这些步骤之间具有一定的逻辑关系,作为一个整体的操作过程,每个步骤必须同时成功或者失败。当所有的步骤都成功的时候,事务就必须“提交”,而当其中有一个步骤失败的时候,整个事务都必须“回滚”,也就是,其中的每一个步骤所做的改动都必须撤销。
举一个例子来说明事务处理的情形。我们考察一个仓库入库的过程。仓库的入库要经过以下步骤:
Ø 填写一张入库单,这张入库单上可能包含多种商品,每一种商品入库一定的数量
Ø 根据入库单上面的商品的数量,需要修改被入库的商品的现有库存量
Ø 保存这张入库单。
上面的操作都必须完整地执行,也就是说,必须在一个“事务”里面完成,否则,就可能出现下面的情形:
Ø 商品1在仓库里面原来有100件,这个时候,有一张入库单准备入库50件商品1
Ø 系统根据入库单的信息,修改商品1的库存量,这个时候商品1的库存量便为150件
Ø 在保存入库单的时候,由于网络故障,入库单保存失败了!
此时的系统中,商品1的库存量已经改变了,已经变成了150件,而实际上,没有任何商品入库!所以,在这种情况下,系统的数据就出现了不一致性。显然,这种情况是不允许发生的。事务处理提供了防止发生这种情况的的方法。
事务的主要特性包括原子性、一致性、隔离性和永久性,即所谓的ACID属性。
原子性
原子性表示事务作为一个工作单元处理。事务可能包括若干个步骤,通过把这些步骤作为一个工作单元处理,并在进程周围放上逻辑边界,就可以要求每一个步骤都完全成功,否则,就不能进行下一步的操作。
每一格步骤都依赖于前一步的顺利完成。如果一步失败,则不能完成任何其余的步骤,前面已经完成的步骤也必须全部回滚。如前面的例子,如果在保存入库单的时候出现问题,则所有的对商品现有库存量的修改,都必须恢复到初始的状态。
一致性
一致性将保证事务使系统或数据处于一致状态。数据处于一致状态就是符合数据库的限制或规则。限制就是事务完成时要成立的数据库条件,已在定义数据库结构时被定义,用以指定主键、数字字段有效范围和可否包含null值之类的事项。
如果事务开始时系统处于一致状态,则事务结束时系统ye应处于一致状态,不管事务成功还是失败,提交还是撤销。
一致性是由同一组业务规则或完整性限制确定的。为了保证一致性,需要事务管理者和应用程序开发人员共同努力。
事务管理者应保证事务的原子性、隔离性和耐久性,应用程序开发人员应保证事务的一致性,指定主键、引用完整性限制和其他条件。当准备提交事务的时候,数据库根据这些规则验证其一致性。如果发现事务结果与系统确定的规则一致,则事务提交。如果结果不符合要求,则事务撤销。
一致性保证了事务所做的任何改变不会使系统处于无效状态。这样能使事务更加可靠。如果事务将钱从一个帐户转到另一个帐户,则只要系统中的总金额保持相同,事务就是一致的。
隔离性
事务的隔离性提供了事务处理中最强大的特性之一。简单地说,隔离性能保证事务访问的任何数据不会受其他事务所做的任何改变的影响,直到第一个事务完成。
这等于让事务象系统中的唯一事务一样执行。其他请求的数据库操作只能在不破坏当前使用的数据时才能进行。这对于支持数据的并发访问至关重要。
由于并发系统中很可能出现错误或搞乱数据的情况,因此保证隔离性和管理并发执行的责任不能由开发人员完成,而应该由系统来提供一个事务管理器。通常的数据库都支持事务管理,另外还有一些独立的事务管理服务器,如COM+、J2EE中的JTA等。
事务可以采用几种不同的隔离级,使用哪种隔离级取决于几种不同的因素。
永久性
永久性指定了事务提交时,对数据所做的任何改变都要记录到永久存储器中。这通常通过事务日志来实现。
事务日志将数据存放在数据库之类的资源中,可以在遇到故障时 重新采用丢失的事务,也可以在遇到错误时撤销事务。简单的说,事务日志负责跟踪数据库中发生的每个数据操作,使数据可以返回到搞乱前的已知状态。一旦系统恢复到已知状态后,便可以利用事务日至更新构造或重新采用从这个状态开始的改变。
永久性在提交的数据改变是个关联合约时也很重要。事务提交表示数据源和应用程序之间的协议。事务日志就是这个协议的书面记录。更为重要的是,改变本身不是永久的,另一事务可能在此后改变数据。事务日志能提供可追查的依据。
8.2 实际开发中可用的事务处理方式
在实际的开发过程中,通常可以采用以下一些事务处理的方式:
Ø 使用数据库的事务处理
Ø 基于数据库连接的事务处理
Ø 使用事务处理服务器
使用数据库的事务处理
一般的关系型数据库都提供了处理事务的能力。在数据库中,调用事务处理的一般流程是:
BEGIN TRANSACTION --进行处理 如果在处理的过程中发生错误,ROLLBACK TRANSACTION 如果一切正常,COMMIT TRANSACTION END TRANSACTION |
下面的代码演示了一个在SQL Server中在触发器中使用T-SQL进行事务处理的例子:
CREATE TRIGGER TestTrig ON TestTab FOR UPDATE AS BEGIN TRANSACTION INSERT INTO TrigTarget SELECT * FROM inserted IF (@@error <> 0) BEGIN ROLLBACK TRANSACTION END ELSE BEGIN COMMIT TRANSACTION END |
基于数据库连接的事务处理
常用的数据库编成接口,如JDBC,ADO、ADO.Net,都提供了基于数据连接进行事务处理的功能。
下面,我们通过一个例子,来看看在JDBC中是怎么使用事务处理的。我们模拟一个银行转帐的过程。
有如下一个数据库表(Account),表示银行的账户:
字段名称 |
数据类型 |
AccountID |
int |
Balance |
Double |
同时,我们在数据库中设定了一个约束条件:Balance>=0。这样,当任何对数据库的操作导致Balance<0的时候,都会引起异常。
可以通过下面的SQL语句来创建这个表:
CREATE TABLE Account( AccountID int, Balance double, Check (Balance>=0) ) |
往数据库中添加如下的记录。
AccountID |
Balance |
1 |
300 |
2 |
1000 |
下面,我们来编写一个转帐的方法。
public boolean transferFunds(int fromAccount,int toAccount,double amount) throws SQLException { const String withdrawSql = "UPDATE Account SET Balance=Balance-"+amount+" WHERE AccountID="+fromAccount; const String depositSql = "UPDATE Account SET Balance=Balance+"+amount+" WHERE AccountID="+toAccount; //首先获取数据库连接 Connection conn; try { Context initCtx = new InitialContext(); javax.sql.DataSource ds = (javax.sql.DataSource) initCtx.lookup(dataSourceName); Connection cn = ds.getConnection(); } catch(Exception e) { return false; } boolean success = false; try { conn.setAutoCommit(false); //开始启动事务 Statement stmt = conn.createStatement(); stmt.executeUpdate(withdrawSql); //取钱 stmt.executeUpdate(depositSql); //存钱 conn.commit(); success = true; } catch (SQLException e) { conn.rollback(); success = false; } finally { conn.close(); } return success; } |
上面的代码,如果我们调用transferFunds(1,2,200),表示从账户1转帐到账户2,那么,数据库的结果就是:
AccountID |
Balance |
1 |
100 |
2 |
1200 |
这个时候,如果在调用一次transferFunds(1,2,200)的方法,那么,由于账户1的数据违反了Balance>=0的约束条件,就会引发一个异常,所有的数据都不会被修改。
类似的,也可以使用.Net下面的编程语言来完成这项工作。下面的代码是用C#写的,可以看到,代码是非常类似的。
public bool TransferFunds(int fromAccount,int toAccount,double amount) { const string connString = "......";//设定数据库连接字符串 const string withdrawSql = "UPDATE Account SET Balance=Balance-" + amount + " WHERE AccountID=" + fromAccount; const string depositSql = "UPDATE Account SET Balance=Balance+" + amount + " WHERE AccountID=" + toAccount; //首先获取数据库连接 IDbConnection conn = null; IDbTransaction trans = null; try { conn = new GetConnection(connString); //得到数据库连接 conn.Open(); } catch (Exception e) { return false; } boolean success = false; try { trans = conn.BeginTransaction(); IDbCommand cmd = conn.CreateCommand(); cmd.ExecuteNonQuery(withdrawSql); //取钱 cmd.ExecuteNonQuery(depositSql); //存钱 trans.Commit(); success = true; } catch (SQLException e) { trans.Rollback(); success = false; } finally { conn.Close(); } return success;
} } |
可以看出,使用基于连接的事务是很简单的,只要指定事务边界,执行数据库代码,然后提交或者撤销事务就可以了。
使用事务处理服务器
前面介绍了使用数据库本身的事务处理,以及使用基于数据库连接的事务。但是,当涉及到多个数据库事务的时候,上面的方式就力不从心了,特别是涉及到一些分布式处理的事务的时候,例如,在多个银行之间转帐。在这种情况下,我们通常需要一个事务处理服务器来协调处理这些事务。
什么是分布式事务
分布式事务就是事务分布在多个资源上、由多个组件共享的事务。分布式事务具有如下的特征:
Ø 组件要在同一原子操作中与多个资源通信。在一个银行的系统中,资金可能从工商银行转帐到中国银行,这就涉及到两个银行系统。
Ø 多个组件要在同一原子操作中操作。资金从工商银行转帐到中国银行的时候,要么转帐成功,双方的帐户的资金都发生变化,要么失败,双方的帐户资金都没有变化,只有一方发生变化的情况是绝对不允许的。
分布式事务需要多个不同的事务管理器的合作。主事务管理器称为分布式事务管理器,与其他事务管理器协调。分布式事务管理器负责控制在若干本地事务管理器之间传递、分界和解析单个事务。本地事务管理器是参与分布式事务的事务管理器。
X/Open组织(即现在的Open Group)定义了分布式事务处理模型。X/Open DTP模型(1994)包括应用程序(AP)、事务管理器(TM)、资源管理器(RM)、通信资源管理器(CRM)四部分。一般,常见的事务管理器(TM)是交易中间件,常见的资源管理器(RM)是数据库,常见的通信资源管理器(CRM)是消息中间件。
X/Open 分布式事务(DTP)模型
XA与两阶段提交协议
XA就是X/Open DTP定义的交易中间件与数据库之间的接口规范(即接口函数),事务管理服务器用它来通知数据库事务的开始、结束以及提交、回滚等。XA接口函数由数据库厂商提供。
通常情况下,事务管理服务器与数据库通过XA 接口规范,使用两阶段提交来完成一个全局事务,XA规范的基础是两阶段提交协议。
在第一阶段,事务管理服务器请求所有相关数据库准备提交(预提交)各自的事务分支,以确认是否所有相关数据库都可以提交各自的事务分支。当某一数据库收到预提交后,如果可以提交属于自己的事务分支,则将自己在该事务分支中所做的操作固定记录下来,并给事务管理服务器一个同意提交的应答,此时数据库将不能再在该事务分支中加入任何操作,但此时数据库并没有真正提交该事务,数据库对共享资源的操作还未释放(处于上锁状态)。如果由于某种原因数据库无法提交属于自己的事务分支,它将回滚自己的所有操作,释放对共享资源上的锁,并返回给事务管理服务器失败应答。
在第二阶段,事务管理服务器审查所有数据库返回的预提交结果,如所有数据库都可以提交,事务管理服务器将要求所有数据库做正式提交,这样该全局事务被提交。而如果有任一数据库预提交返回失败,事务管理服务器将要求所有其它数据库回滚其操作,这样该全局事务被回滚。
以一个全局事务为例,AP首先通知事务管理服务器开始一个全局事务,事务管理服务器通过XA接口函数通知数据库开始事务,然后AP可以对数据库管理的资源进行操作,数据库系统记录事务对本地资源的所有操作。操作完成后事务管理服务器通过XA接口函数通知数据库操作完成。事务管理服务器负责记录AP操作过哪些数据库(事务分支)。AP根据情况通知事务管理服务器提交该全局事务,事务管理服务器会通过XA接口函数要求各个数据库做预提交,所有数据库返回成功后要求各个数据库做正式提交,此时一笔全局事务结束。
XA规范对应用来说,最大好处在于事务的完整性由事务管理服务器和数据库通过XA接口控制,AP只需要关注与数据库的应用逻辑的处理,而无需过多关心事务的完整性,应用设计开发会简化很多。
在EJB中使用分布式事务
在EJB中,容器提供了事务服务的功能,可以使用Java 事务 API(JTA)来访问事务服务,它提供对 Java 事务服务(Java Transaction Service,JTS)的简单访问。对于任何遵循 XA 规范的资源,JTA 的 XA 部分都有能力协调用这些资源的事务(通过接口 javax.transaction.xa.XAResource )。J2EE 中支持 XA 的两个资源类型是 Java 数据库连接(Java Database Connectivity,JDBC)API(通过接口 javax.sql.XAConnection )和 Java 消息服务(Java Message Service,JMS)API(通过接口 javax.jms.XAConnection )。
下面我们通过一个简单的例子来看看在EJB中是如何使用JTA的。
首先,我们需要设置两个数据源,分别针对Oracle和DB2数据库。JNDI分别为“jdbc/OracleXADS”和“jdbc/DB2XADS”。另外,配置一个JMS 提供者,JNDI名为“jms/XAExampleQ”。编写XAExampleSessionBean并添加下面的代码:
|
该方法执行以下三个步骤:
1. 使用名为 jdbc/OracleXADS 的数据源保持数据。
2. 使用名为 jdbc/DB2XADS 的数据源保持数据。
3. 将 JMS 消息中的数据发送到名为 jms/XAExampleQ 的队列。
如果发生任何错误,批处理块将捕捉该异常,将事务标记为回滚,并重新抛出该异常。为了代码的完整性,以下列出了实现该会话 bean 的其余代码:
|
这是用于使用 JDBC 和 JMS 的标准代码。甚至没有任何代码用于定义事务或者将事务变为 XA。但由于EJB 容器将这些代码作为单独的公共 EJB 方法来调用,所以容器会在事务中自动运行这些代码。容器将正常使用简单(单阶段)事务,但当第二个事务资源更新时,容器将自动检测,并将简单事务转换为 XA 事务。作为该工作的一部分,容器将通知资源并为其协调 XA 事务,让这些资源参与该事务。所有这些行为都是自动完成的,不需要开发人员编写任何额外的代码,而仅仅通过实现 EJB 中的代码就可以完成。由此我们也可以看出使用事务管理服务器可以给我们的开发带来的便利。
Websharp中的事务处理
Websharp中提供了基本的事务处理功能,这个通过Transaction接口来完成。这个接口前面已经给出了,在这里再描述一遍。
public interface Transaction { void Begin(); void Commit(); void Rollback(); PersistenceManager PersistenceManager{get;} } |
在设计上,一个Transaction依附于一个PersistenceManager,因此,他只能处理简单的事务,而不能处理分布式事务。这也是由于Websharp框架式一个轻量级的框架,只提供一些基本的服务。
下面的代码描述了使用Websharp进行事务处理的过程:
DatabaseProperty dbp = new DatabaseProperty(); dbp.DatabaseType = DatabaseType.MSSQLServer; dbp.ConnectionString = "Server=127.0.0.1;UID=sa;PWD=sa;Database=WebsharpTest;"; PersistenceManager pm = PersistenceManagerFactory.Instance().Create(dbp);
Order o = new Order(); o.OrderType = new OrderType(3, "音响"); o.OrderID = 3; o.Title = "SecondOrder"; o.IsDigned = false; o.AddTime = DateTime.Now; o.Details = new List<OrderDetail>(2); for (int j = 1; j < 3; j++) { OrderDetail od = new OrderDetail(); od.OrderID = 3; od.ProductID = j; od.Amount = j; o.Details.Add(od); }
Transaction t = pm.CurrentTransaction; t.Begin(); //开始一个事务 try { pm.PersistNew(o, PersistOptions.IncludeChildren); pm.Flush(); t.Commit(); } catch (Exception e) { Console.WriteLine(e.ToString()); t.Rollback(); } finally { pm.Close(); } |
前面说过,Websharp的Transaction接口只能完成简单的事务处理功能,复杂的事务,例如,涉及到两个数据库的事务,可以利用.Net Framework的EnterpriseService(COM+)来完成。
COM+事务
COM+ 是一个对象运行时环境,提供了一套服务旨在简化可伸缩分布式系统的创建过程。.NET 企业服务是基于COM+提供的服务的,或者说,COM+现在是 .NET 企业服务的一部分。
所有的 COM+ 服务都是依据上下文概念实现的。上下文是指一个进程中的空间,可以为驻留在其中的一个或者多个对象提供运行时服务。当位于一个上下文中的对象(或者线程)调用另一个上下文中的对象时,方法将被一个代理侦听,如图 所示。代理为 COM+ 运行时提供了机会,可以对调用进行预处理和后处理,并执行必要的服务代码以满足目标对象的需要,例如调用序列化、声明性事务管理等等。
图 上下文与侦听
COM+ 服务(例如自动事务处理或排队组件)都是可以声明方式配置的。在设计时应用与服务相关的属性,并创建使用这些服务的类的实例。配置某些服务的途径是在与服务相关的类或接口上调用方法。某些服务可以从一个对象流到另一个对象。例如,配置为要求事务的对象可以在第二个对象也支持或要求事务的情况下,将事务扩展到第二个对象。
COM+ 目录中包含配置信息,可以将该配置信息应用于类的实现。在运行时,COM+ 会根据您为代码提供的属性来创建上下文服务层。下面的插图显示了在 COM+ 承载的两个托管对象之间流动的自动事务处理。
承载服务组件的 COM+ 应用程序
服务还可以在 COM+ 和 .NET Framework 对象之间流动。每一个环境都控制其本机代码的实现和执行;COM+ 总是提供对象上下文。
如同 COM 一样,CLR 依赖 COM+ 提供了对构建可伸缩应用程序的开发人员有用的运行时服务。用 CLR 来实现 COM+ 已配置类比使用 COM 来实现它们更容易,并且在某些情况下更有效。这两种技术的集成并不只是通过与 COM 的互操作性才能达到,理解这一点非常重要。也就是说,在可以使用 CLR 实现使用 COM+ 的传统 COM 组件时,CLR 与 COM+ 之间的集成程度实际上已经深入多了,这产生了一种可以与其他 CLR 技术(如远程处理和代码访问安全性)更好地集成的编程模型。COM+ 的 CLR 托管 API 通过 System.EnterpriseServices 命名空间的类型定义。依赖于 COM+ 的 CLR 类使用这些类型定义它们的声明性属性,同对象和调用上下文交互。
System.EnterpriseService 命名空间中最重要的类型是 ServicedComponent。所有使用 COM+ 运行时服务的 CLR 类必须扩展 ServicedComponent,如下所示:
using System.EnterpriseServices; namespace ESExample { [Transaction(TransactionOption.Required)] public classMyCfgClass :ServicedComponent { [AutoComplete] static void Main(){} } } |
在这段代码中,Transaction 属性的存在指示了 MyTxCfgClass 需要使用一个 COM+ 托管的分布式事务。
当已配置类实现后,它必须被编译。编译代码是容易的,但需要牢记两件事情。首先,COM+ 集成基础结构要求被编译的程序集具有强名称。为了创建一个具有强名称的程序集,必须通过运行强名称实用工具 sn.exe,生成一个密钥。接着必须在您的组件代码中使用一个来自于 System.Reflection 命名空间称为 AssemblyKeyFileAttribute 的程序集级别属性来引用该密钥,它被存储在一个文件中,如下面的代码所示:
using System.EnterpriseServices; using System.Reflection; [assembly:ApplicationName("MyApp")] [assembly:ApplicationActivation(ActivationOption.Library)] // AssemblyKeyFile attribute references keyfile generated [assembly:AssemblyKeyFile("keyfile")] namespace ESExample {……} |
其次,在编译具有强名称的程序集时,必须引用导出 System.EnterpriseServices 命名空间中类型的程序集 System.EnterpriseServices.dll。下面给出的是生成一个密钥以及编译一个已配置类所需要的命令:
sn -k keyfile csc /out:ESExample.dll /t:library/r:System.EnterpriseServices.dll MyCfgClass.cs |
在一个基于 CLR 的已配置类已经编译后,就需要部署它的程序集了。可以通过从命令行运行服务安装实用工具 regsvcs.exe 来完成,如下所示:
regsvcs ESExample.dll
该工具完成三件事情。首先,它将 CLR 程序集作为一个 COM 组件注册(如同已经运行了程序集注册实用工具 regasm.exe)。其次,它生成一个 COM 类型库(如同已经运行了程序集到类型库转换器 tlbexp.exe)并且使用它来部署在 COM+ 编录中程序集实现的已配置类。 Regsvcs.exe 在默认情况下将创建程序集的 ApplicationName 与 ApplicationActivation 属性所描述的目标应用程序。(也可以使用命令行开关重写这种行为。)第三,它使用 .NET 反射 API 来询问程序集实现的已配置类的元数据,并使用该信息编程更新 COM+ 编录,使每个类都将有相应的声明性属性设置
一旦一个基于 CLR 的已配置类编译和部署完成,就可以使用它了。从客户端的视角看,已配置类并没有什么特殊的地方;它使用 COM+ 运行时服务这一事实是无关紧要的。下面的代码显示了一个使用前面所定义的 MyTxCfgClass 类的简单客户端:
using ESExample; public class Client { public static void Main(string[] args) { MyTxCfgClass tcc = new MyTxCfgClass(); ... // use object as desired } } |
当然,对于所有的 CLR 代码,当编译客户端时必须提供一个指向已配置类程序集的引用。
此时可以看出,用 .NET CLR 实现 COM+ 已配置类是相当简单的。System.EnterpriseServices 命名空间中的类型提供了 COM+ 的一个托管 API,从而简化了运行时服务的使用。
以上只是简单的介绍了在.Net中使用COM+事务的基本概念,更多的关于COM+事务的知识,可以参考微软的相关资料。