写这篇文章的起因:一直知道 @Transactional 加在方法上"出错会自动回滚",但从来没深想过为什么能回滚。这次把原理彻底扒了一遍——结论是:Spring 只负责"决定提交还是回滚",真正执行回滚的是数据库的 undo log。三层各司其职,下面一层层讲透。
一、一句话先建立整体认知
@Transactional 的本质 = AOP 代理 + 事务管理器 + 数据库事务:
调用方法 → Spring 代理拦截 → 开启事务 → 执行业务 SQL → 成功 commit / 异常 rollback
↓
数据库用 undo log 执行真正的"撤销"三层分工:
- Spring AOP:决定"成功就提交,失败就回滚"
- 数据库连接:承载事务(
setAutoCommit(false)/commit()/rollback()) - 数据库引擎:用 undo log 真正执行回滚(恢复事务前的数据)
二、为什么能回滚:核心原理(三层)
第 1 层:Spring 的 AOP 代理
@Transactional 标记的方法,Spring 会为它所在的 Bean 生成一个代理对象(CGLIB 或 JDK 动态代理)。你调用方法时,实际调用的是代理,代理在方法前后"织入"事务逻辑:
// 你写的(伪代码理解用)
@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 异常(编译期异常) | 默认只回滚 RuntimeException 和 Error | @Transactional(rollbackFor = Exception.class) |
| 多线程调用 | 新线程用的是新连接 = 新事务,管不到 | 事务内别开线程,或用编程式事务 |
| 方法自调用(private helper) | 同上,不经过代理 | 拆 Bean 或走 public 入口 |
| 代理方式问题 | 接口代理(JDK)时类没实现接口 | 用 CGLIB(proxyTargetClass=true) |
最常见的前三个:同类调用、非 public、异常被 catch——面试必问,线上必踩。
五、回滚规则:哪些异常会回滚
// 默认:只回滚 RuntimeException 和 Error
@Transactional
public void save() { ... } // 抛 SQLException(checked)不回滚!
// 显式指定:所有异常都回滚
@Transactional(rollbackFor = Exception.class)
public void save() { ... }- 默认
rollbackFor是RuntimeException.class和Error.class - 业务里最常见:
@Transactional(rollbackFor = Exception.class)兜底
六、@Transactional vs 编程式事务
| 方式 | 写法 | 优点 | 缺点 |
|---|---|---|---|
| 声明式(@Transactional) | 注解,AOP 自动处理 | 简单、侵入小 | 失效场景多、不能在事务中间手动控制 |
| 编程式(TransactionTemplate) | 手动 execute 回调 | 灵活、边界清晰、可在事务中途决定提交 | 代码侵入 |
@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》里的"分布式事务"(事务跨服务时为什么难搞、怎么解决)。
