为什么抛了异常,事务还是没回滚?一次 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 互相引用?》

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐