Spring事务失效场景(干货)

326 阅读10分钟

前置知识点

​ spring事务中默认能捕获的异常要不是运行时异常或者它的子类,要不是error;但是如果是exception或者exception的子类 spring事务无法捕捉到;解决方案是在@Transactional中添加Rollback属性= Exception.class

image.png

注意: 以下带 ->后内容均是个人理解

事务失效场景

1. 方法内部调用

场景:在日常开发中,可能会遇见在某个ServiceImpl类中通过某个方法调用另外一个事务方法.这时候就会发生事务失效,因为Spring事务是通过Aop生成代理对象来完成开启和提交/回滚事务的,所以要想事务生效必须由代理对象来调用方法(事务--->Aop--->代理对象-->代理逻辑(1.开始事务 回滚 提交)).

​ 示例:图中submitOrder方法调用saveOrderDetail()方法时,是通过this去调用的(默认隐藏this 这里显示了)也就是当前对象调用并不是代理对象,所以会导致事务失效.

image.png

@Transactional
    @Override
    public String submitOrder(OrderVo orderVo) throws Exception {
        Order order = new Order();
        order.setId(1L);
        orderMapper.insert(order);
        saveOrderDetail(orderVo,order);
        return "提交订单成功";
    }


    @Transactional(rollbackFor = Exception.class)
    public void saveOrderDetail(OrderVo orderVo, Order order) {
        OrderDetail orderDetail = new OrderDetail();

        orderDetail.setOrderId(order.getId());
        orderDetail.setItemId(orderVo.getItemId());
        int insert1 = orderDetailMapper.insert(orderDetail);
        log.info("插入订单项操作:{}", insert1 > 0 ? "成功" : "失败");
    }

3种解决方案

  1. 自己注入自己 ServiceImpl注入ServiceImpl 来调用方法

    • 在平时开发过程中,一般是通过Service.方法()来调用ServiceImpl重写的方法也就是说ServiceImpl就是一个代理对象

    • @Slf4j
      @Service
      public class BusinessOpsServiceSupport implements BusinessOpsService {
      
      
          @Autowired
          private BusinessOpsServiceSupport businessOpsServiceSupport;
      
          @Autowired
          private OrderMapper orderMapper;
      
          @Autowired
          private OrderDetailMapper orderDetailMapper;
      
      
          @Autowired
          private TransactionBusinessService transactionBusinessService;
      
      
          @Transactional
          @Override
          public String submitOrder(OrderVo orderVo) throws Exception {
              Order order = new Order();
              order.setId(1L);
              orderMapper.insert(order);
              businessOpsServiceSupport.saveOrderDetail(orderVo,order);
              return "提交订单成功";
          }
          
          @Transactional(rollbackFor = Exception.class)
          public void saveOrderDetail(OrderVo orderVo, Order order) {
              OrderDetail orderDetail = new OrderDetail();
      
              orderDetail.setOrderId(order.getId());
              orderDetail.setItemId(orderVo.getItemId());
              int insert1 = orderDetailMapper.insert(orderDetail);
              log.info("插入订单项操作:{}", insert1 > 0 ? "成功" : "失败");
          }
      
    • 注意:

      • 使用这种方式会导致出现循环依赖,Spring默认支持开启循环依赖,而SpringBoot项目默认是关闭的,需要在配置文件中开启
      • spring.main.allow-circular-references=true
  2. 将该方法放入其他Service中

    • 这里通过注入其他Service调用就不会导致事务失效 本质还是生成了代理对象去调
  3. 通过AopContent类 ->最帅的方式

    • 通过aop获取当前impl的代理对象,通过代理对象去调用方法

image.png java @Transactional @Override public String submitOrder(OrderVo orderVo) throws Exception { //通过aop获取代理对象 BusinessOpsServiceSupport supportProxy = (BusinessOpsServiceSupport)AopContext.currentProxy(); Order order = new Order(); order.setId(1L); orderMapper.insert(order); supportProxy.saveOrderDetail(orderVo,order); return "提交订单成功"; }

注意:这种写法在springboot中一定要通过注解或者配置的形式将代理对象暴露出来

image.png

后续事务传播都使用这种方式来实现方法调用,避免出现事务失效

2.事务传播行为导致失效

A.REQUIRED(默认)

    • 官方解释:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务 ->常用

    • 场景:

      • 如果A、B两个方法都加了@Transactional注解,默认是REQUIRED传播行为。那么如果A方法调用B方法,它们会共用一个事务,因为默认会使用同一条连接,相当于一个事务里执行。->A,B异常都回滚

      • 但是如果A方法没有,B方法有@Transactional

      • 当A执行完sql并调用完B方法之后发生异常,不会导致A,B事务回滚**->A没事务,B 是新事务** 当A执行完sql并调用B方法后,B方法发生异常,只会导致B事务回滚**->A没事务,B事务检测异常回滚**

B.SUPPORTS

  • 官方解释:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。->从众,摆烂

    • 配置:@Transactional(propagation = Propagation.SUPPORTS)
  • 场景

      • 如果A方法存在事务(加了@Transactional注解)调B,且B方法开启了事务且配置了SUPPORTS传播行为,那么B方法会挂起自己的事务,加入到A方法的事务来执行。->A,B异常都回滚
      • 如果A方法没有事务(没有加@Transactional注解),B方法开启了事务且配置了SUPPORTS传播行为,那么B方法也会以非事务方式执行。-> A没事务,B和A一致,出现异常都不回滚

C.MANDATORY

  • 官方解释:如果当前存在事务,则加入该事务;如果当前没有事务,自己以事务的形式运行,但是会抛出异常。->抛出的是调用方不存在事务的异常 ->不摆烂 骂前任

  • 配置: @Transactional(propagation = Propagation.MANDATORY)

  • 场景

    • 如果A方法存在事务(加@Transactional注解),B方法配置了MANDATORY传播行为,那么B方法将加入到该存在的事务来执行。->A,B异常都回滚
    • 如果A方法没有事务(没有加@Transactional注解),B方法配置了MANDATORY传播行为,那么B方法将会抛出异常。但是B方法会开启一个事务来执行自己的业务。-> A没事务,B会强制开事务,B出现异常时会回滚,并抛出异常,表示A方法无事务,但A不会回滚
  • 异常:org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'

D.REQUIRES_NEW

  • 官方解释:创建一个新的事务,如果当前存在事务,则把当前事务挂起,在新事务提交后恢复。 ->翅膀硬了,小的犯事大的连带责任

    • 挂起:将之前的数据库连接从ThreadLocal中取出来,放到一个容器中
    • 恢复:在从容器中将刚刚放进去的数据库连接再次放到TreadLocal中去。
  • 举例: 如图->多个sql,先执行A方法sql1,调用B方法sql2时,会将当前数据库链接放到容器中,等sql2执行完时回调,然后会将A数据库链接取出执行sql3...

image.png

image.png

  • 配置:@Transactional(**propagation = Propagation.REQUIRES_NEW)

  • 场景:

    • 如果A方法存在事务(加@Transactional注解),B方法配置了REQUIRES_NEW传播行为,那么B方法将开启一个新的事务.->A异常,A回滚,B不回滚;但B异常,会向上抛出异常导致A一起回滚,NEW传播行为的特殊性,类似于将B方法的sql挪到A方法中

    • 如果A方法没有事务(没有加@Transactional注解),B方法配置了REQUIRES_NEW传播行为。B方法会开启一个事务来执行自己的业务。-> A没事务,B有事务,B出现异常时会回滚,A不会回滚.A异常,AB都不回滚

E.NOT_SUPPORTED

  • 官方解释:以非事务方式运行,如果当前存在事务,则把当前事务挂起。->摸鱼,摸鱼被发现 领导被处分上报/领导批评教育不上报

  • 配置:@Transactional(propagation = Propagation.NOT_SUPPORTED)

    • 场景:

      • 如果A方法没有事务(没有加@Transactional注解),B方法配置了NOT_SUPPORTED传播行为,那么B方法也会以非事务方式执行。-> AB都不回滚
      • 如果A方法存在事务(加了@Transactional注解),B方法配置了NOT_SUPPORTED传播行为,那么B方法会挂起当前的事务(A方法的事务),以非事务方式来执行。-> A异常,A回滚B不回滚;B异常,B不会回滚,会将异常上抛(调用方)导致A回滚/如果A捕获该异常,并做其它处理(非抛异常,如log记录)A就不会回滚

F.NEVER

  • 官方解释:以非事务方式运行,如果当前存在事务,则抛出异常。 -> 通知: 你有,我就有

  • 配置:**@Transactional(propagation = Propagation.NEVER)

    • 场景:

      • 如果A方法没有事务(没有加@Transactional注解),B方法配置了NEVER传播行为,那么B方法会正常以非事务方式执行。 ->AB都不会回滚
      • 如果A方法存在事务(加了@Transactional注解),B方法配置了NEVER传播行为,那么B方法将会抛出异常。但是B方法会和A方法共用同一个事务。也即同一个数据库连接。->当B发生异常时会和A共用一个事务,且还会抛出这个事务是never传播行为的异常,导致AB都回滚
  • 异常:org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'

###G.NESTED

  • 官方解释:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED ->缝合怪

  • 配置:**@Transactional(propagation = Propagation.**NESTED)

    • 场景:

      • 如果A方法存在事务(加@Transactional注解),B方法配置了NESTED传播行为,那么B方法将会在嵌套事务内执行。-> A,B异常都会回滚

      • 和REUIRES和REQUIRES_NEW的区别在哪?

        • 区别于REQUIRES_NEW(父异,子不回;子异,父回)
        • 但和REQUIRED的区别是在NESTED情况下,被调用方发生异常时,调用方可以catch其异常,这样只有子事务回滚,父事务不受影响(两个事务);而Requires是同一事务,catch了也会回滚
    • 如果A方法没有事务(没有加@Transactional注解),B方法配置了NESTED传播行为,那么B方法将启动一个新的嵌套事务执行。 -> A无事务,B开启新事务

  • 整理方便记忆

    • 支持现在的事务

      • REQUIRES ->常用
      • SUPPORTS ->从众,摆烂
      • MANDATORY -> 不摆烂,独立骂前任
    • 不支持现在的事务

      • REQUIRES_NEW -> 翅膀硬了,小的惹事大的连带责任
      • NOT_SUPPORTED -> 摸鱼,摸鱼被发现领导被处分上报/领导批评教育不上报
      • NEVER -> 通知:你有,我就有
    • 嵌套

      • NESTED -> 缝合怪

3.访问权限问题

  • 开发时一般从方法内抽取出来的方法默认为private,而要使用@Transactional必须要public

4.方法使用 final 或static 修饰

  1. Spring事务的底层其实是使用了AOP,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能
  2. 但是某个方法被final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能
  3. 而被static修饰的方法,属于静态方法,属于类,不属于实例对象,无法被代理

5.未被Spring管理 ->没加注解

6.多线程调用导致

  1. 众所周知Spring事务管理器要想对数据库进行io操作必须要从数据源拿到数据库连接才能执行操作,而拿到的数据库连接会放进ThreadLocal
  2. 所以在多线程环境下会导致方法A是数据库链接1,异步B方法从ThreadLocal中拿不到1,就只有重新去源拿链接2 这就会导致不在一个事务中
  3. 所以我们说的同一个事务,其实指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程中,拿到的数据库连接肯定是不一样的。所以事务也是不同的。

注意:在Spring源码中Spring事务是通过数据库的连接来实现的。底层通过使用ThreadLocal来进行存储,key是当前线程,value保存的是一个Map,Map的key又是数据源,value是数据库连接。

7.MyISAM存储引擎不支持事务

8.异常被自己吃掉了

  • 实际开发中,通过try.catch捕获到的异常,没有将异常抛出,选择通过log方式等打印出来不会导致事务回滚,那么如果想要Spring能够正常回滚,则必须要抛出它能够处理的异常。

image.png

  • 强制回滚

    • 如果想返回友好的信息给客户端,并且还想回滚 可以通过以下代码强制回滚

    • TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

9.手动抛出了别的异常

开发中自定义的异常如果没有实现RunTimeExceptionError,而是Exception(非运行时异常),不会回滚