@Transactional注解事务失效的几种原因

@Transactional注解事务失效的几种原因

  • 一、异常被捕获后没有抛出
  • 二、抛出非运行时异常
  • 三、同一个类中方法内部直接调用
    • 3.1 失效原因
    • 3.2 解决办法
      • 3.2.1 新加一个service方法
      • 3.2.2 在该 Service 类中注入自己
      • 3.2.3 通过 AopContent 类
  • 四、未被Spring管理
  • 五、新开启一个线程
  • 六、访问权限问题
  • 七、方法用final修饰
  • 八、数据库本身不支持
  • 九、事务传播属性设置错误
  • 其他
    • 1、大事务问题
    • 2、编程式事务

一、异常被捕获后没有抛出

如果想要 spring 事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,或者自己吞掉了异常没有抛出,则 spring 认为程序是正常的。因此也不会回滚。

二、抛出非运行时异常

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

如果事务注解使用的是@Transactional(),即使异常抛出了,但是抛出的是非RuntimeException类型异常,同样也不会回滚。
如果事务注解使用的是@Transactional(rollbackFor = Exception.class),那么抛出的是非RuntimeException类型异常是可以回滚的。

注: 也就是说,如果抛的异常不正确,spring 事务也不会回滚。

比如,方法上的事务是:@Transactional(rollbackFor = BusinessException.class),而实际执行业务逻辑时,程序报错,抛了 SqlException、DuplicateKeyException 等异常。而 BusinessException 是我们自定义的异常,报错的异常不属于 BusinessException,所以事务也不会回滚。

所以,建议一般情况下,将该参数设置成:Exception 或 Throwable。

三、同一个类中方法内部直接调用

3.1 失效原因

这种场景很常见,方法A调用方法B,其中方法A未使用事务,而方法B使用了事务,此时方法B的事务是不生效的,例子如下,因为updateStatus 方法拥有事务的能力是因为 spring aop 生成代理了对象,但是这种方法直接调用了 this 对象的方法,所以 updateStatus 方法不会生成事务。
例子:

@Service
public class UserService {
 
    @Autowired
    private UserMapper userMapper;
 
  
    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
        updateStatus(userModel);
    }
 
    @Transactional
    public void updateStatus(UserModel userModel) {
        doSameThing();
    }
}

3.2 解决办法

3.2.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) {
       addData1();
       updateData2();
    }
 
 }

3.2.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) {
       addData1();
       updateData2();
    }
 }

3.2.3 通过 AopContent 类

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

上面的方法 2 确实可以解决问题,但是代码看起来并不直观,还可以通过在该 Service 类中使用 AOPProxy 获取代理对象,实现相同的功能。具体代码如下:

@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) {
       addData1();
       updateData2();
    }
 }

四、未被Spring管理

在我们平时开发过程中,有个细节很容易被忽略,即使用 spring 事务的前提是:对象要被 spring 管理,需要创建 bean 实例。
通常情况下,我们通过 @Controller、@Service、@Component、@Repository 等注解,可以自动实现 bean 实例化和依赖注入的功能。
如果有一天,你匆匆忙忙地开发了一个 Service 类,但忘了加 @Service 注解,那么该类不会交给 spring 管理,所以它的 add 方法也不会生成事务。

五、新开启一个线程

在实际项目开发中,多线程的使用场景还是挺多的。如果 spring 事务用在多线程场景中,会有问题吗?
如果两个方法不在同一个线程中,那么获取到的数据库连接不一样,从而是两个不同的事务。
spring 的事务是通过数据库连接来实现的。当前线程中保存了一个 map,key 是数据源,value 是数据库连接。
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。

六、访问权限问题

众所周知,java 的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。
如果方法的访问权限被定义成了private,这样会导致事务失效,spring 要求被代理方法必须是public的。

七、方法用final修饰

spring 事务底层使用了 aop,也就是通过 jdk 动态代理或者 cglib,帮我们生成了代理类,在代理类中实现的事务功能。
但如果某个方法用 final 修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。

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

八、数据库本身不支持

众所周知,在 mysql5 之前,默认的数据库引擎是myisam。

它的缺点就是不支持事务,因此在mysql5之后,myisam逐渐退出了历史的舞台,取而代之的是Innodb。

九、事务传播属性设置错误

我们在使用@Transactional注解时,是可以指定propagation参数的。
该参数的作用是指定事务的传播特性,spring 目前支持 7 种传播特性:

  • REQUIRED 如果当前上下文中存在事务,则加入该事务,如果不存在事务,则创建一个事务,这是默认的传播属性值。
  • SUPPORTS 如果当前上下文中存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
  • MANDATORY 当前上下文中必须存在事务,否则抛出异常。
  • REQUIRES_NEW 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
  • NOT_SUPPORTED 如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
  • NEVER 如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
  • NESTED 如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。设置其他传播特性都不会创建事务。

其他

1、大事务问题

例:

@Service
public class UserService {
    
    @Autowired 
    private RoleService roleService;
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
       query1();
       query2();
       query3();
       roleService.save(userModel);
       update(userModel);
    }
}
 
@Service
public class RoleService {
    
    @Autowired 
    private RoleService roleService;
    
    @Transactional
    public void save(UserModel userModel) throws Exception {
       query4();
       query5();
       query6();
       saveData(userModel);
    }
}

但@Transactional注解,如果被加到方法上,有个缺点就是整个方法都包含在事务当中了。

上面的这个例子中,在 UserService 类中,其实只有这两行才需要事务:

roleService.save(userModel);
update(userModel);

在 RoleService 类中,只有这一行需要事务:

saveData(userModel);

现在的这种写法,会导致所有的 query 方法也被包含在同一个事务当中。

如果 query 方法非常多,调用层级很深,而且有部分查询方法比较耗时的话,会造成整个事务非常耗时,而从造成大事务问题。

2、编程式事务

上面聊的这些内容都是基于@Transactional注解的,主要说的是它的事务问题,我们把这种事务叫做:声明式事务。

其实,spring 还提供了另外一种创建事务的方式,即通过手动编写代码实现的事务,我们把这种事务叫做:编程式事务。例如:

 @Autowired
   private TransactionTemplate transactionTemplate;
   
   ...
   
   public void save(final User user) {
         queryData1();
         queryData2();
         transactionTemplate.execute((status) => {
            addData1();
            updateData2();
            return Boolean.TRUE;
         })
   }

在 spring 中为了支持编程式事务,专门提供了一个类:TransactionTemplate,在它的 execute 方法中,就实现了事务的功能。

相较于@Transactional注解声明式事务,我更建议大家使用基于TransactionTemplate的编程式事务。主要原因如下:

  1. 避免由于 spring aop 问题导致事务失效的问题。
  2. 能够更小粒度地控制事务的范围,更直观。

建议在项目中少使用 @Transactional 注解开启事务。但并不是说一定不能用它,如果项目中有些业务逻辑比较简单,而且不经常变动,使用 @Transactional 注解开启事务也无妨,因为它更简单,开发效率更高,但是千万要小心事务失效的问题。

你可能感兴趣的:(数据库,spring,java,mysql)