@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 且抛出异常,具备回滚的能力。现在里面的回滚能力赋给了外面事务,从而整个事务可以回滚。
结论:结合情形一及情形三,可以明白当事务嵌套时,无论是内部事务或者外部事务,只要其中一个事务具备回滚能力,那么整体的事务就能进行回滚,这就是事务的传播现象。
结论
- @Transactional 只能保证 RuntimeException 的回滚,如果要保证 Exception (非 RuntimeException)的回滚,需加上 rollbackFor = Exception.class。
- @transactional 与 try catch 搭配使用时,需要注意在 catch 里面处理 Exception 后将其抛出,不然 @transactional 无法感知到 Exception, 也就无法回滚。
- 当事务嵌套时,无论是内部事务或者外部事务,只要其中一个事务具备回滚能力,那么整体的事务就能进行回滚,这就是事务的传播现象。