Skip to content

写这篇文章的起因:一直知道 @Transactional 加在方法上"出错会自动回滚",但从来没深想过为什么能回滚。这次把原理彻底扒了一遍——结论是:Spring 只负责"决定提交还是回滚",真正执行回滚的是数据库的 undo log。三层各司其职,下面一层层讲透。

一、一句话先建立整体认知

@Transactional 的本质 = AOP 代理 + 事务管理器 + 数据库事务

调用方法 → Spring 代理拦截 → 开启事务 → 执行业务 SQL → 成功 commit / 异常 rollback

                          数据库用 undo log 执行真正的"撤销"

三层分工:

  1. Spring AOP:决定"成功就提交,失败就回滚"
  2. 数据库连接:承载事务(setAutoCommit(false) / commit() / rollback()
  3. 数据库引擎:用 undo log 真正执行回滚(恢复事务前的数据)

@Transactional 为什么能回滚:三层原理

二、为什么能回滚:核心原理(三层)

第 1 层:Spring 的 AOP 代理

@Transactional 标记的方法,Spring 会为它所在的 Bean 生成一个代理对象(CGLIB 或 JDK 动态代理)。你调用方法时,实际调用的是代理,代理在方法前后"织入"事务逻辑:

java
// 你写的(伪代码理解用)
@Transactional
public void transfer() {
    userMapper.decrease(100);  // 扣款
    userMapper.increase(100);  // 加款
}

// 实际执行的是代理包装后的(伪代码):
public void transfer$proxy() {
    try {
        connection.setAutoCommit(false);  // 开启事务
        target.transfer();                 // 调用真实方法
        connection.commit();               // 成功 → 提交
    } catch (Exception e) {
        connection.rollback();             // 异常 → 回滚
        throw e;
    }
}

关键:回滚的"决定权"在 Spring 这里——它只负责根据方法是否抛异常,调用 commit() 还是 rollback()数据怎么撤销,Spring 不管(那是数据库的事)。

第 2 层:数据库连接(Connection)承载事务

Spring 通过 TransactionManager(如 DataSourceTransactionManager)拿到同一个数据库连接,在这条连接上控制事务:

  • setAutoCommit(false):关闭自动提交 = 事务开始(后面的 SQL 不立即生效,等你 commit)
  • 方法里的所有 SQL 都在这同一条连接上执行(Spring 保证:同一事务共用一个连接)
  • commit():事务内的修改一起生效
  • rollback():事务内的修改一起撤销

第 3 层:数据库引擎的 undo log(真正执行回滚)

这是"为什么能回滚"的终极答案数据库引擎自己记录撤销信息(MySQL InnoDB 用 undo log,MySQL 基础见《MySQL 快速入门》):

执行 UPDATE user SET balance = 0 WHERE id = 1
    ↓ 执行前,undo log 记录旧值 balance = 100
    ↓ 修改生效,内存/磁盘变为 balance = 0

rollback() 被调用
    ↓ InnoDB 按 undo log 反向操作,把 balance 恢复为 100

所以

  • 回滚不是 Spring 做的,是数据库做的——Spring 只是给数据库发了"回滚"指令
  • undo log 只撤销本事务的修改,其他并发事务的数据不受影响(这就是"回滚 ≠ 数据库恢复"的原因——不是整个库还原,只是撤销这一次操作)

三、7 种传播行为(Propagation)

事务怎么和"已有事务"互动,由传播行为决定:

传播行为含义场景
REQUIRED(默认)有事务就加入,没有就新建绝大多数场景
REQUIRES_NEW无论有没有,都新开一个独立事务日志记录、异步任务(失败不影响主事务)
NESTED嵌套事务,子事务回滚不影响外层(Savepoint)部分步骤可单独回滚
SUPPORTS有就加入,没有就不开事务只读查询
NOT_SUPPORTED挂起当前事务,以无事务方式执行非事务操作
MANDATORY必须有事务,否则抛异常强制在事务内调用
NEVER必须无事务,否则抛异常禁止在事务内执行

记忆口诀:REQUIRED 是"搭便车"(有车就上),REQUIRES_NEW 是"自己开车"(不管别人)。

四、失效场景(面试高频,都是血泪)

@Transactional 不是加上就生效,以下情况会静默失效:

失效场景原因解决
同类方法内部调用this.method() 直接调真实对象,不经过代理注入自身/拆到另一个 Bean/用 AopContext.currentProxy()
方法不是 public代理只对 public 方法织入改成 public
异常被 catch 掉方法没抛异常,Spring 以为成功别 catch,或 catch 后手动 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
checked 异常(编译期异常)默认只回滚 RuntimeExceptionError@Transactional(rollbackFor = Exception.class)
多线程调用新线程用的是新连接 = 新事务,管不到事务内别开线程,或用编程式事务
方法自调用(private helper)同上,不经过代理拆 Bean 或走 public 入口
代理方式问题接口代理(JDK)时类没实现接口用 CGLIB(proxyTargetClass=true

最常见的前三个:同类调用、非 public、异常被 catch——面试必问,线上必踩。

五、回滚规则:哪些异常会回滚

java
// 默认:只回滚 RuntimeException 和 Error
@Transactional
public void save() { ... }   // 抛 SQLException(checked)不回滚!

// 显式指定:所有异常都回滚
@Transactional(rollbackFor = Exception.class)
public void save() { ... }
  • 默认 rollbackForRuntimeException.classError.class
  • 业务里最常见:@Transactional(rollbackFor = Exception.class) 兜底

六、@Transactional vs 编程式事务

方式写法优点缺点
声明式(@Transactional)注解,AOP 自动处理简单、侵入小失效场景多、不能在事务中间手动控制
编程式(TransactionTemplate)手动 execute 回调灵活、边界清晰、可在事务中途决定提交代码侵入
java
@Autowired
private TransactionTemplate transactionTemplate;

public void transfer() {
    transactionTemplate.execute(status -> {
        userMapper.decrease(100);
        if (某些条件不满足) {
            status.setRollbackOnly(); // 手动回滚
            return null;
        }
        userMapper.increase(100);
        return null;
    });
}

选择:简单业务用注解,复杂流程(需要中途判断/部分提交)用 TransactionTemplate。

小结

  • @Transactional = AOP 代理决定 commit/rollback + 连接承载事务 + undo log 真正回滚
  • 回滚是数据库做的(undo log 撤销本事务修改),不是"数据库恢复"
  • 传播行为:REQUIRED 搭便车、REQUIRES_NEW 自己开车
  • 失效重灾区:同类调用、非 public、异常被 catch、checked 异常
  • 默认只回滚 RuntimeException,业务上记得加 rollbackFor = Exception.class

下一篇可以看我的《Spring Bean 详解》(了解 Bean 生命周期和代理),或《系统架构设计常见问题 FAQ》里的"分布式事务"(事务跨服务时为什么难搞、怎么解决)。