事务可实现“要么完全成功,要不全部不成功”,保证数据的完整性和一致性,使我们在开发中能方便地实现一些业务逻辑。比如,在股票交易时,除了记录交易的过程,还要更新交易完成之后的账户状态。这两个操作显然必须“要么完全成功,要么全部不成功”,否则,你的麻烦就大了。
当然,如果你不关心数据的完整性和一致性的问题,那么忘了事务吧,因为引入锁、数据库并发等机制之后,对性能还是有影响的。
下面代码中,placeTrade是一个完整的业务逻辑单元LUW(Logical units of work),实现记录交易并更新账户的操作。
- public class TradingServiceImpl {
- @PersistenceContext(unitName="trading") EntityManager em;
- public long insertTrade(TradeData trade) throws Exception {
- em.persist(trade);
- return trade.getTradeId();
- }
- public TradeData placeTrade(TradeData trade) throws Exception {
- try {
- insertTrade(trade);
- updateAcct(trade);
- return trade;
- } catch (Exception up) {
- //log the error
- throw up;
- }
- }
- }
这里使用了JPA,没有使用事务。
上面的代码没有问题,但是在执行的时候,却会发现insertTrade并没有返回预期的交易编号,而是返回0,并且没有任何异常。这就是这个文章提到的第一个陷井:ORM框架需要使用事务来同步对象缓存和数据库。上面的代码没有使用事务,而且也没有显式地调用Flush,因此,在insert操作之后,数据并没有被保存到数据库中,因此,后面的更新操作也就不可能正确。
好,我们使用spring来管理事务,通过Annotation使上面的代码具有事务的能力。
- public class TradingServiceImpl {
- @PersistenceContext(unitName="trading") EntityManager em;
- @Transactional
- public long insertTrade(TradeData trade) throws Exception {
- em.persist(trade);
- return trade.getTradeId();
- }
- }
在加上@TRansactional之后,你会发现,事务还是没有被引入。这就是第二个陷井:使用Annotation引入事务,需要在Spring配置文件中添加<tx:annotation-driven transaction-manager="transactionManager"/>. 看到了吧,你还没有告诉Spring用哪个transactionManager来管理事务,Spring怎么会知道如何处理事务呢?你以为Spring是傻瓜相机吗?
但是仅仅使用@Transactional是远远不够的,很多时候,即使你的代码中加上了@Transactional,事务还是不起作用,原因就是你没有指定事务参数。
默认的@Transactional如果没有设定参数时,其propagation模式是REQUIRED, read-only标记是false。isolation level是READ_COMMITTED,这时,如果你的代码中抛出异常,而且又被你通过try catch捕获的话,事务是不回滚的。
好,我们知道了光用Annotation是不够的,还需要加上一些参数,那么,你已经很了解事务了吗?我们再做下面的测试:
- @Transactional(readOnly = true, propagation=Propagation.SUPPORTS)
- public long insertTrade(TradeData trade) throws Exception {
- // JDBC CODE
- }
上面代码中,我们设置read-only标记为true,也就是说事务为只读,但是却要进行插入操作。上面代码执行结果如何呢?
答案是会正确执行。But why????
因为这里我们用了JDBC操作,没有用ORM,而且propagation设为SUPPORTS, 这样Spring是不会创建一个事务的,而会将事务相关工作委托给数据库的事务。而只有Spring开始了一个事务,read-only才起作用。这里没有事务,因此,read-only就被忽略了。
再看下面的代码:
- @Transactional(readOnly = true, propagation=Propagation.REQUIRED)
- public long insertTrade(TradeData trade) throws Exception {
- // JDBC CODE
- }
相同的代码,我们把propagation设为REQUIRED,那么这时执行结果会怎么样呢? 答案是会抛出一个read only connection exception。为什么?自己想想吧。
如果对于只读的一些查询操作使用readonly标记会怎么样?考虑下面代码。
- @Transactional(readOnly = true)
- public TradeData getTrade(long tradeId) throws Exception {
- return em.find(TradeData.class, tradeId);
- }
问题是:getTrade的执行会在一个事务中吗? 从字面上看,好像不会,因为只有查询操作,而且read-only。不幸地是,你错了。你忘了propagation的默认设置值是REQUIRED,因此缺省状态下,上面代码也会启动一个事务,执行完后COMMIT。不需要却还是用上了事务,性能肯定受影响。所以,正确的方法是read-only的同时还要指定propagation=SUPPORTS,更好的方式是:查询操作根本不需要使用事务。
关于Propagation.REQUIRES_NEW的陷井
有时候,开发人员搞不清楚REQUIRES_NEW和REQUIRED的区别,误用了REQUIRES_NEW而造成很难发现的问题。比如下面代码:
- @Transactional(propagation=Propagation.REQUIRES_NEW)
- public long insertTrade(TradeData trade) throws Exception {...}
- @Transactional(propagation=Propagation.REQUIRES_NEW)
- public void updateAcct(TradeData trade) throws Exception {...}
- @Transactional(propagation=Propagation.REQUIRES_NEW)
- public long insertTrade(TradeData trade) throws Exception {
- insertTrade(trade);
- updateAcct(trade);
- //exception occurs here! Trade rolled back but account update is not!
- ...
- }
问题是:如果updateAcct发生异常,insertTrade操作会回滚吗? 答案是:不会。Propagation.REQUIRES_NEW会挂起当前的事务,新建一个独立的事务,执行完之后,提交并返回,激活先前挂起的事务继续执行。此时发生异常回滚只能回滚第一个事务中的逻辑,而前面独立事务已经COMMITTED了,抓瞎了吧。记录了交易,却没有更新账户状态,哈哈。解决办法是:将REQUIRES_NEW改为REQUIRED。这样,如果方法发现自己已经处在一个事务中了,就不会重新启动一个事务了。
关于Roll back的陷井
看下面的代码:
- @Transactional(propagation=Propagation.REQUIRED)
- public TradeData placeTrade(TradeData trade) throws Exception {
- try {
- insertTrade(trade);
- updateAcct(trade);
- return trade;
- } catch (Exception up) {
- //log the error
- throw up;
- }
- }
怎么样,设置了使用事务,而且REQUIRED强调必须采用事务,这回应该没问题了吧。
你又错了。
在updateAcct时,如果发现该账户根本没有足够的钱来做这个交易,应该回滚,但是,不幸的是,回滚没有发生。原因就在:你自己捕获并处理了Exception. 懊恼吧,我自己受累多写了代码,还有错了?
没错。这可能是使用事务方面最大的一个问题:运行时异常(未捕获的异常)会自动强迫整个事务逻辑单元(LUW)回滚,但是被捕获的异常则不会。
怎么样,出力不讨好了吧。赶紧把try catch去掉吧。