使用事务时应该避免的陷井

标签: | 发表时间:2015-03-27 16:50 | 作者:hupy
出处:http://www.iteye.com

事务可实现“要么完全成功,要不全部不成功”,保证数据的完整性和一致性,使我们在开发中能方便地实现一些业务逻辑。比如,在股票交易时,除了记录交易的过程,还要更新交易完成之后的账户状态。这两个操作显然必须“要么完全成功,要么全部不成功”,否则,你的麻烦就大了。

当然,如果你不关心数据的完整性和一致性的问题,那么忘了事务吧,因为引入锁、数据库并发等机制之后,对性能还是有影响的。

下面代码中,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去掉吧。



已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐