annotation-driventransaction-manager="transactionManager"/>
transaction-manager属性保存一个对在Spring配置文件中定义的事务管理器bean的引用。
这段代码告诉Spring在应用事务拦截器时使用@Transaction注释。
如果没有它,就会忽略@Transactional注释,导致代码不会使用任何事务。
让基本的@Transactional注释在清单4的代码中工作仅仅是开始。
注意,清单4使用@Transactional注释时没有指定任何额外的注释参数。
我发现许多开发人员在使用@Transactional注释时并没有花时间理解它的作用。
例如,像我一样在清单4中单独使用@Transactional注释时,事务传播模式被设置成什么呢?
只读标志被设置成什么呢?
事务隔离级别的设置是怎样的?
更重要的是,事务应何时回滚工作?
理解如何使用这个注释对于确保在应用程序中获得合适的事务支持级别非常重要。
回答我刚才提出的问题:
在单独使用不带任何参数的@Transactional注释时,传播模式要设置为REQUIRED,只读标志设置为false,事务隔离级别设置为READ_COMMITTED,而且事务不会针对受控异常(checkedexception)回滚。
@Transactional只读标志陷阱
我在工作中经常碰到的一个常见陷阱是Spring@Transactional注释中的只读标志没有得到恰当使用。
这里有一个快速测试方法:
在使用标准JDBC代码获得Java持久性时,如果只读标志设置为true,传播模式设置为SUPPORTS,清单5中的@Transactional注释的作用是什么呢?
清单5.将只读标志与SUPPORTS传播模式结合使用—JDBC
@Transactional(readOnly=true,propagation=Propagation.SUPPORTS)
publiclonginsertTrade(TradeDatatrade)throwsException{
//JDBCCode...
}
当执行清单5中的insertTrade()方法时,猜一猜会得到下面哪一种结果:
A.抛出一个只读连接异常
B.正确插入交易订单并提交数据
C.什么也不做,因为传播级别被设置为SUPPORTS
是哪一个呢?
正确答案是B。
交易订单会被正确地插入到数据库中,即使只读标志被设置为true,且事务传播模式被设置为SUPPORTS。
但这是如何做到的呢?
由于传播模式被设置为SUPPORTS,所以不会启动任何事物,因此该方法有效地利用了一个本地(数据库)事务。
只读标志只在事务启动时应用。
在本例中,因为没有启动任何事务,所以只读标志被忽略。
如果是这样的话,清单6中的@Transactional注释在设置了只读标志且传播模式被设置为REQUIRED时,它的作用是什么呢?
清单6.将只读标志与REQUIRED传播模式结合使用—JDBC
@Transactional(readOnly=true,propagation=Propagation.REQUIRED)
publiclonginsertTrade(TradeDatatrade)throwsException{
//JDBCcode...
}
执行清单6中的insertTrade()方法会得到下面哪一种结果呢:
A.抛出一个只读连接异常
B.正确插入交易订单并提交数据
C.什么也不做,因为只读标志被设置为true
根据前面的解释,这个问题应该很好回答。
正确的答案是A。
会抛出一个异常,表示您正在试图对一个只读连接执行更新。
因为启动了一个事务(REQUIRED),所以连接被设置为只读。
毫无疑问,在试图执行SQL语句时,您会得到一个异常,告诉您该连接是一个只读连接。
关于只读标志很奇怪的一点是:
要使用它,必须启动一个事务。
如果只是读取数据,需要事务吗?
答案是根本不需要。
启动一个事务来执行只读操作会增加处理线程的开销,并会导致数据库发生共享读取锁定(具体取决于使用的数据库类型和设置的隔离级别)。
总的来说,在获取基于JDBC的Java持久性时,使用只读标志有点毫无意义,并会启动不必要的事务而增加额外的开销。
使用基于ORM的框架会怎样呢?
按照上面的测试,如果在结合使用JPA和Hibernate时调用insertTrade()方法,清单7中的@Transactional注释会得到什么结果?
清单7.将只读标志与REQUIRED传播模式结合使用—JPA
@Transactional(readOnly=true,propagation=Propagation.REQUIRED)
publiclonginsertTrade(TradeDatatrade)throwsException{
em.persist(trade);
returntrade.getTradeId();
}
清单7中的insertTrade()方法会得到下面哪一种结果:
A.抛出一个只读连接异常
B.正确插入交易订单并提交数据
C.什么也不做,因为readOnly标志被设置为true
正确的答案是B。
交易订单会被准确无误地插入数据库中。
请注意,上一示例表明,在使用REQUIRED传播模式时,会抛出一个只读连接异常。
使用JDBC时是这样。
使用基于ORM的框架时,只读标志只是对数据库的一个提示,并且一条基于ORM框架的指令(本例中是Hibernate)将对象缓存的flush模式设置为NEVER,表示在这个工作单元中,该对象缓存不应与数据库同步。
不过,REQUIRED传播模式会覆盖所有这些内容,允许事务启动并工作,就好像没有设置只读标志一样。
这令我想到了另一个我经常碰到的主要陷阱。
阅读了前面的所有内容后,您认为如果只对@Transactional注释设置只读标志,清单8中的代码会得到什么结果呢?
清单8.使用只读标志—JPA
@Transactional(readOnly=true)
publicTradeDatagetTrade(longtradeId)throwsException{
returnem.find(TradeData.class,tradeId);
}
清单8中的getTrade()方法会执行以下哪一种操作?
A.启动一个事务,获取交易订单,然后提交事务
B.获取交易订单,但不启动事务
正确的答案是A。
一个事务会被启动并提交。
不要忘了,@Transactional注释的默认传播模式是REQUIRED。
这意味着事务会在不必要的情况下启动。
根据使用的数据库,这会引起不必要的共享锁,可能会使数据库中出现死锁的情况。
此外,启动和停止事务将消耗不必要的处理时间和资源。
总的来说,在使用基于ORM的框架时,只读标志基本上毫无用处,在大多数情况下会被忽略。
但如果您坚持使用它,请记得将传播模式设置为SUPPORTS(如清单9所示),这样就不会启动事务:
清单9.使用只读标志和SUPPORTS传播模式进行选择操作
@Transactional(readOnly=true,propagation=Propagation.SUPPORTS)
publicTradeDatagetTrade(longtradeId)throwsException{
returnem.find(TradeData.class,tradeId);
}
另外,在执行读取操作时,避免使用@Transactional注释,如清单10所示:
清单10.删除@Transactional注释进行选择操作
publicTradeDatagetTrade(longtradeId)throwsException{
returnem.find(TradeData.class,tradeId);
}
REQUIRES_NEW事务属性陷阱
不管是使用SpringFramework,还是使用EJB,使用REQUIRES_NEW事务属性都会得到不好的结果并导致数据损坏和不一致。
REQUIRES_NEW事务属性总是会在启动方法时启动一个新的事务。
许多开发人员都错误地使用REQUIRES_NEW属性,认为它是确保事务启动的正确方法。
考虑清单11中的两个方法:
清单11.使用REQUIRES_NEW事务属性
@Transactional(propagation=Propagation.REQUIRES_NEW)
publiclonginsertTrade(TradeDatatrade)throwsException{...}
@Transactional(propagation=Propagation.REQUIRES_NEW)
publicvoidupdateAcct(TradeDatatrade)throwsException{...}
注意,清单11中的两个方法都是公共方法,这意味着它们可以单独调用。
当使用REQUIRES_NEW属性的几个方法通过服务间通信或编排在同一逻辑工作单元内调用时,该属性就会出现问题。
例如,假设在清单11中,您可以独立于一些用例中的任何其他方法来调用updateAcct()方法,但也有在insertTrade()方法中调用updateAcct()方法的情况。
现在如果调用updateAcct()方法后抛出异常,交易订单就会回滚,但帐户更新将会提交给数据库,如清单12所示:
清单12.使用REQUIRES_NEW事务属性的多次更新
@Transactional(propagation=Propagation.REQUIRES_NEW)
publiclonginsertTrade(TradeDatatrade)throwsException{
em.persist(trade);
updateAcct(trade);
//exceptionoccurshere!
Traderolledbackbutaccountupdateisnot!
...
}
之所以会发生这种情况是因为updateAcct()方法中启动了一个新事务,所以在updateAcct()方法结束后,事务将被提交。
使用REQUIRES_NEW事务属性时,如果存在现有事务上下文,当前的事务会被挂起并启动一个新事务。
方法结束后,新的事务被提交,原来的事务继续执行。
由于这种行为,只有在被调用方法中的数据库操作需要保存到数据库中,而不管覆盖事务的结果如何时,才应该使用REQUIRES_NEW事务属性。
比如,假设尝试的所有股票交易都必须被记录在一个审计数据库中。
出于验证错误、资金不足或其他原因,不管交易是否失败,这条信息都需要被持久化。
如果没有对审计方法使用REQUIRES_NEW属性,审计记录就会连同尝试执行的交易一起回滚。
使用REQUIRES_NEW属性可以确保不管初始事务的结果如何,审计数据都会被保存。
这里要注意的一点是,要始终使用MANDATORY或REQUIRED属性,而不是REQUIRES_NEW,除非您有足够的理由来使用它,类似审计示例中的那些理由。
回页首
事务回滚陷阱
我将最常见的事务陷阱留到最后来讲。
遗憾的是,我在生产代码中多次?
?
?
到这个错误。
我首先从SpringFramework开始,然后介绍EJB3。
到目前为止,您研究的代码类似清单13所示:
清单13.没有回滚支持
@Transactional(propagation=Propagation.REQUIRED)
publicTradeDataplaceTrade(TradeDatatrade)throwsException{
try{
insertTrade(trade);
updateAcct(trade);
returntrade;
}catch(Exceptionup){
//logtheerror
throwup;
}
}
假设帐户中没有足够的资金来购买需要的股票,或者还没有准备购买或出售股票,并抛出了一个受控异常(例如FundsNotAvailableException),那么交易订单会保存在数据库中吗?
还是整个逻辑工作单元将执行回滚?
答案出乎意料:
根据受控异常(不管是在SpringFramework中还是在EJB中),事务会提交它还未提交的所有工作。
使用清单13,这意味着,如果在执行updateAcct()方法期间抛出受控异常,就会保存交易订单,但不会更新帐户来反映交易情况。
这可能是在使用事务时出现的主要数据完整性和一致性问题了。
运行时异常(即非受控异常)自动强制执行整个逻辑工作单元的回滚,但受控异常不会。
因此,清单13中的代码从事务角度来说毫无用处;尽管看上去它使用事务来维护原子性和一致性,但事实上并没有。
尽管这种行为看起来很奇怪,但这样做自有它的道理。
首先,不是所有受控异常都是不好的;它们可用于事件通知或根据某些条件重定向处理。
但更重要的是,应用程序代码会对某些类型的受控异常采取纠正操作,从而使事务全部完成。
例如,考虑下面一种场景:
您正在为在线书籍零售商编写代码。
要完成图书的订单,您需要将电子邮件形式的确认函作为订单处理的一部分发送。
如果电子邮件服务器关闭,您将发送某种形式的SMTP受控异常,表示邮件无法发送。
如果受控异常引起自动回滚,整个图书订单就会由于电子邮件服务器的关闭全部回滚。
通过禁止自动回滚受控异常,您可以捕获该异常并执行某种纠正操作(如向挂起队列发送消息),然后提交剩余的订单。
使用Declarative事务模式(本系列的第2部分将进行更加详细的描述)时,必须指定容器或框架应该如何处理受控异常。
在SpringFramework中,通过@Transactional注释中的rollbackFor参数进行指定,如清单14所示:
清单14.添加事务回滚支持—Spring
@Transactional(propagation=Propagation.REQUIRED,rollbackFor=Exception.class)
publicTradeDataplaceTrade(TradeDatatrade)throwsException{
try{
insertTrade(trade);
updateAcct(trade);
returntrade;
}catch(Exceptionup){
//logtheerror
throwup;
}
}
注意,@Transactional注释中使用了rollbackFor参数。
这个参数接受一个单一异常类或一组异常类,您也可以使用rollbackForClassName参数将异常的名称指定为JavaString类型。
还可以使用此属性的相反形式(noRollbackFor)指定除某些异常以外的所有异常应该强制回滚。
通常大多数开发人员指定Exception.class作为值,表示该方法中的所有异常应该强制回滚。
在回滚事务这一点上,EJB的工作方式与SpringFramework稍微有点不同。
EJB3.0规范中的@TransactionAttribute注释不包含指定回滚行为的指令。
必须使用SessionContext.setRollbackOnly()方法将事务标记为