4.2 事务和JTA
事务控制也是Java EE应用中必须处理的问题,它可以保证一系列数据库操作能够准确完成。事务既是保证底层数据库完整性的重要手段,也是保证应用业务逻辑成功执行的重要基础。对于一个实际企业级应用而言,有的只需要采用局部事务控制即可,有的则需要全局事务控制。
JTA(Java Transaction API)则提供了事务划分的标准接口,尤其是当应用程序执行两个需要依赖于不同数据库的操作时,应用程序就需要使用JTA来将这两个操作包含成一个全局事务。
4.2.1 事务的基本概念
事务是由一步或几步数据库操作序列组成的逻辑执行单元,这系列操作要么全部执行,要么全部放弃执行。程序和事务是两个不同的概念。一般而言,一段程序中可能包含多个事务。
事务具备4个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持续性(Durability)。这4个特性也简称为ACID性。
原子性(Atomicity):事务是应用中最小执行单位,就如原子是自然界最小颗粒,具有不可再分的特征一样。事务是应用中不可再分的最小逻辑执行体。
一致性(Consistency):事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而该未完成的事务对数据库所做的修改已被写入数据库,此时,数据库就处于一种不正确的状态。比如银行在两个账户之间转账:从A账户向B账户转入1000元。系统先减少A账户的1000元,然后再为B账户增加1000元。如果全部执行成功,数据库处于一致性状态;如果仅执行完A账户金额的修改,而没有增加B账户的金额,则数据库就处于不一致性状态。因此,一致性是通过原子性来保证的。
隔离性(Isolation):各个事务的执行互不干扰,任意一个事务的内部操作对其他并发的事务都是隔离的。也就是说,并发执行的事务之间不能看到对方的中间状态,并发执行的事务之间不能互相影响。
持续性(Durability):持续性也称为持久性(Persistence),指事务一旦提交,对数据所做的任何改变都要记录到永久存储器中,通常就是保存进物理数据库。
Java EE应用中事务处理一般可以分为如下两类:
局部事务(Local Transaction Processing)
分布式事务(Distributed Transaction Processing,DTP)
对于绝大部分企业应用而言,它只需要涉及单一的数据库资源,因此只需要采用局部事务即可。局部事务通常采用单阶段提交;但也有一些更复杂的企业级应用,它们需要使用一个以上的事务性资源,比如应用程序需要访问多个数据库的数据,这就需要采用分布式事务处理进行管理。分布式事务处理的关键是必须有一种方法保证多个数据库所做的全部动作,它们也可以作为一个整体,要么全部提交,要么全部回滚,这样能保证当业务逻辑跨越多个数据库资源时让多个数据库的数据保持一致。
提示
可以有一种简单的理解方式:数据库事务都用于保证多个数据库操作当成一个整体处理,要么全部提交,要么全部回滚。只是局部事务操作的控制对象是单个的数据库,分布式事务操作的控制对象则是多个数据库。
当应用程序需要访问多个事务性资源时,直接依赖于底层数据库提供的局部事务显然无法保证多个数据库之间的一致性。因为一个数据库无法知道其他数据库在做什么,因此必须借助于中间件的事务管理器来进行协调,中间件的事务管理器负责通知和协调所有参与事务的相关数据库的提交或回滚,最后由各个单独的数据库将自己所做的操作进行实质提交或回滚。
从应用程序的角度来看,中间件的事务管理器消除了底层事务处理的复杂性,从而简化了分布式事务处理的编程步骤——应用程序无须理会底层多个事务资源里局部事务的开始、提交或回滚操作,应用程序只要面向中间件的事务管理器编程,用全局事务管理的API来开始、提交或回滚事务即可,而事务管理器则负责管理底层的多个事务资源。但应用程序向事务管理器发出一个事务处理操作,而底层则可能转换为多个低级别的事务命令。图4.12显示了事务管理器的功能示意图。
图4.12 事务管理器的功能示意图
4.2.2 分布式事务处理、XA规范和2PC协议
当一个业务操作需要涉及多个数据库时,这就可称为分布式事务处理了,实现分布式事务处理的关键是采用一种手段保证事务涉及的所有数据库所做的全部动作要么全部生效,要么全部回滚。为了协调多个事务资源的分布式事务处理,多个事务资源底层必须使用一种通用的事务协议,目前流行的分布式事务处理规范就是XA规范。
X/Open组织(即现在的Open Group)定义了分布式事务处理模型。X/Open DTP模型(1994)包括应用程序(AP)、事务管理器(TM)、资源管理器(RM)、通信资源管理器(CRM)4部分。一般来说,常见的事务管理器(TM)就是事务中间件(通常由应用服务器来实现),常见的资源管理器(RM)是数据库,常见的通信资源管理器(CRM)是消息中间件。
通常来说,应用程序对单个数据库内部的多个DML操作会组成一个局部事务,因为无须跨越多个事务性资源,因此直接使用底层数据库的事务支持就足够了;如果应用程序的数据库访问涉及对多个数据库的修改,那就面临着分布式事务处理了,分布式事务处理的对象是全局事务,通过全局事务才可以保证多个数据库之间的一致性。
X/Open组织为分布式事务处理指定了事务中间件与数据库之间的接口规范,这种规范就是XA规范。事务中间件用它来通知数据库事务的开始、提交或回滚等。
X/Open组织仅仅指定了分布式事务处理的XA规范,但具体的实现则由不同的数据库厂商自行提供;对于大部分主流的商业数据库,如Oracle、SQL Server等,它们都提供了支持XA规范的驱动;但一些小型的开源数据库如MySQL,则没有提供支持XA规范的驱动。
而XA规范的理论基础就是两阶段提交(2 Phase Commit,2PC)协议,该协议定义了单个的事务管理器如何协调和管理一个或多个数据库的局部事务,该协议大致可分为如下5个步骤:
1 应用程序面向事务管理器编程,应用程序调用事务管理器的提交方法。
2 事务管理器通知参与全局事务的每个数据库,告诉它们准备开始提交事务——第一个阶段从现在开始。
3 参与全局事务的各个数据库进行局部事务的预提交。
4 事务管理器收集到各个数据库预提交的结果。
5 第二阶段开始,事务管理器收集到所有参与全局事务预提交的结果之后做出相应的判断:如果所有数据库的局部事务预提交的结果都可以成功,事务管理器向每个数据库都发送进行实际提交的命令;如果任意一个数据库的局部事务预提交的结果失败了,事务管理器将向每个数据库发送进行实际回滚的命令,让所有数据库退回修改之前的状态。
对于单个数据库局部事务预提交的过程,我们进一步做解释:当某一数据库收到预提交要求后,如果可以提交属于自己的事务分支,则将自己在该事务分支中所做的操作记录下来,并给事务中间件一个同意提交的应答,此时数据库将不能再向该事务分支中加入任何操作,但此时数据库并没有真正提交该事务,底层数据库对共享资源的操作还未释放(处于上锁状态)。如果由于某种原因数据库无法提交属于自己的事务分支,它将回滚自己的所有操作,释放对共享资源上的锁,并返回给事务中间件失败的应答。
XA规范对应用来说,最大的好处在于事务的完整性由事务中间件和数据库来控制,而应用程序只需要关注业务逻辑的实现,而无须过多关心事务的完整性,从而大大简化应用程序开发的难度。
具体来说,如果没有事务中间件,应用系统需要在程序内部以编程方式来通知底层多个数据库事务的开始、提交或回滚,当出现异常情况时必须由专门的程序对数据库进行反向操作才能完成回滚。对于包含多个事务分支的全局事务,回滚时情况将变得异常复杂。但加入了事务管理器之后,全局事务的提交是由事务中间件负责的,应用程序只需通知事务中间件提交或回滚事务,就可以控制整个事务(底层可能涉及多个数据库)的全部提交或回滚,应用程序完全不用理会这些复杂的控制。
在典型的Java EE应用服务器中,全局事务管理器是必需的组件,它会负责协调和管理参与全局事务的多个数据库的局部事务处理。
在一个涉及多个数据库的分布式事务处理中,为保证全局事务的完整性,两阶段提交是必需的。但典型的两阶段提交,对数据库来说事务从开始到结束(提交或回滚)时间相对较长,在事务处理期间数据库使用的资源(如逻辑日志、各种锁)将一直处于锁定状态,直到事务结束时才会释放。因此,使用典型的两阶段提交相对来说会占用更多的资源,因此会带来一定的性能损失。
当一个全局事务只涉及一个数据库时,可以将两阶段提交优化成单阶段提交。当应用程序通知事务中间件提交事务时,事务中间件直接通知底层数据库提交事务,从而缩短参与事务的时间,以提高事务处理的效率。单阶段提交是两阶段提交的一种特例,与两阶段提交一样,单阶段提交也是标准的。实际上,WebLogic服务器已经实现了这种单阶段优化。
除此之外,WebLogic服务器还允许在两阶段提交中使用非XA数据库,就如前面在如图4.3所示的配置页面中看到的,WebLogic服务器提供了“记录上一个资源”和“仿真两阶段提交”的技术。
4.2.3 使用JTA全局事务保证多数据库的一致性
通过使用JTA编程,开发者可以用一种与事务管理器无关的方式来开始、提交或回滚事务,Java EE应用服务器通过Java事务服务(Java Transaction Service,JTS)来实现事务管理器。但应用程序代码无须直接调用JTS方法,它只需面向JTA方法即可,由JTA来调用底层的JTS进行处理。
JTA事务由Java EE事务管理器负责控制,它可以保证多个数据库更新的一致性,通过JTA即可实现全局事务控制。
如果希望使用JTA事务,开发者可调用javax.transaction.UserTransaction接口的begin、commit、rollback等方法来控制事务。
提示
虽然JTA主要由位于javax.transaction包下的3个接口:UserTransaction、Transaction Manager和Transaction组成,这些接口共享公共的事务操作,如commit()和rollback()。但有些特殊的事务操作,如suspend()、resume()和enlistResource(XAResource xaRes)等方法只在特定的接口中才提供。对于大部分开发者来说,他们只要了解UserTransaction接口即可,因为他们只要执行常规的事务操作,自然有应用服务器在后台负责处理大部分事务管理细节。
下面先介绍WebLogic服务器中使用JTA来保证多数据库的一致性。假设已经在WebLogic服务器中配置了两个支持XA规范的数据源(配置过程参考前一节介绍),两个数据源都是连接到Oracle 10g数据库。接着以一个简单的Web应用为例,Web应用中使用JTA的JSP页面代码如下:
程序清单:codes\04\4.2\txTest\test.jsp
<% //初始化Context,使用InitialContext初始化Context Context ctx = new InitialContext(); //通过JNDI查找第一个数据源 DataSource oracleDs = (DataSource)ctx.lookup("oracle"); //通过JNDI查找第二个数据源 DataSource otherDs = (DataSource)ctx.lookup("other"); //通过JDNI查找获取WebLogic服务器提供的事务管理器 UserTransaction tx = (UserTransaction)ctx .lookup("javax.transaction.UserTransaction"); //获取数据库连接 Connection oracleConn = oracleDs.getConnection(); Connection otherConn = otherDs.getConnection(); Statement oracleStmt = null; Statement otherStmt = null; //开始事务 tx.begin(); try { //获取Statement oracleStmt = oracleConn.createStatement(); otherStmt = otherConn.createStatement(); //下面语句可以插入成功 int result = oracleStmt.executeUpdate( "insert into dept values(50 , '研发部' , '广州')"); out.println(result == 1 ? "第一个数据库插入成功!" : "失败!"); //下面语句将失败(因为Oracle的dept表中已有主键为40的记录) otherStmt.executeUpdate( "insert into dept values(40 , '市场部' , '广州')"); //提交事务 tx.commit(); } catch(SQLException ex) { ex.printStackTrace(); //回滚事务 tx.rollback(); } finally { //关闭资源 oracleStmt.close(); otherStmt.close(); oracleConn.close(); otherConn.close(); } %>
上面页面代码中粗体字代码就是JTA事务控制的关键代码,要在WebLogic服务器中获取JTA事务管理器,只需查询到JNDI名为javax.transaction.UserTransaction的对象即可。
上面代码是一个典型的JTA事务控制逻辑,程序依次执行多个数据库访问(可能跨越不同数据库,如上面示例就跨越了两个数据库),当所有数据库访问都可以成功时,程序通过JTA控制全局事务提交;只要任何一个数据库的数据访问失败,JTA控制全局事务就回滚,通过这种方式即可处理跨越多个数据库的分布式事务,保证多个数据库的一致性。
访问上面页面,看到页面将会输出“第一个数据库插入成功!”,这表明第一个数据库的插入操作是成功的;但由于第二个数据库的插入操作失败,这将导致整个全局事务回滚,因此第一个数据库的dept表中依然没有主键为50的记录。
下面以JBoss服务器为例来介绍使用JTA控制全局事务。同样地,我们需要先为JBoss配置至少两个支持XA规范的数据源(两个以上的数据库才可体会到分布式事务处理),此时配置数据源要记得在图4.9中选择“XA Datasources”节点来配置XA数据源。接下来的步骤如下:
1 选择配置XA数据源的模板,如图4.13所示。
图4.13 选择配置XA数据源的模板
2 由于我们配置的数据源就是要连接到Oracle 10g服务器,因此这里选择“Oracle XA (XA Datasource)”列表项,然后单击“Continue”按钮即可看到如图4.14所示的页面。
3 在如图4.14所示页面中为数据源配置填写相关信息,记得添加一个名为URL的属性,该属性的属性值为连接Oracle数据库的URL,如jdbc:oracle:thin:@127.0.0.1:1521:ora10g。输入完成后,单击“Save”按钮即可。
图4.14 配置连接到Oracle的XA数据源
正如前面介绍的,上面配置步骤将会在JBoss服务器的server\default\deploy目录下生成一个oracle-ds.xml文件,我们将该文件复制一份,重命名为“other.xml”,并对该文件进行修改,修改后的新文件内容如下:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <datasources> <xa-datasource> <jndi-name>other</jndi-name> <rar-name>jboss-xa-jdbc.rar</rar-name> <use-java-context>true</use-java-context> <connection-definition>javax.sql.DataSource</connection-definition> <jmx-invoker-name>jboss:service=invoker,type=jrmp</jmx-invoker-name> <min-pool-size>1</min-pool-size> <max-pool-size>20</max-pool-size> <blocking-timeout-millis>30000</blocking-timeout-millis> <idle-timeout-minutes>30</idle-timeout-minutes> <prefill>false</prefill> <background-validation>false</background-validation> <background-validation-millis>0</background-validation-millis> <validate-on-match>true</validate-on-match> <no-tx-separate-pools/> <statistics-formatter>org.jboss.resource.statistic.pool .JBossDefaultSubPoolStatisticFormatter</statistics-formatter> <isSameRM-override-value>false</isSameRM-override-value> <allocation-retry>0</allocation-retry> <allocation-retry-wait-millis>5000</allocation-retry-wait-millis> <security-domain-and-application xsi:type="securityMetaData" xmlns:xsi="http://www.w3.org/2001/XML Schema-instance"/> <metadata> <type-mapping>Oracle9i</type-mapping> </metadata> <type-mapping>Oracle9i</type-mapping> <user-name>scott</user-name> <password>tiger</password> <exception-sorter-class-name>org.jboss.resource .adapter.jdbc.vendor.OracleExceptionSorter</exception-sorter-class-name> <prepared-statement-cache-size>0</prepared-statement-cache-size> <share-prepared-statements>false</share-prepared-statements> <set-tx-query-timeout>false</set-tx-query-timeout> <query-timeout>0</query-timeout> <use-try-lock>60000</use-try-lock> <xa-datasource-class>oracle.jdbc.xa.client.OracleXADataSource </xa- datasource-class> <xa-datasource-property name="URL"> jdbc:oracle:thin:@192.168.1.18:1521:orcl</xa-datasource-property> <xa-resource-timeout>0</xa-resource-timeout> </xa-datasource> </datasources>
从上面粗体字代码可以看出,该文件所需要改动的内容不同,只需要改动数据源的JNDI名、连接数据库的用户名、密码和URL即可。
至此,我们为JBoss服务器配置了两个数据源,这两个数据源连接到两台不同计算机上的Oracle数据库。类似地,我们也在JBoss服务器内部署一个Web应用,通过应用中如下JSP页面来示范通过JTA控制全局事务。
程序清单:codes\04\4.2\txTest.war\test.jsp
<% //初始化Context,使用InitialContext初始化Context Context ctx = new InitialContext(); //通过JNDI查找第一个数据源 DataSource oracleDs = (DataSource)ctx.lookup("java:/oracle"); //通过JNDI查找第二个数据源 DataSource otherDs = (DataSource)ctx.lookup("java:/other"); //通过JDNI查找获取JBoss服务器提供的事务管理器 UserTransaction tx = (UserTransaction)ctx .lookup("UserTransaction"); //获取数据库连接 Connection oracleConn = oracleDs.getConnection(); Connection otherConn = otherDs.getConnection(); Statement oracleStmt = null; Statement otherStmt = null; //开始事务 tx.begin(); try { //获取Statement oracleStmt = oracleConn.createStatement(); otherStmt = otherConn.createStatement(); //下面语句可以插入成功 int result = oracleStmt.executeUpdate( "insert into dept values(50 , '研发部' , '广州')"); out.println(result == 1 ? "第一个数据库插入成功!" : "失败!"); //下面语句将失败(因为Oracle的dept表中已有主键为40的记录) otherStmt.executeUpdate( "insert into dept values(40 , '市场部' , '广州')"); //提交事务 tx.commit(); } catch(SQLException ex) { ex.printStackTrace(); //回滚事务 tx.rollback(); } finally { //关闭资源 oracleStmt.close(); otherStmt.close(); oracleConn.close(); otherConn.close(); } %>
从上面粗体字代码可以看出,在JBoss服务器和WebLogic服务器中使用JTA控制全局事务的差别并不大,基本思路完全相同,只是WebLogic服务器中JTA事务管理器的JNDI名是javax.transaction. UserTransaction,而JBoss服务器中JTA事务管理器的JNDI名是UserTransaction。
使用浏览器浏览该页面,同样可以看到“输出第一个数据库插入成功!”字符串,但底层两个数据库的dept表中不会增加任何记录,因为第二条插入语句引发了异常,从而导致全局事务回滚了。