盘点导致Spring事务失效的4个场景

3552 篇文章 115 订阅

1,非运行时异常导致事务无法回滚

我们知道,Spring是通过AOP的方式来实现事务的,而在处理事务的过程中,Spring只有捕获到RuntimeException或者Error的时候才会触发回滚操作,如果我们在代码中抛出的是非运行时异常,而又没有特殊配置的话,事务就会无法回滚。

下面我们以一个简单的例子,复现一下这种情况,以及针对这种情况的解决方案。

本文Springboot版本:2.7.6,数据源为MySQL。

首先创建一个测试用的User对象:

@Data
public class User {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private String pwd;
}
复制代码

建表语句:

CREATE TABLE user  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `pwd` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB;
复制代码

测试逻辑:往user表插入一条数据,如果插入成功,就抛出exception异常,测试数据是否回滚。

@Service
@AllArgsConstructor
public class DemoService {
    private final UserMapper userMapper;
  
    @Transactional
    public ResponseEntity<Object> addUser(User user) throws Exception {
        int insert = this.userMapper.insert(user);
        if (insert > 0) {
            throw new Exception("异常回滚测试");
        }
        return ResponseEntity.ok().build();
    }
}
复制代码

新建测试方法:

@Test
public void addUserTest() throws Exception {
    User user = new User();
    user.setName("测试");
    user.setPwd("123456");
    this.demoService.addUser(user);
}
复制代码

运行测试方法,从控制台可以看到,我们手动指定的异常被成功抛出。

但是,当异常发生时,事务并没有被回滚,数据依然被插入到了数据库。

解决办法:
1,将异常包装成运行时异常:throw new RuntimeException("异常回滚测试");

2,在@Transactional指定回滚的异常类型,@Transactional(rollbackFor = Exception.class)。

一般来说,使用第二种方式会更清晰一些,但是有些朋友往往会忘记手动指定回滚的异常类型,进而导致非预期的bug产生。

2,通过this调用本类事务方法导致的事务无法回滚

随着业务的发展,核心业务代码会越来越多,同一个方法也会越写越长。我们为了使代码逻辑更加高内聚低耦合,会将功能相同的代码进行封装成一个个的子方法。

但是,如果我们对事务的运行机制了解不透彻,随意在同一个类中通过this调用事务方法,就可能导致非预期的bug。

@Service
@RequiredArgsConstructor
public class DemoService {
    private final UserMapper userMapper;
    public ResponseEntity<Object> addUser(User user){
        //注意这一行
        this.doAddUser(user);
        return ResponseEntity.ok().build();
    }

    @Transactional(rollbackFor = Exception.class)
    public void doAddUser(User user) {
        int insert = this.userMapper.insert(user);
        if (insert > 0) {
            throw new RuntimeException("测试添加异常回滚");
        }
    }
}
复制代码

如以上代码所示,在addUser方法中调用了事务方法doAddUser,如果数据插入成功,就抛出一个异常,测试数据是否能够回滚。

通过测试用例可以看到,异常已经抛出,但是数据库中却成功的插入了数据,我们期望的数据并没有回滚。

原因探究:

原因其实很简单,通过this方法调用时,Spring的代理没能起作用,事务自然也就无法介入,关于这一点的原理在之前的文章中也有分析过,感兴趣的朋友可以去看一看。

有的朋友可能会说,项目的代码已经是这样了,再将老方法重写到新类中也不现实,有没有办法改动较小的方式呢?

其实很简单,现在事务失效的原因是代理失效,那么想办法让代理重新生效就行了。

我们在本类中注入一个当前对象,这个对象可以被Spring代理,那么这个对象的方法自然也可以被代理。

@Service
@RequiredArgsConstructor
public class DemoService {
    private final UserMapper userMapper;
    @Resource
    private DemoService self;
    public ResponseEntity<Object> addUser(User user){
        //通过self引用使代理生效
        this.self.doAddUser(user);
        return ResponseEntity.ok().build();
    }

    @Transactional(rollbackFor = Exception.class)
    public void doAddUser(User user) {
        int insert = this.userMapper.insert(user);
        if (insert > 0) {
            throw new RuntimeException("测试添加异常回滚");
        }
    }
}
复制代码

3,被声明的事务方法是private类型

这种错误在博主刚工作时遇到挺多次的,不过现在现代IDE已经越来越智能了,对于这种情况会直接给出错误提示,所以这里提出这种错误只是告诉大家,事务方法是不能声明为private的。

至于为什么不能是private,那自然还是和代理有关了。

4,嵌套事务异常导致事务被提前关闭而报错

当使用嵌套事务时,需要明确指定事务的传播范围。

@Service
@RequiredArgsConstructor
public class DemoService {
    private final UserMapper userMapper;
    @Resource
    private DemoService self;

    @Transactional(rollbackFor = Exception.class)
    public ResponseEntity<Object> addUser(User user) {
        int insert = this.userMapper.insert(user);
        if (insert > 0) {
            try {
                this.self.update(user);
            } catch (Exception e) {
                System.out.println("即使更新异常也不要影响添加数据");
            }
        }
        return ResponseEntity.ok().build();
    }

    @Transactional(rollbackFor = Exception.class)
    public void update(User user) {
        user.setPwd("666666");
        int update = this.userMapper.updateById(user);
        if (update > 0) {
            throw new RuntimeException("测试更新数据回滚");
        }
    }
}
复制代码

如以上代码,我们添加完一条数据之后,尝试将密码更新为666666,并且希望即使更新异常,也不要影响添加操作。

然而运行测试用例,我们会得到这样一条错误信息:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only。

什么意思呢?就是当Spring处理事务时,发现事务已经被回滚了。

这是因为我们并没有指定事务的传播行为,默认情况下,Spring的事务传播是REQUIRED,即:如果本来有事务,则加入该事务,如果没有事务,则创建新的事务。

我们添加数据时启动了一个事务,更新数据时,Spring判断当前已经存在事务,所以就不再新建事务,而是加入当前事务。

但是当更新操作失败时,需要对事务进行回滚,更新是没问题的,正常回滚。

但是插入操作就不行了,当要提交插入操作的事务时,由于事务已经被回滚了,无法再次操作,Spring只好报错来提示我们了。

如何处理呢?在更新操作上指明事务的传播范围就行。

@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
public void update(User user) {
  user.setPwd("666666");
  int update = this.userMapper.updateById(user);
  if (update > 0) {
    throw new RuntimeException("测试更新数据回滚");
  }
}
复制代码

再测试一下,发现插入操作的事务可以正常提交了。

总结

事务是我们日常开发工作中无法避免的一个功能,深刻理解事务的运行机制,正确使用事务的声明式操作,才能让我们写出更健壮的代码。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值