Spring 事务底层原理 & @Transactional 实现机制
底层核心逻辑
@Transactional 是基于 Spring AOP 动态代理实现的:
- 目标类实现接口 → 使用 JDK 动态代理
- 目标类无接口 → 使用 CGLIB 子类代理 容器启动时会为加了事务方法的 Service 生成代理对象,代理对象包裹原始类,在方法前后增加事务开启、提交、回滚逻辑。
事务生效必要条件
必须外部调用代理对象的方法,切面才能拦截到事务逻辑。 举个反例:
java
@Service
public class SmsService {
public void batchSave() {
// this 是原始对象,不是代理!
this.saveSingle();
}
@Transactional
public void saveSingle() {
// 事务失效
}
}
原因:this指向原生对象,没有经过 AOP 代理增强,事务代码不会执行。
同类内部调用解决方案
- 从 Spring 上下文获取自身代理对象再调用;
- 将带事务的方法拆分到另一个 Service,跨类调用。
Spring 七大事务传播行为 Propagation
传播行为作用
控制:当一个带事务的方法调用另一个事务方法时,子方法如何处理当前事务。
七大传播行为详解
| 传播类型 | 规则说明 | 业务场景 |
|---|---|---|
| REQUIRED(默认) | 上层有事务就加入;无则新建事务 | 短信新增、修改、下发等核心写业务 |
| REQUIRES_NEW | 无论是否存在事务,都新建独立事务,互不影响 | 操作日志、异常记录,主事务回滚日志仍入库 |
| SUPPORTS | 有事务就共用,无事务则无事务执行 | 列表查询、统计查询 |
| NOT_SUPPORTED | 挂起上层事务,以无事务运行 | 大数据导出、海量数据统计 |
| MANDATORY | 强制上层必须存在事务,不存在直接抛异常 | 底层内部业务方法,不允许无事务调用 |
| NEVER | 禁止上层存在事务,有事务直接抛异常 | 只读查询,严禁写库操作 |
| NESTED | 有父事务创建保存点子事务;无则新建事务 | 批量导入短信,部分失败局部回滚 |
外层方法 A(已经开启事务 → 上层 / 父事务),调用内层方法 B。
我们讨论:方法 B 该怎么处理当前已经存在的父事务。
如果外层 A没有事务,也会一并说明。
REQUIRED(默认,最常用)
大白话:有船就上船,没船自己造一艘船
- 外层 A 有事务:B 直接加入这个父事务,大家同坐一条船。 只要任意一处报错,整条船翻,全部一起回滚。
- 外层 A 没有事务:B 自己新建事务。
- 使用场景:普通新增、修改短信(要求全部成功 / 全部失败)
REQUIRES_NEW
大白话:不管外面有没有船,我直接新开一条独立小船
执行 B 的时候:先把父事务暂停,自己新开全新事务。 两条船互不干涉!
父事务翻车回滚 ≠ 子事务翻车;子事务提交了就永久生效。
业务例子:短信下发失败,主业务回滚,操作日志依旧保存。
重点:两个独立事务,两条数据库连接。
SUPPORTS
大白话:随缘,有船我就搭顺风船,没船我就游泳(无事务)
- 外层有事务:并入父事务;
- 外层没有事务:B 直接无事务运行。
- 使用场景:单纯查询接口,不需要强制事务。
NOT_SUPPORTED
大白话:拒绝坐船!外面有船,我先让大船靠边暂停,我裸泳执行
外层 A 存在事务 → 先挂起父事务,B 在无事务环境执行;
B 执行完之后,再恢复之前暂停的父事务。
风险:B 里面操作数据库没有事务,出现异常无法回滚。
场景:大数据导出、海量统计,不想占用事务长时间锁表。
MANDATORY
大白话:强制要求外面必须有大船!没有船我直接罢工抛异常
- 外层有事务:正常加入父事务执行;
- 外层没有事务:直接抛出异常,拒绝运行。
- 场景:底层内部方法,规定必须在外层事务中调用,禁止单独执行。
NEVER
大白话:严禁坐船!外面但凡有大船,直接罢工报错
- 外层有事务 → 抛出异常,不让执行;
- 外层没有事务 → 正常无事务执行。
- 场景:纯只读查询,防止有人错误加写操作、污染事务。
NESTED(嵌套事务)
大白话:不新开小船,在当前大船上面标记救生点(保存点)
前提:外层 A 有事务 B 不会创建全新独立事务,只在父事务中建立一个保存点。
两种情况:
- 只有子方法 B 报错:只回滚到这个保存点,外层 A 前面操作可以保留、继续提交;
- 如果外层父事务整体报错:整条大船全部回滚,子事务跟着一起回滚。
和 REQUIRES_NEW 核心区别:
NESTED = 同一条船,只有一个数据库连接;
REQUIRES_NEW = 两条独立小船,两个连接。
场景:批量导入短信,错误条目局部回滚,正确数据保留。
背诵口诀
REQUIRED 默认;REQUIRES_NEW 新开;SUPPORTS 随缘;NOT_SUPPORTED 挂起;MANDATORY 必须有;NEVER 禁止;NESTED 嵌套保存点
REQUIRED:有则共用,无则新建
REQUIRES_NEW:强行新开,互不影响
SUPPORTS:有就共用,没有不用
NOT_SUPPORTED:挂起事务,裸奔执行
MANDATORY:必须有事务,不然报错
NEVER:不能有事务,有就报错
NESTED:大船打标记,支持局部回滚
易混区分:REQUIRES_NEW vs NESTED
- REQUIRES_NEW:完全独立新事务,父事务回滚不影响子事务;
- NESTED:依附父事务保存点,父事务整体回滚,子事务一起回滚。
总结
- 默认传播行为是 REQUIRED,日常增删改业务首选;
- REQUIRES_NEW 适合日志、记录类隔离场景,事务完全独立;
- NESTED 用于批量局部回滚,依赖数据库保存点;
- SUPPORTS 适配查询,NOT_SUPPORTED 用于大数据导出;
- MANDATORY、NEVER 多用于底层约束校验,业务开发很少用。
七大事务失效场景
场景 1:同类内部 this 自调用
java
@Service
public class SmsService {
public void batch() {
this.save();
}
@Transactional
public void save() {}
}
原因:this 指向原生对象,并非 AOP 代理对象,事务增强逻辑不会执行。
解决:拆分到其他 Service、获取自身代理对象调用。
场景 2:方法非 public(private/protected)
Spring AOP 动态代理只能拦截 public 修饰的方法,私有方法不会生成代理增强,事务直接失效。
场景 3:异常被 try-catch 捕获,未重新抛出
Spring 依靠捕获异常触发事务回滚,若捕获后不抛出,框架不知道发生错误,不会回滚。 两种解决方式:
- catch 中抛出运行时异常:
throw new RuntimeException("失败"); - 手动标记回滚:
java
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
场景 4:默认只回滚 RuntimeException、Error
普通受检异常 Exception 不会触发回滚,开发规范统一配置:
java
@Transactional(rollbackFor = Exception.class)
场景 5:数据库引擎 MyISAM
MyISAM 不支持事务、回滚,必须使用 InnoDB 引擎。
场景 6:传播行为配置错误
配置 NOT_SUPPORTED、NEVER 会强制脱离事务,造成预期外不回滚。
场景 7:多线程异步操作
子线程新开数据库连接,独立事务,子线程异常无法回滚主线程数据。
总结
- 高频事务失效三大场景:this 同类调用、非 public 方法、异常被捕获不抛出;
- 默认仅回滚运行时异常,业务统一添加
rollbackFor = Exception.class; - MyISAM 引擎无事务能力,数据库必须使用 InnoDB;
- 多线程、错误传播配置也会引发事务失效。
业务落地 ------ 批量短信与独立日志事务场景
批量短信导入业务场景
需求:批量新增多条短信记录,只要一条插入异常,全部数据回滚,不存在部分入库。
标准实现
java
@Transactional(rollbackFor = Exception.class)
public void batchAddSms(List<Sms> list) {
for (Sms sms : list) {
smsMapper.insert(sms);
}
}
默认传播行为 REQUIRED,所有数据库操作属于同一个事务,异常统一回滚。
开发踩坑点
- 循环内单独 try-catch 捕获单条异常:异常被吞噬,整体事务不会回滚,产生脏数据;
- 超大批量一次性写入:长事务会占用数据库行锁,引发阻塞、性能下降,建议分批次处理。
短信操作日志隔离场景
需求:短信下发失败,主业务数据回滚,但异常日志必须持久化保存。 解决方案:日志保存方法配置Propagation.REQUIRES_NEW
- 生成全新独立事务,和主事务完全隔离;
- 主事务回滚、提交,均不会影响日志事务的提交。
NESTED 嵌套事务适用场景
适合:批量操作,允许单独失败数据回滚,其余数据正常保存。
例:批量导入客户短信,某几条数据格式错误,仅回滚错误条目,正确数据保留。
底层依赖 MySQL InnoDB 保存点,父事务全部回滚时,嵌套子事务也会一并回滚,和 REQUIRES_NEW 有本质区别。
总结
- 全量原子批量(失败全部回滚):使用默认 REQUIRED;
- 局部回滚批量(错的单独回滚,正确留存):使用 NESTED;
- 日志隔离、独立记录不受主事务影响:使用 REQUIRES_NEW;
- 大批量数据避免长事务,建议拆分批次处理,减少锁竞争
经典问题
高频对比:REQUIRES_NEW vs NESTED
1)REQUIRES_NEW
- 新开完全独立事务,新连接、新事务上下文;
- 父事务回滚,子事务依旧可以正常提交; 适用:操作日志、异常记录、埋点数据。
2)NESTED
- 不新建独立事务,仅在父事务内创建保存点;
- 子事务单独异常:仅回滚到当前保存点,父事务其他操作可正常提交;
- 父事务整体回滚:所有嵌套子事务全部跟着回滚; 适用:批量导入,部分失败局部回滚。
try-catch 场景手动回滚两种写法
场景:业务需要捕获异常做自定义处理,但仍然要触发事务回滚。
方式 1:捕获后抛出运行时异常
java
@Transactional(rollbackFor = Exception.class)
public void test() {
try {
// 数据库操作
} catch (Exception e) {
log.error("异常", e);
throw new RuntimeException("业务失败");
}
}
方式 2:手动标记事务回滚(不抛异常也能回滚)
java
import org.springframework.transaction.support.TransactionAspectSupport;
try {
// 业务操作
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
全章节整体汇总总结
- 底层原理:@Transactional 基于 AOP 动态代理,同类 this 调用会事务失效;
- 七大传播行为:默认 REQUIRED;日志隔离用 REQUIRES_NEW;批量局部回滚用 NESTED;查询用 SUPPORTS;大数据导出用 NOT_SUPPORTED;
- 事务失效 7 大场景:this 自调用、非 public 方法、异常被捕获不抛出、未配置 rollbackFor、MyISAM 引擎、传播行为配置错误、多线程;
- 业务落地:全量批量回滚用 REQUIRED,局部回滚用 NESTED,日志记录用 REQUIRES_NEW;
- 核心:分清 REQUIRES_NEW 与 NESTED 区别、手动回滚两种写法、事务失效高频场景。
上层事务、子事务
上层事务 = 父事务
被调用方法的事务 = 子事务
举一个短信业务场景
java
// 方法A:上层(父)方法
@Transactional // 【父事务/上层事务】传播行为 REQUIRED
public void sendSmsTask() {
smsMapper.insertRecord();
// 调用方法B
logService.saveOperateLog();
}
// 方法B:下层(子)方法
@Transactional(propagation = Propagation.REQUIRES_NEW) //【子事务】
public void saveOperateLog(){
logMapper.insertLog();
}
- 上层事务(父事务) 最先发起事务的外层方法
sendSmsTask()创建出来的事务。 外部最先调用它,它先开启事务,所以叫上层事务。 - 子事务 上层方法内部,调用另外一个带
@Transactional的方法,这个被调用方法产生的事务,就是子事务。
核心关系:外层调用者 = 上层事务;内层被调用方法 = 子事务
结合传播行为举两个关键例子
例子 1:REQUIRES_NEW
父事务:sendSmsTask(上层事务)
子事务:saveOperateLog(REQUIRES_NEW)
子事务会暂停父事务,新建完全独立事务。
父事务回滚,子事务不受影响(日志依旧入库)。
例子 2:NESTED
父事务:批量导入短信(上层事务)
子事务:单条短信保存 子事务不会新建独立事务,只是在父事务里创建保存点。
✅ 子事务异常:只回滚到保存点,父事务其他数据可以继续提交
❌ 父事务整体回滚:所有嵌套子事务一起回滚
容易混淆的关键点
- 只有方法 A 调用方法 B的时候,才存在「上层事务、子事务」;
- 如果直接单独调用方法 B,不存在上层事务;
- REQUIRED:子方法没有独立事务,直接合并到上层事务,属于同一个事务;
- 很多人误区: NESTED ≠ REQUIRES_NEW
- REQUIRES_NEW:两个完全独立事务(两条数据库连接)
- NESTED:同一个事务连接,依靠保存点实现局部回滚。
谁先开启事务,谁就是上层事务(父事务); 上层方法内部调用的带事务方法,对应的就是子事务