首页>国内 > 正文

盘点Spring事务失效的4种写法及解决方案,RReview代码再也不慌了

2022-12-19 09:49:50来源:今日头条

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

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

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

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


(资料图)

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

@Datapublic 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@AllArgsConstructorpublic class DemoService {    private final UserMapper userMapper;      @Transactional    public ResponseEntity addUser(User user) throws Exception {        int insert = this.userMapper.insert(user);        if (insert > 0) {            throw new Exception("异常回滚测试");        }        return ResponseEntity.ok().build();    }}

新建测试方法:

@Testpublic 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@RequiredArgsConstructorpublic class DemoService {    private final UserMapper userMapper;    public ResponseEntity 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@RequiredArgsConstructorpublic class DemoService {    private final UserMapper userMapper;    @Resource    private DemoService self;    public ResponseEntity 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@RequiredArgsConstructorpublic class DemoService {    private final UserMapper userMapper;    @Resource    private DemoService self;    @Transactional(rollbackFor = Exception.class)    public ResponseEntity 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("测试更新数据回滚");  }}

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

总结

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

关键词: 更新数据 这种情况 传播范围 测试数据 解决方案

相关新闻

Copyright 2015-2020   三好网  版权所有 联系邮箱:435 22 640@qq.com  备案号: 京ICP备2022022245号-21