@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:表示不易事务运行,当前若存在事务则挂起,详细可以参考博客:Spring中的事务(六)_曰业而安的博客-CSDN博客

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方法不会生成事务。

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

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

6.1 新加一个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); 
    } 
 
 } 

​

​

另外看到还有两种方法可以解决此问题,但感觉不是很直观。

6.2 在该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内部的三级缓存保证了它,不会出现循环依赖问题。

6.3 通过AopContext类

在该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); 
    } 
 } 

7. 异常被吃了

@Service
public class EmployeeServiceImpl implements EmployeeService{

    @Transactional
    public void updateEmployee(Employee employee){
        try{
            update(employee);
        }catch(Exception e){
            log.error(e.getMessage(),e);
        }
    }

}

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

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

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

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

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

上面的例子再抛出一个异常

​
@Service
public class EmployeeServiceImpl implements EmployeeService{
    @Transactional
    public void updateEmployee(Employee employee){
        try{

        }catch{
            throw new Exception(更新失败!!!"")
        }
    }

}

​

这样事务也是不生效的,因为默认回滚的是RuntimeException,如果你想要触发其它的异常回滚,需要在注解上配置:

@Transactional(rollbackFor = Exception.class)

这个配置仅限于Throwable异常类及其子类。

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是数据库连接。

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

希望对看到的小伙伴有所帮助,欢迎各位留言。。。

  • 17
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值