@Transactional 回滚问题(try catch、嵌套)

@Transactional 回滚问题(try catch、嵌套)

springboot 提供了事务注解 @transactional ,当事务内出现异常时,可以回滚之前执行的代码,避免脏数据的产生。当 @transactional 与 try catch 搭配使用或者进行事务嵌套时,可能会出现无法回滚的问题。

1、建表

这里我建立了一张简单的 user 与 address 表用于后面的测试:

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `name` varchar(30) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `email` varchar(50) DEFAULT NULL COMMENT '邮箱',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

CREATE TABLE `address` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `city` varchar(10) DEFAULT NULL COMMENT '城市',
  `address` varchar(10) DEFAULT NULL COMMENT '地址',
  `user_id` int(11) DEFAULT NULL COMMENT '用户id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='地址';

2、简单测试

@Transactional
public void save() {
    User user = new User();
    user.setName("张三");
    user.setAge(18);
    userMapper.insert(user);
    throw new RuntimeException();
}

事务内报了 RuntimeException ,可以正常回滚。

@Transactional
public void save() throws Exception {
    User user = new User();
    user.setName("张三");
    user.setAge(18);
    userMapper.insert(user);
    throw new Exception();
}

事务内报了 Exception (非 RuntimeException), 事务不能回滚。

@Transactional(rollbackFor = Exception.class)
public void save() throws Exception {
    User user = new User();
    user.setName("张三");
    user.setAge(18);
    userMapper.insert(user);
    throw new Exception();
}

对于 Exception (非 RuntimeException),加上 rollbackFor = Exception.class 可以实现回滚。

结论:@Transactional 只能保证 RuntimeException 的回滚,如果要保证 Exception (非 RuntimeException)的回滚,需加上 rollbackFor = Exception.class。

3、搭配 try catch

try catch 用于捕捉并处理异常,而 @transactional 只有在感知到异常时才会回滚,因此两者搭配可能会出现无法回滚的问题。

@Transactional(rollbackFor = Exception.class)
public void save() throws Exception{
    try {
        User user = new User();
        user.setName("张三");
        user.setAge(18);
        userMapper.insert(user);
        throw new Exception();
    } catch (Exception e) {
        throw e;
    }
}

try catch 抛出 Exception,可以被 @transactional 感知到,此时事务可以正常回滚。

@Transactional(rollbackFor = Exception.class)
public void save() throws Exception{
    try {
        User user = new User();
        user.setName("张三");
        user.setAge(18);
        userMapper.insert(user);
        throw new Exception();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

而在这里,只捕捉打印了 Exception,并无抛出,无法被 @transactional 感知,因此不能回滚。

结论:@transactional 与 try catch 搭配使用时,需要注意在 catch 里面处理 Exception 后将其抛出,不然 @transactional 无法感知到 Exception, 也就无法回滚。

4、@transactional 嵌套

下面讨论三种情形。

情形一:内外分别抛出 Exception, 外面事务加上 rollbackFor = Exception.class,里面事务不加:

//外面
@Transactional(rollbackFor = Exception.class)
public void save() throws Exception {
    addressService.save();
    User user = new User();
    user.setName("张三");
    user.setAge(18);
    userMapper.insert(user);
    throw new Exception();
}

//里面
@Transactional
public void save() throws Exception{
    Address address = new Address();
    address.setCity("北京市");
    address.setAddress("朝阳区");
    address.setUserId(1);
    addressMapper.insert(address);
    throw new Exception();
}

在此情形下事务可以回滚,因为外面事务添加了 rollbackFor = Exception.class,具备处理 Exception(非RuntimeException)的能力,即使里面不加也没有影响,事务正常回滚。

情形二:外面抛出 Exception, 内部事务添加 rollbackFor = Exception.class,外面不加:

//外面
@Transactional
public void save() throws Exception {
    addressService.save();
    User user = new User();
    user.setName("张三");
    user.setAge(18);
    userMapper.insert(user);
    throw new Exception();
}

//里面
@Transactional(rollbackFor = Exception.class)
public void save() throws Exception{
    Address address = new Address();
    address.setCity("北京市");
    address.setAddress("朝阳区");
    address.setUserId(1);
    addressMapper.insert(address);
}

此时事务无法回滚,这是什么原因呢?可以清楚知道外面没有处理 Exception(非RuntimeException)的能力,里面虽然加上了 rollbackFor = Exception.class,但并未抛出异常,结合两者可知事务不能正常回滚。

情形三:内外都抛出 Exception,内部事务添加 rollbackFor = Exception.class,外面不加:

//外面
@Transactional
public void save() throws Exception {
    addressService.save();
    User user = new User();
    user.setName("张三");
    user.setAge(18);
    userMapper.insert(user);
    throw new Exception();
}

//里面
@Transactional(rollbackFor = Exception.class)
public void save() throws Exception{
    Address address = new Address();
    address.setCity("北京市");
    address.setAddress("朝阳区");
    address.setUserId(1);
    addressMapper.insert(address);
    throw new Exception();
}

测试后发现事务正常回滚,这有点神奇。显然外面没有事务回滚的能力,而里面由于加上了 rollbackFor = Exception.class 且抛出异常,具备回滚的能力。现在里面的回滚能力赋给了外面事务,从而整个事务可以回滚。

结论:结合情形一及情形三,可以明白当事务嵌套时,无论是内部事务或者外部事务,只要其中一个事务具备回滚能力,那么整体的事务就能进行回滚,这就是事务的传播现象。

结论

  1. @Transactional 只能保证 RuntimeException 的回滚,如果要保证 Exception (非 RuntimeException)的回滚,需加上 rollbackFor = Exception.class。
  2. @transactional 与 try catch 搭配使用时,需要注意在 catch 里面处理 Exception 后将其抛出,不然 @transactional 无法感知到 Exception, 也就无法回滚。
  3. 当事务嵌套时,无论是内部事务或者外部事务,只要其中一个事务具备回滚能力,那么整体的事务就能进行回滚,这就是事务的传播现象。

参考博客:https://blog.51cto.com/u_13847344/2913640

posted @ 2022-09-09 16:37  MyDistance  阅读(3301)  评论(0编辑  收藏  举报