文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-12-01 01:24

关注

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("测试更新数据回滚");
}
}

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

总结

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

来源:今日头条内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯