Spring 事务失效的场景

2,850 阅读4分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

日积月累,水滴石穿 😄

1、private、final、static 方法

@Transactional 注解标注的方法的访问权限必须是 public;

@Transactional 注解标注的方法不能被 final、static 修饰,被标注的方法必须是可覆盖的。这是因为事务底层使用的是 aop,而 aop 使用的是代理模式。代理模式生成的代理类无法重写被 final、static 修饰的方法。而 private 方法对子类不可见。

image.png image.png image.png

2、非代理对象调用

非代理对象调用调用事务方法,事务方法会失效。如下:

public void transfer() {
    String sql = "update `test`  set money = money + 100 where id = 1;";
    jdbcTemplate.update(sql);
    reduce();
}

@Transactional
public void reduce() {
    String sql = "update `test`  set money = money - 100 where id = 2;";
    jdbcTemplate.update(sql);
    int i = 1 / 0;
}

这种情况两个方法的操作都不会进行回滚。reduce() 方法相当于 this.reduce(),而 this 不是代理对象,所以 reduce 方法事务失效。

Spring 事务是基于 AOP 实现的,被 @Transactional 标注后,产生的对象是代理对象。事务的提交、回滚就是代理逻辑,想使用到代理逻辑需要使用代理对象。

解决方案也有几种,比如:

  • ① 将事务方法移动到另外一个类中、
  • ② 在本类中注入自己、
  • ③ 使用 @EnableAspectJAutoProxy(exposeProxy = true) + AopContext.currentProxy()

小杰这里使用第二种方式。

@Autowired
private TestServiceImpl serviceImpl;

public void transfer() {
    String sql = "update `test`  set money = money + 100 where id = 1;";
    jdbcTemplate.update(sql);
    serviceImpl.reduce();
}

@Transactional
public void reduce() {
    String sql = "update `test`  set money = money - 100 where id = 2;";
    jdbcTemplate.update(sql);
    int i = 1 / 0;
}

这样 reduce() 方法就不会出现事务失效,所以发生异常,会进行回滚。但 transfer 就不是个事务方法,所以不会回滚。

3、将异常处理掉

 @Transactional
 public void transfer() {
     String sql = "update `test`  set money = money + 100 where id = 1;";
     jdbcTemplate.update(sql);
     //serviceImpl.reduce();
     try {
         int i = 1 /0;
     } catch (Exception e) {
        
     }
 }

4、抛出的异常不在回滚范围内

 @Transactional
 public void transfer() throws Exception {
     String sql = "update `test`  set money = money + 100 where id = 1;";
     jdbcTemplate.update(sql);
     //serviceImpl.reduce();
     try {
         int i = 1 /0;
     } catch (Exception e) {
         throw new Exception(e);
     }
 }

默认情况下,Spring 事务只有遇到 RuntimeException 以及 Error 时才会回滚,在遇到检查型异常时是不会回滚的,比如 IOException、TimeoutException。所以,一般情况下都需要使用 rollbackFor参数指定回滚异常类,比如:@Transactional(rollbackFor = Exception.class)

5、使用错误的传播行为

@Transactional(rollbackFor = Exception.class)
public void transfer() {
    String sql = "update `test`  set money = money + 100 where id = 1;";
    jdbcTemplate.update(sql);
    serviceImpl.reduce();
}

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void reduce() {
    String sql = "update `test`  set money = money - 100 where id = 2;";
    jdbcTemplate.update(sql);
    int i = 1 / 0;
}

这种写法会使 reduce 方法事务失效,出现异常不会回滚。这是因为使用了 NOT_SUPPORTED 的传播行为,该行为的特性是:以非事务方式运行,如果当前存在事务,则把当前事务挂起。而 transfer 方法会进行事务回滚,这是因为 reduce 方法的异常会往上抛,被 transfer 感知到,进行了事务回滚。

6、多线程调用

@Transactional(rollbackFor = Exception.class)
public void transfer() throws InterruptedException {
    String sql = "update `test`  set money = money + 100 where id = 1;";
    jdbcTemplate.update(sql);
    new Thread(() ->{
        serviceImpl.reduce(jdbcTemplate);
    }).start();
    Thread.sleep(1000);
}

@Transactional(rollbackFor = Exception.class)
public void reduce(JdbcTemplate jdbcTemplate) {
    String sql = "update `test`  set money = money - 100 where id = 2;";
    jdbcTemplate.update(sql);
    int i = 1 / 0;
}

从示例代码中,可以看到事务方法 transfer 调用了事务方法 reduce,而 reduce 方法是开启了一个新线程调用的。这样会导致 reduce 方法不会加入到 transfer 事务中,reduce 方法会重新创建一个新事务。 这是因为 Spring 的事务是通过数据库连接来创建的,同一个事务,只能用同一个数据库连接。而多线程场景下,拿到的数据库连接是不一样的,即会导致获取到的不同事务。既然是两个事务,则没办法进行统一回滚。

7、数据库引擎不支持事务

比如 Mysql 的 MyISAM引擎就不支持事务。

8、代理类过早实例化

@Service
public class TestServiceImpl implements BeanPostProcessor, Ordered {

    @Autowired
    private TestServiceImpl serviceImpl;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional(rollbackFor = Exception.class)
    public void transfer()  {
        String sql = "update `test`  set money = money + 100 where id = 1;";
        jdbcTemplate.update(sql);
        serviceImpl.reduce(jdbcTemplate);
    }

    private void reduce(JdbcTemplate jdbcTemplate) {
        String sql = "update `test`  set money = money - 100 where id = 2;";
        jdbcTemplate.update(sql);
        int i = 1 / 0;
    }


    @Override
    public int getOrder() {
        return 1;
    }
}

当代理类的实例化早于 AbstractAutoProxyCreator后置处理器,就无法被AbstractAutoProxyCreator后置处理器进行AOP增强。

上面 8 种事务失效场景中,需要我们平常注意的只有 2、3、4、5。

  • 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞和关注。