SpringBoot @Transactional 注解未生效

1. SpringBoot 中使用事务的正确方式

Spring 框架中,要让框架自动管理事务需要显式使用 @EnableTransactionManagement注解,但是SpringBoot 的自动配置机制已经在org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration 中启用了该注解,故不再需要在启动类上添加这个注解。这样在 SpringBoot 中只要@Transactional 注解方法就表明开启了自动事务管理,其正确的使用方式如下

  1. 声明接口方法 Service

    public interface Service {
        void updateResult(String req);
    }
    
  2. ServiceImpl 接口方法实现上添加注解

    public class ServiceImpl implements Service{
       // 该对象为数据库操作实例,具体实现此处不表
      @Autowired
      private ResultRepository mResultRepository;
      
      @Transactional
      @Override
      public void updateResult(String req) {
          mResultRepository.update(req);
      }
    }
    
  3. ResultController 中通过 spring 自动注入的 Service 对象来调用 updateResult() 方法,即可实现事务管理

    @RestController
    public interface ResultController {
    
     @Autowired
     private Service service;
     
     @GetMapping(value = "result/update")
     public void updateResult(String req) {
         service.updateResult(req);
     }
    }
    

2. @Transactional 注解未生效的原因

2.1 数据库引擎不支持事务

数据库事务是基于数据库引擎的,使用事务首先要保证数据库引擎支持事务。以 MySQL 为例,InnoDB 引擎支持事务,而 MyISAM 引擎不支持事务,这个盲点有时会产生很多不必要的问题

2.2 @Transactional 修饰的方法未通过接口调用

Spring 是采用 AOP 的方式来执行事务的,事务要生效那么AOP必须生效。Spring 中 AOP 实现方式可参考短文 AOP 的实现方式,其两种实现 JDK 动态代理CGLIB 生成子类 原理都是在目标方法的外部包装一层逻辑,这样在外部调用的时候流程为 代理类方法-->目标方法,AOP的增强就生效了。当对象内部方法互相调用,其流程表述为 代理类方法1-->目标方法1-->目标方法2目标方法2 的增强逻辑根本不会被调用,自然AOP 就失效了。

  • 注意
    正常情况下 CGLIB 实现 AOP 的方式是生成目标类的子类,那么根据方法重写的规则,我们知道即便是内部方法调用其流程也应该是 代理类方法1-->目标方法1-->代理类方法2-->目标方法2,AOP 理应生效。Spring 中有注解@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true) 可强制使用 CGLIB 来实现 AOP,但是笔者在添加该注解后发现事务方法内部调用依然不生效。深入其实现,发现在 Spring 的 CglibAopProxy内部类DynamicAdvisedInterceptor#intercept() 拦截方法实现如下,内部调用时拦截链为空,不会调用代理类增强方法
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                ......
     			// Check whether we only have one InvokerInterceptor: that is,
     			// no real advice, but just reflective invocation of the target.
     			if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
     				// We can skip creating a MethodInvocation: just invoke the target directly.
     				······
     				// 如果没有拦截器则直接调用目标方法,Spring不会让内部方法互相调用产生拦截
     				// methodProxy.invokeSuper() 才会调用代理类的增强方法
     				retVal = methodProxy.invoke(target, argsToUse);
     			}
     			......
     	}
    

2.3 @Transactional 修饰的方法内部捕获异常

方法内部使用 try/catch,异常未被抛出,不会回滚。Spring 自动事务管理通过方法是否抛出异常来决定执行提交还是回滚,默认只有 RuntimeException 才会回滚,但是这个异常范围有点小,rollbackFor 参数指定为 Exception.class 是很有必要的

2.4 @Transactional 多数据源环境下未指定 transactionManager 参数

多数据源环境下如果没有指定 transactionManager 参数,默认的 transactionManager 一般是主库的事务管理器,而如果期望的事务操作不是在主库中,发生错误是显而易见的。另外如果一个事务中涉及到了多个数据库,Spring中的自动事务管理是无法正常进行的,因为当前数据库A操作完之后,Spring 框架会复用已有的数据库连接去操作另一个数据库B,这样大部分情况下就会报出表不存在之类的错误

  • 分布式事务可借助其他工具实现,例如 atomikos

3. 解决方案

3.1 自动管理事务

使用 @Transactional 注解内部调用事务失效的原因就是AOP 失效,但是使用 AopContext.currentProxy()来获取代理对象再调用其事务方法被笔者证明可使AOP生效,但是无法使事务生效 ,具体原因有待进一步分析。不过无数的经验证明,仅凭技术无法实现的能力,通常可以通过改变结构来实现。以下两种解决方案笔者更倾向于第一种在 Controller 中重新组织 Service 调用的方式,第二种将数据库操作组合下沉到仓储层的做法在业务复杂时会不可避免地混入业务逻辑,与分层思想有一定出入

3.1.1 将 Service 服务调用组织到 Controller 中

采用上文SpringBoot中使用事务的正确方式的例子 ,如果在 ServiceImpl 将其重写的updateResult()方法分拆成了两部分,一部分是其他逻辑,一部分是更新数据库。显而易见的,添加在内部调用方法 updateRepository()上的事务注解不会生效。这时候一个解决的思路就是,将 updateRepository()方法向上抽取到 Service接口中,将其暴露为外部接口,然后在 Controller 里面重新组织Service 方法的调用流程。例如之前在 Controller 中只调用了service.updateResult(req),现在可以组织调用为 service.updateResult(req);service.updateRepository(req);

在这里插入图片描述

public class ServiceImpl implements Service{
  @Autowired
  private ResultRepository mResultRepository;
  
  @Override
  public void updateResult(String req) {
  // 1. 其他逻辑
  ······
  // 2. 更新数据库
      updateRepository(req);
  }

  @Transactional
  public void updateRepository(String req) {
      mResultRepository.update(req);
  }
}

3.1.2 将数据库的操作组合下沉到仓储层 Repository 中

在业务代码中,事务表现比较突出的特性为原子性。通常一个 Dao 接口对应一个数据表 Mapper,但有时我们也可以使用一个聚合的 Dao 接口,使其实现类包含多个数据表 Mapper,并将多个 Mapper 的操作封装在同一个接口方法实现中,这样使用 @Transactional 注解来修饰这个实现方法就能达到事务效果

在这里插入图片描述

3.2 手动管理事务

手动事务就是在代码中显式地进行事务操作,与上古时代基于 Java 原生 API 手动管理数据库连接的开发模式完全相同,不过认真说起来即便是那会大部分事务也是通过 Filter 过滤器把数据库连接 DataConnection 放在 ThreadLocal 实现,实现可参考 TransactionFilter。SpringBoot 中手动事务的使用方法如下,需注意此处注入的 DataSourceTransactionManager默认的数据源的事务管理器,如果在多数据源环境下,需要为每个数据源都配置一个事务管理器,然后注入的时候根据数据源指定其事务管理器

// 注入如下两个对象
@Resource
DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
TransactionDefinition transactionDefinition;

public void update(){
    //开启事务
    TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
    try{
        this.update();
        //无异常,提交事务
        dataSourceTransactionManager.commit(transactionStatus);
    }catch(Exception e){
        //异常,回滚事务
        dataSourceTransactionManager.rollback(transactionStatus);
    }
}
发布了97 篇原创文章 · 获赞 88 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_45505313/article/details/103284559
今日推荐