盘点导致Spring事务失效的4个场景
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("测试更新数据回滚");
}
}
复制代码
再测试一下,发现插入操作的事务可以正常提交了。
总结
事务是我们日常开发工作中无法避免的一个功能,深刻理解事务的运行机制,正确使用事务的声明式操作,才能让我们写出更健壮的代码。