第14章 事务
Dale Green著
JSP WU 译
一个典型的企业应用程序在一个或多个数据库中访问和存储信息。因为这些信息对于商业操作非常重要,它必须精确、实时、可靠。如果允许多个程序同时更新相同的数据,就会破坏数据的完整性。如果在一个商业交易处理过程中,部分数据被更新后系统崩溃也将破坏数据完整性。事务通过预防以上情况的发生确保数据的完整性。事务控制多个应用程序对数据库的并发操作。如果发生系统崩溃,事务确保恢复的数据崩溃前将保持一致。
本章内容:
什么是事务
容器管理事务
事务的属性
回滚容器管理事务
同步会话bean实例变量
容器管理事务中不允许使用的方法
Bean 管理事务
JDBC事务
JTA 事务
非提交返回事务
在Bean管理事务中不允许使用的方法
企业Bean事务摘要
事务超时
隔离级别
更新多个数据库
Web 组件事务
一.什么是事务
模拟一个商业交易,应用程序需要完成几个步骤。例如,一个财物应用程序,可能会将资金从经常性帐户(checking account)转到储蓄性账户(saving account),该交易的伪码表示如下:
begin transaction
debit checking account
credit savings account
update history log
commit transaction
三个步骤要么全部完成,要么一个都不做。否则数据完整性将被破坏。因为事务中的所有步骤被看作一个统一的整体,所以事务一般被定义为一个不可分割的工作单元。
结束事务有两种方法:提交或者回滚。当一个事务提交,数据修改被保存。如果事务中有一个步骤失败,事务就回滚,这个事务中的已经执行的动作被撤销。例如在上面的伪码中,如果在处理第二步的时候硬盘驱动器崩溃,事务的第一步将被撤销。尽管事务失败,数据的完整性不会被破坏,因为帐目仍然保持平衡。
前面伪码中,begin和commit标明了事务的界限。当设计一个企业Bean的时候,你要决定怎样通过容器管理或bean管理事务来指定事务界限。
二.容器管理事务
在容器管理事务的企业Bean中,EJB容器来设定事务界线。你能够在任何企业Bean中使用容器管理事务:会话Bean、实体Bean或者 Message-driven Bean。容器管理事务简化了开发,因为企业Bean不用编码来显式制定事务界限。代码不包括开始结束事务的语句。典型的,容器会在一个企业Bean的方法被调用前立即开始一个事务,在这个方法退出以前提交这个事务。每个方法都关联一个事务。在一个方法中不允许嵌套或多个的事务存在。容器管理事务不需要所有的方法都关联事务。当部署一个Bean时,通过设定部署描述符中的事务属性来决定方法是否关联事务和如何关联事务。
事务的属性
一个事务的属性控制了事务的使用范围。图 14-1说明了为什么控制事务的范围很重要。图中,method-A开始一个事务然后调用Bean-2中的method-B.它运行在method-A开始的事务中还是重新执行一个新的事务?结果要看method-B中的事务属性。
图 14-1 Transaction Scope
一个事务属性可能有下面的属性之一:
☆ Required
☆ RequiresNew
☆ Mandatory
☆ NotSupported
☆ Supports
☆ Never
Required
如果客户端正在一个运行的事务中调用一个企业Bean的方法,这个方法就在这个客户端的事务中执行。如果客户端不关联一个事务,这个容器在运行该方法前开始一个新的事务。
Required属性在许多事务环境中可以很好的工作,因此你可以把它作为一个默认值,至少可以在早期开发中使用。因为事务的属性是在部署描述符中声明的,在以后的任何时候修改它们都很容易。
RequiresNew
如果客户端在一个运行的事务中调用企业Bean的方法,容器的步骤是:
1.挂起客户端的事务
2.开始一个新的事务
3.代理方法的调用
4.方法完成后重新开始客户端的事务
如果客户端不关联一个事务,容器运行这个方法以前同样开始一个新的事务。如果你想保证该方法在任何时候都在一个新事物中运行,使用RequiresNew属性。
Mandatory
如果客户端在一个运行的事务中调用企业Bean的方法,这个方法就在客户端的事务中执行。如果客户端不关联事务,容器就抛出TransactionRequiredException 异常。
如果企业Bean的方法必须使用客户端的事务,那么就使用Mandatory属性。
NotSupported
如果客户端在一个运行的事务中调用企业Bean的方法,这个容器在调用该方法以前挂起客户端事务。方法执行完后,容器重新开始客户端的事务。
如果客户端不关联事务,容器在方法运行以前不会开始一个新的事务。为不需要事务的方法使用NotSupported属性。因为事务包括整个过程,这个属性可以提高性能。
Supports
如果客户端在一个运行的事务中调用企业Bean的方法,这个方法在客户端的事务中执行,如果这个客户端不关联一个事务,容器运行该方法前也不会开始一个新的事务。因为该属性使方法的事务行为不确定,你应该谨慎使用Supports属性。
Never
如果客户端在一个运行的事务中调用企业Bean的方法,容器将抛出RemoteException异常。如果这个客户端不关联一个事务,容器运行该方法以前不会开始一个新的事务。
Summary of Transaction Attributes(事务属性概要)
表 14-1 列出了事务属性的影响。事务T1和T2都被容器控制。T1是调用企业Bean方法的客户端的事务环境。在大多数情况下,客户端是其它的企业Bean。T2是在方法执行以前容器启动的事务。在表 14-1中,“None”的意思是这个商业方法不在容器控制事务中执行。然而,该商业方法中的数据库操作可能在DBMS管理控制的事务中执行。
Setting Transaction Attributes (设定事务属性)
因为事务属性被保存在配置描述符中,他们会在J2EE应用程序开发的几个阶段被改变:创建企业Bean,应用程序装配和部署。然而, 当创建这个Bean的时候指定它的属性是企业Bean开发者的责任。只有将该组件装配到一个更大的应用程序时可以由开发者修改该属性,而不要期待J2EE应用程序部署者来指定该事务属性。
表 14-1
事物属性和范围
|
事务属性
|
客户端事务
|
商业方法事务
|
Required
|
None
|
T2
|
T1
|
T1
|
RequiresNew
|
None
|
T2
|
T1
|
T2
|
Mandatory
|
None
|
error
|
T1
|
T1
|
NotSupported
|
None
|
None
|
T1
|
None
|
Supports
|
None
|
None
|
T1
|
T1
|
Never
|
None
|
None
|
T1
|
Error
|
你可以为整个企业Bean或者单个方法指定事务属性。如果你为整个企业Bean和它某个方法各指定一个事务属性,为该方法指定的事务属性优先。当为单个方法指定事务属性时,不同类型企业Bean的要求也不同。会话Bean需要为商业方法定义属性属性,但create方法不需要定义事务属性。实体Bean需要为商业方法、create方法、remove方法和查找(finder)方法定义事务属性。Message-driven Bean需要为onMessage方法指定属性事务,而且只能是Required和NotSupported其中之一。
容器管理事务的回滚
在以下两中情况下,事务将回滚。第一,如果产生一个系统异常,容器将自动回滚该事务。第二,通过调用EJBContext接口SetRollbackOnly方法,Bean方法通知容器回滚该事务。如果Bean抛出一个应用异常,事务将不会自动回滚,但可以调用SetRollbackOnly回滚。对于一个系统和应用的异常,参考第5章的处理异常一节。
下面这个例子的代码在j2eetorial/examples/src/bank目录下。在命令行窗口下进入j2eetutorial/examples目录执行ant bank命令编译这些代码,执行 ant create-bank-table命令创建要用到的表。一个BankApp.ear样本文件在j2eetutorial/examples/ears目录下。通过BnankEJB 实例的transferToSaving方法来说明setRollbackOnly方法的用法。如果余额检查出现负数,那么transferToSaving调用setRollBackOnly回滚事务并抛出一个应用程序异常(InsufficientBalanceException)。updateChecking和updateSaving 方法更新数据表。如果更新失败,这两个方法抛出SQLException异常而transgerToSaving方法抛出EJBException异常。因为EJBException是一个系统异常,它使容器事务自动回滚事务。TransferTuSaving 方法的代码如下:
public void transferToSaving(double amount) throws
InsufficientBalanceException {
checkingBalance -= amount;
savingBalance += amount;
try {
updateChecking(checkingBalance);
if (checkingBalance < 0.00) {
context.setRollbackOnly();
throw new InsufficientBalanceException();
}
updateSaving(savingBalance);
} catch (SQLException ex) {
throw new EJBException
("Transaction failed due to SQLException: "
+ ex.getMessage());
}
}
当一个容器回滚一个事务,它总是会撤消事务中已执行的SQL语句造成的数据改动。然而,仅仅在实体Bean中容器回滚才会改变Bean的实例变量(与数据库状态有关的字段)。(这是因为容器会自动调用实体Bean的ejbLoad方法,该方法从数据库中读入实例变量的值。)当发生回滚,会话Bean必须显式重新设置所有被事务改动过的实例变量。重设会话Bean的实例变量最简单的方法是实现SessionSynchronization接口。
同步会话Bean的实例变量
SessionSynchronization接口是可选的,它允许你在Bean的实例变量和它们在数据库中的相应值之间保持同步。容器会在事务的几个主要阶段调用SessionSynchronization接口的对应方法—afterBegin、beforeCompletion和afterCompletion。
AfterBegin方法通知Bean实例一个新的事务已经开始。容器在调用商业方法以前立即调用afterBegin方法。afterBegin方法是从数据库中读入实例变量值的最佳位置。例如,在BanBean类中,在afterBegin方法中从读入了CheckingBalance和savingBalance变量的值:
public void afterBegin() {
System.out.println("afterBegin()");
try {
checkingBalance = selectChecking();
savingBalance = selectSaving();
} catch (SQLException ex) {
throw new EJBException("afterBegin Exception: " +
ex.getMessage());
}
}
商业方法方法完成以后,容器调用beforeCompletion方法,不过仅仅是在事务提交以前。BeforeCompletion方法是会话Bean回滚事务的最后时机(通过调用setRollbackOnly方法).如果会话Bean还没有实例变量的值更新数据库,就在beforCompletion方法里实现。
afterCompletion方法指出事务已经完成。它只有一个布尔型的参数,true表示事务被正确提交false表示事务回滚。如果事务回滚,会话Bean可以在该方法中从数据库中重新读取它的实例变量值:
public void afterCompletion(boolean committed) {
System.out.println("afterCompletion: " + committed);
if (committed == false) {
try {
checkingBalance = selectChecking();
savingBalance = selectSaving();
} catch (SQLException ex) {
throw new EJBException("afterCompletion SQLException:
" + ex.getMessage());
}
}
}
容器管理事务中不允许使用的方法
你不应该调用可能干扰容器设置的事务界线的方法,下面列出了所有禁止的方法:
☆ java.sql.Connection接口的commit、setAutoCommit和rollback方法
☆ javax.ejb.EJBContext 接口的getUserTransaction方法
☆ javax.transaction.UserTransaction接口的所有方法
然而你可以在Bean管理事务中使用这些方法设置事务界限。
三.Bean管理事务
在一个Bean管理事务中,会话Bean或者Message-driven Bean是用代码显式设置事务界线的。实体Bean不能使用Bean管理事务,只能使用容器管理的事务。虽然容器管理事务Bean需要较少的代码,但它也有一个局限:方法执行时,它只能关联一个事务或不关联任何事务。如果这个局限使你Bean编码困难,你应该考虑使用Bean管理事务。(译者:实际上J2EE服务器不支持嵌套事物,那么Bean管理事务唯一的优点就是可以在一个方法中一次启动多个事务)
下面的伪码很好说明了Bean管理事对商业逻辑的紧密控制。通过检查各种条件,伪码决定是否在商业方法中启动或停止不同的事务。
begin transaction
...
update table-a
...
if (condition-x)
commit transaction
else if (condition-y)
update table-b
commit transaction
else
rollback transaction
begin transaction
update table-c
commit transaction
当为会话Bean或Message-driver Bean的Bean管理事务编码时,你必须决定是使用jdbc或者JTA事务。下面的内容论述了两种事务类型。
JDBC 事务
JDBC事务通过DBMS事务管理器来控制。你可能会为了使用会话Bean中的原有代码而采用JDBC事务将这些代码封装到一个事务中。使用JDBC事务,要调用java.sql.Connection接口的commit和rollback方法。事务启动是隐式的。一个事务的从最近的提交、回滚或连接操作后的第一个SQL的语句开始。(这个规则通常是正确的,但可能DBMS厂商的不同而不同)
代码资源
下面的例子在j2eetutorial/examples/src/ejb/warehouse目录下。在命令行窗口中进入j2eetutorial/examples目录执行ant bank命令编译这些源文件,执行ant create-warehouse-table命令创建要用到的表,一个样本WarehouseApp.ear文件在j2eetutorial/example/ears 目录下。
下面的代码来自WarehouseEJB例子,一个会话Bean通过使用Connection接口的方法来划定Bean管理事务界限。ship方法以调用名为con的连接对象的setAutoCommit方法开始,该方法通知DBMS不要自动提交每个SQL语句。接下来ship 方法更新order_item和inventory数据表。如果更新成功,这个事务就会被提交。如果出现异常,事务就回滚。
public void ship (String productId, String orderId, int
quantity) {
try {
con.setAutoCommit(false);
updateOrderItem(productId, orderId);
updateInventory(productId, quantity);
con.commit();
} catch (Exception ex) {
try {
con.rollback();
throw new EJBException("Transaction failed: " +
ex.getMessage());
} catch (SQLException sqx) {
throw new EJBException("Rollback failed: " +
sqx.getMessage());
}
}
}
JTA 事务
JTA是Java Transaction API 的缩写。这些API 允许你用独立于具体的事务管理器实现的方法确定事务界限。J2EE SDK 事务管理器通过Java事务服务(Java Transaction Service, JTS)实现。但是你的代码并不直接调用JTS中的方法,而是调用JTA方法来替代,JTA方法会调用底层的JTS实现。
JTA事务被J2EE 事务管理器管理。你可能需要使用一个JTA事务,因为它能够统一操作不同厂商的数据库。一个特定DBMS的事务管理器不能工作在不同种类的数据库上。然而J2EE事务管理器仍然有一个限制——它不支持嵌套事务。就是说,它不能在前一个事务结束前启动另一个事务。
下面例子的源代码在j2eetutorial/examples/src/ejb/teller目录下,在命令行窗口进入j2eetutorial/examples目录,执行ant teller命令编译这些源文件,执行ant create-bank-teller命令创建要用到的表。一个样本TellerApp.ear文件在j2eetutorial/examples/ears目录下。
要自己确定事务界限,可以调用javax.transaction.UserTransaction接口的begin、commit和rollback方法来确定事务界限(该接口只能在SessionBean中使用,实体Bean不允许使用用户自定义的)。下面选自TellerBean类的代码示范了UserTransaction的用法。begin和commit方法确定了数据库操作的事务界限,如果操作失败则调用rollback回滚事务并抛出EJBException异常。
public void withdrawCash(double amount) {
UserTransaction ut = context.getUserTransaction();
try {
ut.begin();
updateChecking(amount);
machineBalance -= amount;
insertMachine(machineBalance);
ut.commit();
} catch (Exception ex) {
try {
ut.rollback();
} catch (SystemException syex) {
throw new EJBException
("Rollback failed: " + syex.getMessage());
}
throw new EJBException
("Transaction failed: " + ex.getMessage());
}
}
非提交返回事务
使用Bean管理事务的无状态会话Bean在事务返回前必须提交或者返回事务,而有状态的会话Bean没有这个限制。
对于使用JTA事务的有状态会话Bean,Bean实例和事务的关联越过大量用户调用被保持,甚至被调用的每个商业方法都打开和关闭数据库连接,该市无关联也不断开,直到事务完成(或回滚)。
对于使用JDBC事务的有状态会话Bean,JDBC连接越过用户调用保持Bean和事务之间的关联。连接关闭,事务关联将被释放。
在Bean管理事务中不允许使用的方法
在Bean管理的事务中不能调用EJBContext接口的getRollbackOnly和setRollbackOnly方法,这两个方法只能在容器管理事务中被调用。在Bean管理事务中,应调用UserTransaction接口的getStatus和rollback方法。
四.企业Bean事务摘要
如果你不能确定怎么在企业Bean中使用事务,可以用这个小技巧:在Bean的部署描述符中,制定事务类型为容器管理,把整个Bean(所有方法)的事务属性设置为Required。大多数情况下,这个配置可以满足你的事务需求。
表14-2列出了不同类型的企业Bean所允许使用的事务类型。实体Bean只能使用容器管理事务,但可以在部署描述符中配置事务属性,并可以调用EJBContext接口的setRollbackOnly方法来回滚事务。
表 14-2
企业Bean
允许的事务类型
|
|
企业Bean
类型
|
容器管理事务
|
Bean管理事务
|
|
JTA
|
JDBC
|
|
实体Bean
|
Y
|
N
|
N
|
|
会话Bean
|
Y
|
Y
|
Y
|
|
Message-driven
|
Y
|
Y
|
Y
|
|
会话Bean既可以使用容器管理事务也可以使用Bean管理事务。Bean管理事务又有两种类型:JDBC事务和JTA事务。JDBC事务使用Connection接口的commit和rollback方法来划分事务界限。JTA事务使用UserTransaction接口的begin、commit和rollback方法来划分事务界限。
在Bean管理事务的会话Bean中,混合使用JTA事务和JDBC事务是可能的。但是我不推荐这样使用,因为这样会造成代码的调试和维护都很困难。
Message-driver Bean和会话Bean一样既可以使用容器管理事务也可以使用Bean管理事务。
五.事务超时
对于容器管理事务,事务超时间隔是通过设置default.properties文件中ransaction.timeout属性的值来确定的,该文件在J2EE SDK安装目录的config子目录下。如下例将事务超时间隔设置为5秒钟:
transaction.timeout=5
这样,当事务在5秒钟内还没有完成,容器将回滚该事务。
J2EE SDK安装后,超时间隔的缺省值为0,表示不计算超时,无论事务执行多长时间,除非异常出错回滚,一直等待事务完成。
只有使用容器管理事务的企业Bean才会受到transaction.timeout属性值的影响。Bean管理的JTA事务使用UserTransaction接口的setTransactionTimeout方法来设置事务超时间隔。
六.隔离级别
事务不仅保证事务界限内的数据库操作全部完成(或回滚)同时还隔离数据库更新语句。隔离级别描述被修改的数据对其他事物的可见度。
假如一个应用程序在事务中修改一个顾客的电话号码,在事务结束前另一个应用程序要读取该条记录的电话号码。那么第二个应用程序是读取修改过但还没提交的数据,还是读取未修改前的老数据呢?答案就取决于事务的隔离级别。如果事务允许其他程序读取未提交的数据,会因为不用等待事务结束而提高性能,同时也有一个缺点,如果事务回滚,其他应用程序读取的将是错误的数据。
容器管理持久性(CMP)的实体Bean的事务级别无法修改,它们使用DBMS的默认个理解别,通常是READ_COMMITTED。
Bean管理持久性(BMP)的实体Bean和两种会话Bean都可以通过在程序中调用底层DBMS提供的API来设置事务级别。例如,一个DBMS可能允许你如下调用setTransactionIsolation方法将隔离级别设置成可读取未提交数据:
Connection con;
...
con.setTransactionIsolation(TRANSACTION_READ_UNCOMMITTED);
不要在事务执行期间更改隔离级别,通常隔离级别的更改会引起DBMS产生一次隐式提交。因为隔离级别的控制会跟具体的DBMS厂商不同而不同,具体的信息请参考DBMS的文档。J2EE平台规范不包括隔离级别标准。
七.更新多个数据库
J2EE事务管理器控制着除了Bean管理的JDBC事务以外的所有企业Bean事务,它允许企业Bean在同一个事务中更新多个数据库。下面示范在单个事务中更新多个数据库的两个应用。
图14-2中,客户端调用Bean-A的商业方法,商业方法启动一个事务,更新数据库X和Y,Bean-A的商业方法有调用Bean-B的商业方法,Bean-B的商业方法更新数据库Z然后返回事务的控制权给Bean-A的商业方法,由Bean-A提交该事务。三个数据库的更新都在同一个事务中发生。
图 14-2 更新多个数据库
图14-3中,客户端调用Bean-A的商业方法,该商业方法启动一个事务并更新数据库X,然后调用另一个J2EE服务器中的Bean-B的方法,该方法更新数据库Y。J2EE服务器保证两个数据库的更新都在同一个事务中进行(笔者认为应该是第一个J2EE服务器的事务管理器管理整个事物)。
图 14-3 跨越J2EE服务器更新多个数据库
八.Web 组件事务
Web组件中划分事务界限可以使用java.sql.Connection接口和javax.transaction.UserTransaction接口中的任意一个。跟Bean管理事务的会话Bean使用一样的两个接口。这两个接口的使用方法参考前面几节的内容。Web组件事务的例子在第10章Servlet技术第四节共享信息的访问数据库小节讲述过。