深入了解Spring的事务管理

Spring 的事务处理对于我们来说即带来了便利性,又因为它的黑盒性质,带来了使用和处理的不确定性,如果不能明白Spring事务管理的内在原理,那么迟早会出现一些Spring事务管理的bug.

业务背景

在新的跨境业务当中,我们需要同步业务的订单和订单对应的商品,由于上线很急,没有进行充分的单元测试。便带来了Spring事务的一个bug。

表结构(少部分)

  1. CREATE TABLE `outbound_order` (
  2. `order_no` varchar(64) NOT NULL DEFAULT '''''' COMMENT '订单号/出货单id',
  3. `user_id` varchar(64) DEFAULT NULL COMMENT '用户',
  4. `order_status` int(2) DEFAULT NULL COMMENT '操作状态',
  5. UNIQUE KEY `order_no_unique_index` (`order_no`,`user_id`)
  6. ) ENGINE=InnoDB AUTO_INCREMENT=419 DEFAULT CHARSET=utf8 COMMENT='出货单'
  7. CREATE TABLE `outbound_item` (
  8. `order_no` varchar(64) DEFAULT NULL COMMENT '订单',
  9. `ae_user_id` varchar(64) DEFAULT NULL COMMENT '用户',
  10. PRIMARY KEY (`id`)
  11. ) ENGINE=InnoDB AUTO_INCREMENT=440 DEFAULT CHARSET=utf8

在实际的定时任务中进行的处理如下

  1. @Scheduled(cron = "0 0 */2 * * *")
  2. public void sync(){
  3. //-----查询用户
  4. //同步用户订单
  5. addInfo(xxxx);
  6. }
  7. @Transactional
  8. public void addInfo(List<Order> orderList) {
  9. //查询订单状态,获取订单商品入库
  10. //处理订单对应的商品
  11. List<Item> itemList = xxxx(orderList);
  12. //处理订单
  13. orderDao.addOrderList(orderList);
  14. itemDao.addItemList(itemList);
  15. }

按照预期是要么订单和订单商品同时同步入库,或者同时失败。但是实际却是订单入库,但是订单对应的商品入库失败。

那么究竟是什么问题,首先看日志,发现是有数据没有通过接口返回而抛出了异常,但是为什么订单插入了,一时间发现是事务没有处理,但是他的类名却是OrderTaskEnchanerSpringByCglib说明
是进入了代理的,带着这一层去翻了以下源代码.

原理分析

根据 Spring#Aop 的处理方式直接定位 CglibAopProxy的DynamicAdvisedInterceptor#intercept 可以看到如下代码

  1. //expose-proxy = true,将proxy保存到ThreadLocal中
  2. if (this.advised.exposeProxy) {
  3. // Make invocation available if necessary.
  4. oldProxy = AopContext.setCurrentProxy(proxy);
  5. setProxyContext = true;
  6. }
  7. //找到切面结合
  8. List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
  9. Object retVal;
  10. //没有切面,直接使用target掉用,从而没有进入Spring的事务代码
  11. if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
  12. Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
  13. retVal = methodProxy.invoke(target, argsToUse);
  14. }else {
  15. // We need to create a method invocation...
  16. retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
  17. }

经过分析我们的 sync() 却是没有 @Transactional 注解,也就不存在 aop 切面了. 但是另一个疑问又出现了 addInfo 方法被 @Transactional 注解了啊
于是再次debug发现,这是 this 调用,根本就不是 proxy 代理 在调用,既然不是代理方法,那么肯定就不会进入aop的拦截当中了,既然如此肯定就没有事务处理了。

结合本次出现的事务故障,又结合几种出现的场景进行了分析.

场景1. 事务1同时调用2次dao方法,未发生异常

  1. @Transactional
  2. public Integer doSomething(Integer key) {
  3. PerformData performData = new PerformData();
  4. performDataDao.addPerformData(performData);
  5. performDataDao.addPerformData(performData);
  6. return 1;
  7. }

场景2. 事务1调用一次dao方法,内部再调用一次this.inner,不抛出异常

  1. @Transactional
  2. public Integer doSomething(Integer key) {
  3. PerformData performData = new PerformData();
  4. performDataDao.addPerformData(performData);
  5. this.inner(performData);
  6. return 1;
  7. }
  8. @Transactional(propagation = Propagation.REQUIRES_NEW)
  9. public void inner(performData) {
  10. performDataDao.addPerformData(performData);
  11. }

场景3. 场景同2,但是inner方法抛出异常且不处理

  1. @Transactional
  2. public Integer doSomething(Integer key) {
  3. PerformData performData = new PerformData();
  4. performDataDao.addPerformData(performData);
  5. this.inner(performData);
  6. return 1;
  7. }
  8. @Transactional(propagation = Propagation.REQUIRES_NEW)
  9. public void inner(PerformData performData) {
  10. performDataDao.addPerformData(performData);
  11. //抛出异常
  12. throw new RuntimeException();
  13. }

场景4. 场景同3,invoke方法被普获并且处理

  1. @Transactional
  2. public Integer doSomething(Integer key) {
  3. PerformData performData = new PerformData();
  4. performDataDao.addPerformData(performData);
  5. try {
  6. this.inner(performData);
  7. }catch (Exception e){
  8. //不抛出去
  9. }
  10. return 1;
  11. }
  12. @Transactional(propagation = Propagation.REQUIRES_NEW)
  13. public void inner(PerformData performData) {
  14. performDataDao.addPerformData(performData);
  15. throw new RuntimeException();
  16. }

this 调用,加不加 @Transactional 一样的,没有区别,都不会走代理,如何使内部方法的注解生效参见 ((PerformDataServiceImpl)AopContext.currentProxy()).inner(performData);

场景1结果:2次插入成功,可以理解
场景2结果:2次插入成功,可以理解
场景3结果:2次插入失败,异常被Spring普获,随后回滚。
场景4接口:2次插入成功,异常被上级方法普获,但是没有抛出去,Spring普获不到,直接commit

Spring 事务回滚源代码参考 TransactionAspectSupport#invokeWithinTransaction

  1. try {
  2. // This is an around advice: Invoke the next interceptor in the chain.
  3. // This will normally result in a target object being invoked.
  4. retVal = invocation.proceedWithInvocation();
  5. }
  6. catch (Throwable ex) {
  7. // target invocation exception
  8. completeTransactionAfterThrowing(txInfo, ex);//如果是RuntimeException|Error或者设定了RollbackFor就回滚,否则提交
  9. throw ex;//重新抛出
  10. }

小结

在这次业务出现的一些bug中还是学到一些基础的东西没有掌握完全,尤其是Spring相关,都还是只是看到了表面。尤其是@Transactional的标注位置和事务的处理息息相关。Service 和 Dao 类的设计也存在不合理,这种不合理在需要使用处理的时候会变的格外突出。

2018-11-09