开发者

解决Spring事务@Transactional多层嵌套失效问题

目录
  • 场景
  • 一、Spring事务实现方式及原理
  • 二、事务失效原因
    • 2.1 数据库本身不支持事物
    • 2.2 方法不是Public
    • 2.3 未被 Spring 管理的Bean
    • 2.4 当前类的调用
    • 2.5 配置的事物传播性有问题
    • 2.6 异常被你 "抓住"了
    • 2.7 rollbackFor 异常指定错误
  • 三、Spring的事务传播行为
    • 四、解决思路
      • 五、采取方法
        • 总结

          场景

          在 AService 中,我会直接调用 A 的数据操作层去操作 A的数据 以及 A关联密切的其它数据,在操作完之后,会去调用 BService 和 CService 中更新对应的数据,并在每个方法上使用了事务,但在调用 BService 或者 CService 时候出现了异常,此时出现异常的BService 或者 CService 中数据没有改变,回滚了。

          但在 AService 中调用的 update 方法和出现异常前已经执行完的方法执行成功并且没有回滚。

          伪代码如下:

          1、AService实现类

          @Service
          @Slf4j
          public class AServiceImpl implements IAService {
              private final IBService bService;
              private final ICService cService;
              public AServiceImpl(IBService bService, ICService cService) {
                  this.bService = bService;
                  this.cService = cService;
              }
              
              @Override
              @Transactional(rollbackFor = Exception.class)
              public String modifyA(TestAParam param) throws BaseException {
                  // 兜底:入参空字段校验
                  this.judgeNull(param);
          
                  TestModifyParam modifyParam = param.getModifyParam();
          
                  String aCode = update(param, Boolean.TRUE);
          
                  if (null != modifyParam.getStatus() && TestStatusConstant.EDITED.equals(modifyParam.getStatus())){
                      // 保存B信息
                      bService.saveInfo(param, aCode);
                      // 保存C信息
                      cService.saveInfo(param, aCode);
                  }
          
                  // 更新A数据关联其它数据
                  if (StringUtils.isNotBlank(aCode)){
                      setOtherData(param);
                  }
          
                  return aCode;
              }
          
              @Transactional(rollbackFor = Exception.class)
              public void setOtherData(TestAParam param) throws BaseException {
                  // 其它关联数据处理
              }
          
              @Transactional(rollbackFor = Exception.class)
              public String update(TestAParam param, Boolean directlyFlag) throws BaseException {
                  // 更新处理
                 return null;
              }
          
              @Transactional(rollbackFor = Exception.class)
              public void judgeNull(TestAParam param) throws BaseException {
                  // 参数空校验与提醒处理
              }
          
          }

          2、BService 和 CService 实现类(CService实现类异常处理差不多相同)

          @Service
          @Slf4j
          public class BServiceImpl implements IBService {
              @Override
              @Transactional(rollbackFor = Exception.class)
              public void saveInfo(TestAParam param, String aCode) throws BaseException {
                  // 数据校验
          
                  // 模拟代码,出现不匹配数据错误情况会抛出异常
                  if (Objects.isNull(param)){
                      throw new BaseException(CodeEnum.FAILED.getCode(),"BServiceImpl saveInfo param mistake");
                  }
          
                  // 其它操作
          
              }
          }

          一、Spring事务实现方式及原理

          Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring 是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过 binlog 或者 redo log 实现的。

          一般我们在程序里面使用的都是在方法上面加 @Transaction 注解,这种属于声明式事物

          声明式事务本质是通过 AOP 功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

          二、事务失效原因

          2.1 数据库本身不支持事物

          这里以 mysql 为例,其 MyISAM 引擎是不支持事务操作的,InnoDB 才是支持事务的引擎,一般要支持事务都会使用 InnoDB。

          2.2 方法不是Public

          注解 @Transactional 只能放在 public 修饰的方法上才起作用private 方法是不会被spring代理的)因此是不会有事物产生的,这种做法是无效的。

          2.3 未被 Spring 管理的Bean

          没有被spring管理的bean, spring连代理对象都无法生成,事务自然是无效的。

          2.4 当前类的调用

          @Service
          public class UserServiceImpl implements UserService {
          
              public void update(User user) {
                  updateUser(user);
              }
          
              @Transactional(rollbackFor = Exception.class)
              public void updateUser(User user) {
                  // update user
              }
          
          }

          上面的这种情况下是不会有事物管理操作的。

          通过看声明式事物的原理可知,spring使用的是AOP切面的方式本质上使用的是动态代理来达到事物管理的目的,当前类调用的方法上面加 @Transactional 这个是没有任何作用的,因为调用这个方法的是this。

          再看下面的一种例子:

          @Service
          public class UserServiceImpl implements UserService {
          
              @Transactional(rollbackFor = Exception.class)
              public void update(User user) {
                  updateUser(user);
              }
          
              @Transactional(propagation = Propagation.REQUIRES_NEW)
              public void updateUser(User user) {
                  // update user
              }
          
          }

          这次在 update 方法上加了 @Transactional,updateUser 加了 REQUIRES_NEW 新开启一个事务,那么新开的事务管用么?

          答案是:不管用!

          因为它们发生了自身调用,就调该类自己的方法,而没有经过 Spring 的代理类,默认只有在外部调用事务才会生效,这也是老生常谈的经典问题了。

          还有就是在没有指定事务传播行为时,从源码中可以看到默认是使用 Propagation.REQUIRED。

          解决Spring事务@Transactional多层嵌套失效问题

          2.5 配置的事物传播性有问题

          @Service
          public class UserServiceImpl implements UserService {
          
              @Transactional(propagation = Propagation.NOT_SUPPORTED)
              public void update(User user) {
                  // update user
              }    
          }

          2.6 异常被你 "抓住"了

          @Service
          public class UserServiceImpl implements UserService {
          
              @Transactional(rollbackFor = Exception.class)
              public void update(User user) {
          
                try{
                  // update user
                }catch(Execption e){
                   log.error("异常",e)
                }
              }    
          }

          异常被抓了,这样子代理类就没办法知道你到底有没有错误,需不需要回滚,所以这种情况也是没办法回滚。

          2.7 rollbackFor 异常指定错误

          @Service
          public class UserServiceImpl implements UserService {
          http://www.devze.com
              @Transactional
              public void update(User user) {
                  // update user
              }    
          }

          上面这种没有指定回滚异常,这个时候默认的回滚异常是 RuntimeException ,如果出现其他异常那么就不会回滚事物。

          三、Spring的事务传播行为

          Spring 事务的传播行为说的是,当多个事务同时存在的时候, Spring 如何处理这些事务的行为。

          类型说明
          PROPAGATION_REQUIRED如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
          PROPAGATION_SUPPORTS支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
          PROPAGATION_MANDATORY支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
          PROPAGATION_REQUIRES_NEW创建新事务,无论当前存不存在事务,都创建新事务。
          PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
          PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
          PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按 REQUIRED 属性执行。

          当传播行为设置了PROPAGATION_NOT_SUPPORTED,PROPAGATION_NEVER,PROPAGATION_SUPPORTS这三种时,就有可能存在事物不生效。

          四、解决思路

          1、声明式事务处理,采用合适的事务传播行为,将AService中修改A中数据方法update和更新A数据关联其它数据方法setOtherData通过AppContext.getBean()的方式直接获取AService中的方法,不违背事务失效原因中的2.4项(当前类的调用),并将保持B和C信息的方法单独抽取出来,提供一个saveBandCInfo方法,加上事务处理和传播行为,并抛出异常信息。

          2、使用编程式事务管理,手动配置事务边界,确保modifyA中所有方法在事务中执行。

          五、采取方法

          由于AService中的方法 modifyA 调用链路比较长(业务急需处理好),如果使用声明式事务处理,改动起来是比较大的,中间链路可能会存在事务传播行为失效的情况,此时使用编程式事务管理解决就会很明显轻松解决。(这方法并不建议大家日常使用,建议使用声明事务更好点)

          伪代码如下:

          @Service
          @Slf4j
          public class AServiceImpl implements IAService {
              private final IBService bService;
              private final ICService cService;
              private final PlatformTransactionManager transactionManager;
              public AServiceImpl(IBService bService, ICService 编程客栈cService, PlatformTransactionManager transactionManager) {
                  this.bService = bService;
                  this.cService = cService;
                  this.transactionManager = transactionManager;
              }
              
              @Override
              @Transactional(rollbackFor = Exception.class)
              public String modifyA(TestAParam param) throws BaseException {
                  // 兜底:入参空字段校验
                  this.judgeNull(param);
                  // 编程式事务
                  DefaultTransactionDefinition def = new DefaultTransactionDefinition();
                  TransactionStatus status = transactionManager.getTransaction(def);
                  try {
                      TestModifyParam modifyParam = param.getModifyParam();
                      String aCode = update(param, Boolean.TRUE);
                      if (null != modifyParam.getStatus() && TestStatusConstant.EDITED.equals(modifyParam.getStatus())){
                          // 保存B信息
                          bSepythonrvice.saveInfo(param, aCode);
                          // 保存C信息
                          cService.saveInfo(param, aCode);
                      }
                      // 更新A数据关联其它数据
                      if (StringUtils.isNotBlank(aCode)){
                          setOtherData(param);
                      }
                      // 提交事务
                      transactionManager.commit(status);
                      return aCode;
                  } catch (Exception e) {
                      // 回滚事务
                      transactionManager.rollback(status);
                      // 处理异常或根据需要重新抛出异常
                      throw new BaseException(CodeEnum.FAILED.getCode(),"modifyA error message is {}", e.getMessage());
                  }
                      
              }
          
              @Transactional(rollbackFor = Exception.class)
              public void setOtherData(TestAParam productStrategyDO) throws BaseException {
                  // 其它关联数据处理
              }
          
              @Transactional(rollbackFor = Exception.class)
              public String update(TestAParam param, Boolean directlyFlag) throws BaseException {
                  // 更新处理
                  return null;
              }
          
              @Transactional(rollbackFor = Exception.class)
              public void judgeNull(TestAParam param) throws BaseException {
                  // 参数空校验与提醒处理
              }
          

          上述代码使用了 PlatformTransajavascriptctionManager接口的实现来手动管理事务。

          在代码中,我们首先获取 transactionManager的实例,然后使用该实例手动创建事务定义和事务状态。在try块中执行upd编程客栈ate方法和其它方法,并在最后根据执行情况手动提交或回滚事务。

          通过这种方式,可以确保update方法的操作在事务中进行,且在其它方法中发生异常时能够回滚。如果采取这个方式请确保在相应的配置类中将transactionManager正确配置为适用于你的应用程序的事务管理器实现。

          总结

          以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

          0

          上一篇:

          下一篇:

          精彩评论

          暂无评论...
          验证码 换一张
          取 消

          最新开发

          开发排行榜