为什么抛了异常,事务还是没回滚?一次 Debug 看懂 Spring 的回滚规则
为什么抛了异常,事务还是没回滚?一次 Debug 看懂 Spring 的回滚规则
上一篇我们通过源码分析了事务为什么会失效。
评论区有个问题出现得特别多:
明明加了 @Transactional,也确实抛了异常,为什么数据库里的数据还是提交成功了?
如果你也遇到过这种情况,先别怀疑 Spring。
因为很多时候:
事务没有失效
事务也执行了
只是没有回滚
今天通过 Debug 一步步看看 Spring 到底是怎么决定回滚的。
第一坑:try-catch 导致事务不回滚
先看代码:
@Transactional
public void save() {
userMapper.insert(user);
try {
int i = 1 / 0;
} catch (Exception e) {
e.printStackTrace();
}
}
先别往下看。
猜一下最终结果:
A:插入成功
B:插入失败
很多人会选:
B
因为:
发生异常
↓
事务回滚
但实际结果:
插入成功
数据库里已经有数据了。
为什么?
Debug 进入:
TransactionInterceptor
继续:
invokeWithinTransaction()
正常情况下,发生异常后会进入:
completeTransactionAfterThrowing()
然后执行回滚。
但这次根本没有进去。
因为异常已经被你吃掉了。
流程如下:
RuntimeException
↓
catch
↓
异常消失
↓
Spring认为执行成功
↓
commit
Spring 根本不知道这里出过异常。
它看到的只是:
save();
正常结束;
自然执行提交。
如何让它回滚?
最简单的方法:
@Transactional
public void save() {
userMapper.insert(user);
try {
int i = 1 / 0;
} catch (Exception e) {
throw e;
}
}
重新抛出异常。
流程变成:
异常
↓
TransactionInterceptor
↓
捕获异常
↓
回滚
第二坑:明明抛了异常,为什么还是提交了?
再看这个例子:
@Transactional
public void save() throws Exception {
userMapper.insert(user);
throw new Exception("测试异常");
}
很多人看到这里会觉得:
这次总该回滚了吧
结果运行:
数据依然提交
为什么?
继续 Debug。
进入:
RuleBasedTransactionAttribute
再进入:
rollbackOn(Throwable ex)
核心源码:
@Override
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException ||
ex instanceof Error);
}
看到这里答案就出来了。
Spring 默认规则:
RuntimeException
√ 回滚
Error
√ 回滚
Exception
× 不回滚
而刚才抛的是:
Exception
属于受检异常(Checked Exception)。
所以 Spring 默认不会回滚。
为什么 Spring 默认不回滚 Exception?
很多人第一次看到这里会疑惑:
既然都是异常
为什么区别对待?
Java异常体系:
Throwable
│
├── Error
│
└── Exception
│
├── RuntimeException(运行时异常)
│
└── CheckedException(非运行时异常)
Spring认为:
RuntimeException
通常表示程序错误
需要回滚
例如:
NullPointerException
ArithmeticException
IndexOutOfBoundsException
而:
Checked Exception
更多是业务异常
例如:
IOException
SQLException
ParseException
Spring默认交给开发者自己决定。
所以不会自动回滚。
第三坑:为什么 rollbackFor 有效?
很多项目里都能看到:
@Transactional(
rollbackFor = Exception.class
)
为什么加上这一句就好了?
再次 Debug。
进入:
RollbackRuleAttribute
会发现 Spring 在判断回滚时:
不仅检查:
RuntimeException
还会检查:
rollbackFor
例如:
@Transactional(
rollbackFor = Exception.class
)
public void save() throws Exception {
throw new Exception("测试");
}
流程变成:
Exception
↓
匹配 rollbackFor
↓
满足回滚规则
↓
rollback
所以很多公司的规范都会要求:
@Transactional(
rollbackFor = Exception.class
)
直接统一配置。
避免开发人员忘记异常类型。
第四坑:最容易误判的场景
看下面代码:
@Transactional
public void save() {
userMapper.insert(user);
throw new RuntimeException();
}
这次一定回滚。
没有问题。
但是:
@Transactional
public void save() {
try {
userMapper.insert(user);
throw new RuntimeException();
} catch (Exception e) {
log.error("异常", e);
}
}
结果:
提交成功
为什么?
因为 Spring 判断回滚的依据不是:
是否发生过异常
而是:
方法最终是否抛出了异常
这是很多人第一次 Debug 才发现的事情。
Spring看到的是:
save();
正常返回;
于是:
commit();
Debug 路径
建议自己跟一次源码。
从:
userService.save();
进入:
TransactionInterceptor
然后:
invokeWithinTransaction()
继续:
completeTransactionAfterThrowing()
或者:
commitTransactionAfterReturning()
你会发现事务最终只有两个结果:
commit
或者
rollback
而决定进入哪个分支的核心代码就是:
rollbackOn(Throwable ex)
【图】
业务方法
↓
TransactionInterceptor
↓
是否发生异常
↓
rollbackOn()
↓
满足规则?
↓
是 否
↓ ↓
rollback commit
总结
很多人以为:
发生异常
↓
事务回滚
实际上 Spring 的逻辑是:
发生异常
↓
事务拦截器收到异常
↓
满足回滚规则
↓
事务回滚
所以以下情况不会回滚:
try-catch吃掉异常
Checked Exception
不满足rollbackFor规则
而以下情况会回滚:
RuntimeException
Error
rollbackFor指定异常
记住下面这张图,基本就理解 Spring 的事务回滚机制了:
异常
↓
TransactionInterceptor
↓
rollbackOn()
↓
满足规则
↓
rollback
最后留个问题。
下面代码会回滚吗?
@Transactional
public void a() {
b();
}
@Transactional
public void b() {
throw new RuntimeException();
}
很多人觉得一定回滚。
但线上经常出现:
UnexpectedRollbackException
这又是为什么?
下一篇:
《为什么会有循环依赖?Spring 为什么允许两个 Bean 互相引用?》
更多推荐



所有评论(0)