1. 本地事务:紧密依赖于底层资源管理器(例如数据库连接 ),事务处理局限在当前事务资源内。此种事务处理方式不存在对应用服务器的依赖,因而部署灵活却无法支持多数据源的分布式事务。如下面的JDBC事务。
2. 分布式事务处理 : Java 事务编程接口(JTA:Java Transaction API)和 Java 事务服务 (JTS;Java Transaction Service) 为 J2EE 平台提供了分布式事务服务。
下面就具体总结一下J2EE中的两种平面事务:
Java事务API由三个部分组成:
UserTransaction接口:
javax.transaction.UserTransaction接口给应用程序提供了编程控制事务边界的能力。该接口可以供Java客户端程序或EJB使用。
UserTransaction的begin方法开始一个全局事务,并将该事务与调用线程关联。事务到线程的管理是由事务管理器完成的,对应用程序透明的。
对嵌套事务的支持不是必须的。如果调用线程的上下文已经与事务关联,并且事务管理器的实现并不支持嵌套的事务,UserTransaction的begin方法调用时将抛出NotSupportedException。
底层的事务管理器的实现负责提供不同应用程序间事务上下文的传播,事务管理器位于客户端和服务器计算机上。 传播的事务上下文的格式由客户端和服务器计算机协商确定。例如,如果事务管理器是JTS规范的实现,将使用CORBA OTS 1.1规范中描述的事务上下文传播格式。事务上下文的传播对于应用程序来说是透明的。
从JNDI获取UserTransaction:
如果环境中安装了JTA的实现,应可以从java:comp/UserTransaction
获取UserTransaction。
EJB服务器对UserTransaction的支持:
EJB服务器需要支持UserTransaction接口,以供Bean管理事务的EJB使用。通过EJBContext的getUserTransaction方法,EJB构件可以获取到UserTransaction接口。这样,EJB应用程序不需要与事务管理器直接交互,就可以划分事务边界。取而代之的是 EJB依赖于EJB服务器提供EJB规范中定义的所有事务处理。(EJB服务器和事务管理器之间的交互对于应用是透明。实现事务管理的责任是在EJB容器和服务的提供商。
下面的例子说明了Bean管理事务的会话Bean如何使用UserTransaction:
// 在会话Bean的setSessionContext方法中, // 将Bean的上下文存放到一个实例变量中 this.ctx = sessionContext; // 在Bean业务逻辑中 UserTransaction utx = ctx.getUserTransaction(); // 开始一个事务 utx.begin(); // 做一些事情 // 提交 utx.commit();
在了解事务的概念后,看看目前J2EE已经做了哪些支持,一般来说,J2EE应用服务器支持JDBC事务、JTA(Java Transaction API[java事务API])事务、容器管理事务(一般由容器来进行管理)。一般情况下,最好不要在程序中同时使用上述三种事务类型,比如在JTA事务中嵌套JDBC事务。第二方面,事务要在尽可能短的时间内完成,不要在不同方法中实现事务的使用(事务的嵌套要求更加良好的设计)。
在JDBC中怎样将多个SQL语句组合成一个事务呢?在JDBC中,打开一个连接对象Connection时,缺省是auto-commit模式,每个SQL语句都被当作一个事务,即每次执行一个语句,都会自动的得到事务确认。为了能将多个SQL语句组合成一个事务,要将auto-commit模式屏蔽掉。在auto-commit模式屏蔽掉之后,如果不调用commit()方法,SQL语句不会得到事务确认。在最近一次commit()方法调用之后的所有SQL会在方法commit()调用时得到确认。
使用如下方法控制连接的独立性等级: conn.setTransactionIsolation(Connection.TRANSACTION_XXX);
事务的开始与结束: 首先,因为默认的JDBC事务是自动提交的,故我们在获取数据库连接后,应该将连接改为非自动提交模式: conn.setAutoCommit(false); //标志着新事务的开始 conn.commit(); //提交 conn.rollback(); //回滚
在JDBC 3.0新增功能中,提供对事务commit和rollback的更好支持,在事务过程中,在两个操作间可以插入一个命名的存储点作为标记,因此可以将事务回滚到那个标记,保留标记有效前的所有操作。 示例: conn.setAutoCommit(false); Statement stmt = conn.createStatement();
stmt.executeUpdate(update1); Savepoint point1 = conn.setSavepoint("point1");
stmt.executeUpdate(update2); stmt.executeUpdate(update3);
conn.rollback(point1); conn.commit(); |
Java 事务 API(JTA) 及其同门兄弟 Java 事务服务(Java Transaction Service JTS)为 J2EE 平台提供了分布式事务服务。一个分布式的事务涉及一个事务管理器和一个或者多个资源管理器。一个资源管理器是任何类型的持久性的数据存储。事务管理器负责协调所有事务参与者之间的通信。对于跨数据源(例如多个数据库,或者数据库 与 JMS)的大型应用,则必须使用全局事务 JTA (Java Transaction API)。JTA 为 J2EE 平台提供了分布式事务服务,它隔离了事务与底层的资源,实现了透明的事务管理方式。本文将深入探寻 JTA 的体系架构,并通过详细的代码介绍其实现机制。
分布式事务(Distributed Transaction)包括事务管理器(Transaction Manager)和一个或多个支持 XA 协议的资源管理器 ( Resource Manager )。我们可以将资源管理器看做任意类型的持久化数据存储;事务管理器承担着所有事务参与单元的协调与控制。JTA 事务有效的屏蔽了底层事务资源,使应用可以以透明的方式参入到事务处理中;
但是与本地事务相比,XA 协议的系统开销大,在系统开发过程中应慎重考虑是否确实需要分布式事务。若确实需要分布式事务以协调多个事务资源,则应实现和配置所支持 XA 协议的事务资源,如 JMS、JDBC 数据库连接池等。
JTA事务工作流程
WEB服务器(比如:WebLogic Server)将根据以下条件返回不同种类的包装器:
1、所使用的 JDBC 驱动程序类是否支持 XA
2、是从 DataSource 还是从 TxDataSource 获得连接
3、调用 getConnection() 时是否在事务内运行
4、是否通过 RMI 从远程获得连接
决定返回哪种包装器的算法的工作方式如下:
JTA实例代码:
/** * 测试Jboss中的JTA事务 * <p>@author javer QQ:84831612</p> * @date 2005 */ javax.transaction.UserTransaction tx = null; java.sql.Connection conn = null; try{ tx = (javax.transaction.UserTransaction) context.lookup("java:comp/UserTransaction"); //取得JTA事务,本例中是由Jboss容器管理 javax.sql.DataSource ds = (javax.sql.DataSource) context.lookup("java:/XAOracleDS"); //取得数据库连接池,必须有支持XA的数据库、驱动程序 tx.begin(); conn = ds.getConnection(); //conn.setAutoCommit(false); java.sql.Statement statement = conn.createStatement(); String sql = "insert into testtable (cell1,cell2,cell3,cell4) values('"+System.currentTimeMillis()+"','','','')"; int insert = statement.executeUpdate(sql); //conn.commit(); //JTA事务中不要嵌套JDBC事务啦!!!重要,切记,否则会抛出异常!!! out.println("插入了" + insert + "行记录!<br />"); if(true)throw new Exception("故意抛出的异常!"); int num = statement.executeUpdate("delete testtable"); out.println("删除了" + num + "行记录!<br />"); tx.commit(); } catch (Exception e) { if(tx!=null) try{tx.rollback();}catch(Exception e1){out.println("catch:事务回滚失败!<br />");} out.println("catch:" + e.getClass() + ";" + e.getMessage()+"<br />"); }finally{ if(conn!=null) try{conn.close();}catch(Exception e1){out.println("finally:关闭数据库连接失败!<br />");} }
public void transferAccount() { UserTransaction userTx = null; Connection connA = null; Statement stmtA = null; Connection connB = null; Statement stmtB = null; try{ // 获得 Transaction 管理对象 userTx = (UserTransaction)getContext().lookup("\java:comp/UserTransaction"); // 从数据库 A 中取得数据库连接 connA = getDataSourceA().getConnection(); // 从数据库 B 中取得数据库连接 connB = getDataSourceB().getConnection(); // 启动事务 userTx.begin(); // 将 A 账户中的金额减少 500 stmtA = connA.createStatement(); stmtA.execute("update t_account set amount = amount - 500 where account_id = 'A'"); // 将 B 账户中的金额增加 500 stmtB = connB.createStatement(); stmtB.execute("\update t_account set amount = amount + 500 where account_id = 'B'"); // 提交事务 userTx.commit(); // 事务提交:转账的两步操作同时成功(数据库 A 和数据库 B 中的数据被同时更新) } catch(SQLException sqle){ try{ // 发生异常,回滚在本事务中的操纵 userTx.rollback(); // 事务回滚:转账的两步操作完全撤销 //( 数据库 A 和数据库 B 中的数据更新被同时撤销) stmt.close(); conn.close(); ... }catch(Exception ignore){ } sqle.printStackTrace(); } catch(Exception ne){ e.printStackTrace(); } }
3、容器事务
容器事务主要是J2EE应用服务器提供的,容器事务大多是基于JTA完成,这是一个基于JNDI的,相当复杂的API实现。相对编码实现JTA事务管理, 我们可以通过EJB容器提供的容器事务管理机制(CMT)完成同一个功能,这项功能由J2EE应用服务器提供。这使得我们可以简单的指定将哪个方法加入事 务,一旦指定,容器将负责事务管理任务。这是我们土建的解决方式,因为通过这种方式我们可以将事务代码排除在逻辑编码之外,同时将所有困难交给J2EE容器去解决。使用EJB CMT的另外一个好处就是程序员无需关心JTA API的编码,不过,理论上我们必须使用EJB.
四、三种Java事务差异
1、JDBC事务控制的局限性在一个数据库连接内,但是其使用简单。
2、JTA事务的功能强大,事务可以跨越多个数据库或多个DAO,使用也比较复杂。
3、容器事务,主要指的是J2EE应用服务器提供的事务管理,局限于EJB应用使用。
很多开发人员都会对 JTA 的内部工作机制感兴趣:我编写的代码没有任何与事务资源(如数据库连接)互动的代码,但是我的操作(数据库更新)却实实在在的被包含在了事务中,那 JTA 究竟是通过何种方式来实现这种透明性的呢? 要理解 JTA 的实现原理首先需要了解其架构:它包括事务管理器(Transaction Manager)和一个或多个支持 XA 协议的资源管理器 ( Resource Manager ) 两部分, 我们可以将资源管理器看做任意类型的持久化数据存储;事务管理器则承担着所有事务参与单元的协调与控制。 根据所面向对象的不同,我们可以将 JTA 的事务管理器和资源管理器理解为两个方面:面向开发人员的使用接口(事务管理器)和面向服务提供商的实现接口(资源管理器)。其中开发接口的主要部分即为 上述示例中引用的 UserTransaction 对象,开发人员通过此接口在信息系统中实现分布式事务;而实现接口则用来规范提供商(如数据库连接提供商)所提供的事务服务,它约定了事务的资源管理功能,使得 JTA 可以在异构事务资源之间执行协同沟通。以数据库为例,IBM 公司提供了实现分布式事务的数据库驱动程序,Oracle 也提供了实现分布式事务的数据库驱动程序, 在同时使用 DB2 和 Oracle 两种数据库连接时, JTA 即可以根据约定的接口协调者两种事务资源从而实现分布式事务。正是基于统一规范的不同实现使得 JTA 可以协调与控制不同数据库或者 JMS 厂商的事务资源,其架构如下图所示:
开发人员使用开发人员接口,实现应用程序对全局事务的支持;各提供商(数据库,JMS 等)依据提供商接口的规范提供事务资源管理功能;事务管理器( TransactionManager )将应用对分布式事务的使用映射到实际的事务资源并在事务资源间进行协调与控制。 下面,本文将对包括 UserTransaction、Transaction 和 TransactionManager 在内的三个主要接口以及其定义的方法进行介绍。
面向开发人员的接口为 UserTransaction (使用方法如上例所示),开发人员通常只使用此接口实现 JTA 事务管理,其定义了如下的方法:
面向提供商的实现接口主要涉及到 TransactionManager 和 Transaction 两个对象
Transaction 代表了一个物理意义上的事务,在开发人员调用 UserTransaction.begin() 方法时 TransactionManager 会创建一个 Transaction 事务对象(标志着事务的开始)并把此对象通过 ThreadLocale 关联到当前线程。UserTransaction 接口中的 commit()、rollback(),getStatus() 等方法都将最终委托给 Transaction 类的对应方法执行。Transaction 接口定义了如下的方法:
TransactionManager 本身并不承担实际的事务处理功能,它更多的是充当用户接口和实现接口之间的桥梁。下面列出了 TransactionManager 中定义的方法,可以看到此接口中的大部分事务方法与 UserTransaction 和 Transaction 相同。 在开发人员调用 UserTransaction.begin() 方法时 TransactionManager 会创建一个 Transaction 事务对象(标志着事务的开始)并把此对象通过 ThreadLocale 关联到当前线程上;同样 UserTransaction.commit() 会调用 TransactionManager.commit(), 方法将从当前线程下取出事务对象 Transaction 并把此对象所代表的事务提交, 即调用 Transaction.commit()
在 系统开发过程中会遇到需要将事务资源暂时排除的操作,此时就需要调用 suspend() 方法将当前的事务挂起:在此方法后面所做的任何操作将不会被包括在事务中,在非事务性操作完成后调用 resume()以继续事务(注: 要进行此操作需要获得 TransactionManager 对象, 其获得方式在不同的 J2EE 应用服务器上是不一样的)
下面将通过具体的代码向读者介绍 JTA 实现原理。下图列出了示例实现中涉及到的 Java 类,其中 UserTransactionImpl 实现了 UserTransaction 接口,TransactionManagerImpl 实现了 TransactionManager 接口,TransactionImpl 实现了 Transaction 接口
public void begin() throws NotSupportedException, SystemException { // 将开始事务的操作委托给 TransactionManagerImpl TransactionManagerImpl.singleton().begin(); }
// 此处 transactionHolder 用于将 Transaction 所代表的事务对象关联到线程上 private static ThreadLocal<TransactionImpl> transactionHolder = new ThreadLocal<TransactionImpl>(); //TransacationMananger 必须维护一个全局对象,因此使用单实例模式实现 private static TransactionManagerImpl singleton = new TransactionManagerImpl(); private TransactionManagerImpl(){ } public static TransactionManagerImpl singleton(){ return singleton; } public void begin() throws NotSupportedException, SystemException { //XidImpl 实现了 Xid 接口,其作用是唯一标识一个事务 XidImpl xid = new XidImpl(); // 创建事务对象,并将对象关联到线程 TransactionImpl tx = new TransactionImpl(xid); transactionHolder.set(tx); }
现在我们就可以理解 Transaction 接口上没有定义 begin 方法的原因了:Transaction 对象本身就代表了一个事务,在它被创建的时候就表明事务已经开始,因此也就不需要额外定义 begin() 方法了。
public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { // 检查是否是 Roll back only 事务,如果是回滚事务 if(rollBackOnly){ rollback(); return; } else { // 将提交事务的操作委托给 TransactionManagerImpl TransactionManagerImpl.singleton().commit(); } }
public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { // 取得当前事务所关联的事务并通过其 commit 方法提交 TransactionImpl tx = transactionHolder.get(); tx.commit(); }
同理, rollback、getStatus、setRollbackOnly 等方法也采用了与 commit() 相同的方式实现。 UserTransaction 对象不会对事务进行任何控制, 所有的事务方法都是通过 TransactionManager 传递到实际的事务资源即 Transaction 对象上。
上述示例演示了 JTA 事务的处理过程,下面将为您展示事务资源(数据库连接,JMS)是如何以透明的方式加入到 JTA 事务中的。首先需要明确的一点是,在 JTA 事务 代码中获得的数据库源 ( DataSource ) 必须是支持分布式事务的。在如下的代码示例中,尽管所有的数据库操作都被包含在了 JTA 事务中,但是因为 MySql 的数据库连接是通过本地方式获得的,对 MySql 的任何更新将不会被自动包含在全局事务中。
public void transferAccount() { UserTransaction userTx = null; Connection mySqlConnection = null; Statement mySqlStat = null; Connection connB = null; Statement stmtB = null; try{ // 获得 Transaction 管理对象 userTx = (UserTransaction)getContext().lookup("java:comp/UserTransaction"); // 以本地方式获得 mySql 数据库连接 mySqlConnection = DriverManager.getConnection("localhost:1111"); // 从数据库 B 中取得数据库连接, getDataSourceB 返回应用服务器的数据源 connB = getDataSourceB().getConnection(); // 启动事务 userTx.begin(); // 将 A 账户中的金额减少 500 //mySqlConnection 是从本地获得的数据库连接,不会被包含在全局事务中 mySqlStat = mySqlConnection.createStatement(); mySqlStat.execute(" update t_account set amount = amount - 500 where account_id = 'A'"); //connB 是从应用服务器得的数据库连接,会被包含在全局事务中 stmtB = connB.createStatement(); stmtB.execute(" update t_account set amount = amount + 500 where account_id = 'B'"); // 事务提交:connB 的操作被提交,mySqlConnection 的操作不会被提交 userTx.commit(); } catch(SQLException sqle){ // 处理异常代码 } catch(Exception ne){ e.printStackTrace(); } }
为什么必须从支持事务的数据源中获得的数据库连接才支持分布式事务呢?其实支持事务的数据源与普通的数据源是不同 的,它实现了额外的 XADataSource 接口。我们可以简单的将 XADataSource 理解为普通的数据源(继承了 java.sql.PooledConnection),只是它为支持分布式事务而增加了 getXAResource 方法。另外,由 XADataSource 返回的数据库连接与普通连接也是不同的,此连接除了实现 java.sql.Connection 定义的所有功能之外还实现了 XAConnection 接口。我们可以把 XAConnection 理解为普通的数据库连接,它支持所有 JDBC 规范的数据库操作,不同之处在于 XAConnection 增加了对分布式事务的支持。通过下面的类图读者可以对这几个接口的关系有所了解:
应用程序从支持分布式事务的数据源获得的数据库连接是 XAConnection 接口的实现,而由此数据库连接创建的会话(Statement)也为了支持分布式事务而增加了功能,如下代码所示:
public void transferAccount() { UserTransaction userTx = null; Connection conn = null; Statement stmt = null; try{ // 获得 Transaction 管理对象 userTx = (UserTransaction)getContext().lookup(" java:comp/UserTransaction"); // 从数据库中取得数据库连接, getDataSourceB 返回支持分布式事务的数据源 conn = getDataSourceB().getConnection(); // 会话 stmt 已经为支持分布式事务进行了功能增强 stmt = conn.createStatement(); // 启动事务 userTx.begin(); stmt.execute("update t_account ... where account_id = 'A'"); userTx.commit(); } catch(SQLException sqle){ // 处理异常代码 } catch(Exception ne){ e.printStackTrace(); } }
我们来看一下由 XAConnection 数据库连接创建的会话(Statement)部分的代码实现(不同的 JTA 提供商会有不同的实现方式,此处代码示例只是向您演示事务资源是如何被自动加入到事务中)。 我们以会话对象的 execute 方法为例,通过在方法开始部分增加对 associateWithTransactionIfNecessary 方法的调用,即可以保证在 JTA 事务期间,对任何数据库连接的操作都会被透明的加入到事务中。
public void execute(String sql) { // 对于每次数据库操作都检查此会话所在的数据库连接是否已经被加入到事务中 associateWithTransactionIfNecessary(); try{ // 处理数据库操作的代码 .... } catch(SQLException sqle){ // 处理异常代码 } catch(Exception ne){ e.printStackTrace(); } } public void associateWithTransactionIfNecessary(){ // 获得 TransactionManager TransactionManager tm = getTransactionManager(); Transaction tx = tm.getTransaction(); // 检查当前线程是否有分布式事务 if(tx != null){ // 在分布式事务内,通过 tx 对象判断当前数据连接是否已经被包含在事务中, //如果不是那么将此连接加入到事务中 Connection conn = this.getConnection(); //tx.hasCurrentResource, xaConn.getDataSource() 不是标准的 JTA // 接口方法,是为了实现分布式事务而增加的自定义方法 if(!tx.hasCurrentResource(conn)){ XAConnection xaConn = (XAConnection)conn; XADataSource xaSource = xaConn.getDataSource(); // 调用 Transaction 的接口方法,将数据库事务资源加入到当前事务中 tx.enListResource(xaSource.getXAResource(), 1); } } }
XAResource 与 Xid: XAResource 是 Distributed Transaction Processing: The XA Specification 标准的 Java 实现,它是对底层事务资源的抽象,定义了分布式事务处理过程中事务管理器和资源管理器之间的协议,各事务资源提供商(如 JDBC 驱动,JMS)将提供此接口的实现。使用此接口,开发人员可以通过自己的编程实现分布式事务处理,但这些通常都是由应用服务器实现的(服务器自带实现更加 高效,稳定) 为了说明,我们将举例说明他的使用方式。
在使用分布式事务之前,为了区分事务使之不发生混淆,必须实现一个 Xid 类用来标识事务,可以把 Xid 想象成事务的一个标志符,每次在新事务创建是都会为事务分配一个 Xid,Xid 包含三个元素:formatID、gtrid(全局事务标识符)和 bqual(分支修饰词标识符)。 formatID 通常是零,这意味着你将使用 OSI CCR(Open Systems Interconnection Commitment, Concurrency 和 Recovery 标准)来命名;如果你要使用另外一种格式,那么 formatID 应该大于零,-1 值意味着 Xid 为无效。
gtrid 和 bqual 分别包含 64 个字节二进制码来分别标识全局事务和分支事务, 唯一的要求是 gtrid 和 bqual 必须是全局唯一的。
XAResource 接口中主要定义了如下方法:
在事务被提交时,Transaction 对象会收集所有被当前事务包含的 XAResource 资源,然后调用资源的提交方法,如下代码所示:
public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { // 得到当前事务中的所有事务资源 List<XAResource> list = getAllEnlistedResouces(); // 通知所有的事务资源管理器,准备提交事务 // 对于生产级别的实现,此处需要进行额外处理以处理某些资源准备过程中出现的异常 for(XAResource xa : list){ xa.prepare(); } // 所有事务性资源,提交事务 for(XAResource xa : list){ xa.commit(); } }
通 过如上介绍相信读者对 JTA 的原理已经有所了解,本文中的示例代码都是理想情况下的假设实现。一款完善成熟的 JTA 事务实现需要考虑与处理的细节非常多,如性能(提交事务的时候使用多线程方式并发提交事务)、容错(网络,系统异常)等, 其成熟也需要经过较长时间的积累。感兴趣的读者可以阅读一些开源 JTA 实现以进一步深入学习。