@Transactional注解 失效场景 及 解决版本

失效场景

1 数据库首先要支持事务:

以mysql为例,其MyISAM引擎是不支持事务操作的,InnoDB才支持事务的引擎。从mysql5.5开始默认引擎是InnoDB,之前默认的是MyISAM

2.数据源没有配置事务管理器

3. 没有被spring管理

事务注解加到service层,如果没有@Service注解,这个类就不会被加载成一个Bean,那这个类就不会被spring管理,事务自然就失效了

4.方法不是public

根据spring官网,@Transactional只能用于public的方法上,否则事务不会生效,如果非要用在非public方法上,可以开启AspectJ代理模式。AspectJ使用加载时织入的方式,支持所有的pointcut,因此可以支持内部方法的事务设置

5.@Transactional 注解属性 propagation 设置错误

如注解配置的不支持事务,使用了Propagation。NOT_SUPPORTED:表示不易事务运行,当前若存在事务则挂起

6.同一个类中方法调用,导致@Transactional失效

开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法a,a再调用本类的方法b(不论方法b是用public还是private修饰),但方法A没有声明注解事务,而b方法有。则外部调用方法a之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。

那为啥会出现这种情况?其实还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。

 
    //@Transactional
    @GetMapping("/test")
    private void a(User user) throws Exception {
    
    
        userMapper.insert(user);
        b(user);
    }
 
    @Transactional
    public void b(User user) throws Exception {
    
    
       doSomething();
    }

补充:

如果a方法同样加了 @Transactional注解,此时的事务是否能生效呢?

   上面的代码只是放开a方法上的@Transactional注解,此时a方法的事务依然是无法生效的。我们看到在事务方法a中,直接调用事务方法b。从前面介绍的内容可知,b方法拥有事务是因为spring aop生成了代理对象,但是这种方法直接调用了this对象的方法,所以a方法不会生成事务。

由此可见,在同一个类中的方法直接内部调用,会导致事务失效。

那么问题来了,这种场景如何解决呢?

方法一:新加一个service方法

这个方法非常简单,只需要新加一个Service方法,把@Transactional注解加到新的Service方法上,把需要事务执行的代码移到新方法中

​
​
@Servcie 
public class ServiceA {
    
     
   @Autowired 
   prvate ServiceB serviceB; 
 
   public void save(User user) {
    
     
         queryData1(); 
         queryData2(); 
         serviceB.doSave(user); 
   } 
 } 
 
 @Servcie 
 public class ServiceB {
    
     
 
    @Transactional(rollbackFor=Exception.class) 
    public void doSave(User user) {
    
     
       userMapper.insert(user); 
       b(user); 
    } 
 
 } 
 
​
 
​

方法二:在该Service类中注入自己

如果不想再新加一个Service类,在该Service类中注入自己也是一种选择。

​
​
​
@Servcie 
public class ServiceA {
    
     
   @Autowired 
   prvate ServiceA serviceA; 
 
   public void save(User user) {
    
     
         queryData1(); 
         queryData2(); 
         serviceA.doSave(user); 
   } 
 
   @Transactional(rollbackFor=Exception.class) 
    public void doSave(User user) {
    
     
       userMapper.insert(user); 
       b(user); 
    } 
 }

这种做法不会造成循环依赖问题,其实spring IOC内部的三级缓存保证了它,不会出现循环依赖问题。

方法三:通过AopContext类

引入依赖

<dependency>
   <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
</dependency>

开启AOP获取(在SpringBoot启动类或者配置类上加)

@EnableAspectJAutoProxy(exposeProxy = true)

在该Service类中使用AopContext.currentProxy()获取代理对象

@Servcie 
public class ServiceA {
    
     
  
   public void save(User user) {
    
     
         queryData1(); 
         queryData2(); 
         ((ServiceA)AopContext.currentProxy()).doSave(user); 
   } 
 
   @Transactional(rollbackFor=Exception.class) 
    public void doSave(User user) {
    
     
       userMapper.insert(user); 
       b(user); 
    } 
 } 

把异常吃了,然后又不抛出来,事务怎么回滚呢?

如果想要spring事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则spring认为程序是正常的。

默默的说句,即使开发者没有手动捕获异常,但如果抛的异常不正确,spring事务也不会回滚。

因为spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚。

7.通过try catch 把异常吃了

8.异常类型错误【@Transactional 注解属性 rollbackFor 设置错误】

默认回滚的是RuntimeException,如果你想要触发其它的异常回滚,需要在注解上配置

@Transactional(rollbackFor = Exception.class)

9. 方法用final修饰

有时候,某个方法不想被子类重写,这时可以将方法定义成final的。普通方法这样定义没问题,但是如果将事务定义为final,这样会导致事务失效。

为什么呢?

   spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。

   但如果某个方法被final修饰了,那么在它的代理类中,就无法重写该方法,事务也就失效了。

注意:如果某个方法是static的,同样无法通过动态代理,变成事务方法。

10.多线程调用

在实际项目开发中,多线程的使用场景还是挺多的。如果spring事务用在多线程场景中,会有问题吗?

@Slf4j 
@Service 
public class UserService {
    
     
 
    @Autowired 
    private UserMapper userMapper; 
    @Autowired 
    private RoleService roleService; 
 
    @Transactional 
    public void add(UserModel userModel) throws Exception {
    
     
        userMapper.insertUser(userModel); 
        new Thread(() -> {
    
     
            roleService.doOtherThing(); 
        }).start(); 
    } 
} 
 
@Service 
public class RoleService {
    
     
 
    @Transactional 
    public void doOtherThing() {
    
     
        System.out.println("保存role表数据"); 
    } 
} 

从上面的例子中,我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。

这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。

如果看过spring事务源码的朋友,可能会知道spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。

我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。

猜你喜欢

转载自blog.csdn.net/u014212540/article/details/132203122
今日推荐