Spring 事务传播机制 & 事务失效场景

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 代理增强,事务代码不会执行。

同类内部调用解决方案

  1. 从 Spring 上下文获取自身代理对象再调用;
  2. 将带事务的方法拆分到另一个 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 不会创建全新独立事务,只在父事务中建立一个保存点。

两种情况:

  1. 只有子方法 B 报错:只回滚到这个保存点,外层 A 前面操作可以保留、继续提交;
  2. 如果外层父事务整体报错:整条大船全部回滚,子事务跟着一起回滚。

和 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:依附父事务保存点,父事务整体回滚,子事务一起回滚。

总结

  1. 默认传播行为是 REQUIRED,日常增删改业务首选;
  2. REQUIRES_NEW 适合日志、记录类隔离场景,事务完全独立;
  3. NESTED 用于批量局部回滚,依赖数据库保存点;
  4. SUPPORTS 适配查询,NOT_SUPPORTED 用于大数据导出;
  5. 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 依靠捕获异常触发事务回滚,若捕获后不抛出,框架不知道发生错误,不会回滚。 两种解决方式:

  1. catch 中抛出运行时异常:throw new RuntimeException("失败");
  2. 手动标记回滚:
java 复制代码
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

场景 4:默认只回滚 RuntimeException、Error

普通受检异常 Exception 不会触发回滚,开发规范统一配置:

java 复制代码
@Transactional(rollbackFor = Exception.class)

场景 5:数据库引擎 MyISAM

MyISAM 不支持事务、回滚,必须使用 InnoDB 引擎。

场景 6:传播行为配置错误

配置 NOT_SUPPORTED、NEVER 会强制脱离事务,造成预期外不回滚。

场景 7:多线程异步操作

子线程新开数据库连接,独立事务,子线程异常无法回滚主线程数据。

总结

  1. 高频事务失效三大场景:this 同类调用、非 public 方法、异常被捕获不抛出;
  2. 默认仅回滚运行时异常,业务统一添加rollbackFor = Exception.class
  3. MyISAM 引擎无事务能力,数据库必须使用 InnoDB;
  4. 多线程、错误传播配置也会引发事务失效。

业务落地 ------ 批量短信与独立日志事务场景

批量短信导入业务场景

需求:批量新增多条短信记录,只要一条插入异常,全部数据回滚,不存在部分入库。

标准实现

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 有本质区别。

总结

  1. 全量原子批量(失败全部回滚):使用默认 REQUIRED;
  2. 局部回滚批量(错的单独回滚,正确留存):使用 NESTED;
  3. 日志隔离、独立记录不受主事务影响:使用 REQUIRES_NEW;
  4. 大批量数据避免长事务,建议拆分批次处理,减少锁竞争

经典问题

高频对比: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();
}

全章节整体汇总总结

  1. 底层原理:@Transactional 基于 AOP 动态代理,同类 this 调用会事务失效;
  2. 七大传播行为:默认 REQUIRED;日志隔离用 REQUIRES_NEW;批量局部回滚用 NESTED;查询用 SUPPORTS;大数据导出用 NOT_SUPPORTED;
  3. 事务失效 7 大场景:this 自调用、非 public 方法、异常被捕获不抛出、未配置 rollbackFor、MyISAM 引擎、传播行为配置错误、多线程;
  4. 业务落地:全量批量回滚用 REQUIRED,局部回滚用 NESTED,日志记录用 REQUIRES_NEW;
  5. 核心:分清 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();
}
  1. 上层事务(父事务) 最先发起事务的外层方法 sendSmsTask() 创建出来的事务。 外部最先调用它,它先开启事务,所以叫上层事务。
  2. 子事务 上层方法内部,调用另外一个带@Transactional的方法,这个被调用方法产生的事务,就是子事务。

核心关系:外层调用者 = 上层事务;内层被调用方法 = 子事务

结合传播行为举两个关键例子

例子 1:REQUIRES_NEW

父事务:sendSmsTask(上层事务)

子事务:saveOperateLog(REQUIRES_NEW)

子事务会暂停父事务,新建完全独立事务

父事务回滚,子事务不受影响(日志依旧入库)。

例子 2:NESTED

父事务:批量导入短信(上层事务)

子事务:单条短信保存 子事务不会新建独立事务,只是在父事务里创建保存点

✅ 子事务异常:只回滚到保存点,父事务其他数据可以继续提交

父事务整体回滚:所有嵌套子事务一起回滚

容易混淆的关键点

  1. 只有方法 A 调用方法 B的时候,才存在「上层事务、子事务」;
  2. 如果直接单独调用方法 B,不存在上层事务;
  3. REQUIRED:子方法没有独立事务,直接合并到上层事务,属于同一个事务;
  4. 很多人误区: NESTED ≠ REQUIRES_NEW
  • REQUIRES_NEW:两个完全独立事务(两条数据库连接)
  • NESTED:同一个事务连接,依靠保存点实现局部回滚。

谁先开启事务,谁就是上层事务(父事务); 上层方法内部调用的带事务方法,对应的就是子事务

相关推荐
格兰芬多呼神护卫20 小时前
# Agent 企业评估方案深度分析:从“看答案”到持续质量闭环_码士教育
java·开发语言·jvm
豆角焖肉20 小时前
冒泡、快排、堆排的实现逻辑与选择思路
java·算法·排序算法·快排·冒泡·堆排
这就是佬们吗21 小时前
回溯算法三板斧---掌握「回溯三问」思考模板快速入门回溯
java·数据库·算法
SelectDB21 小时前
SelectDB search() 实战教程:从 Elasticsearch 迁移到一条 SQL 搞定搜索与分析
后端
SelectDB21 小时前
Apache Doris / SelectDB 全栈实战教程:从 ClickBench 全球登顶到 AI Native 部署落地
后端
qq_1715388521 小时前
深入浅出 MyBatis XML:从入门到精通
xml·java·mybatis
SelectDB21 小时前
Apache Doris HTAP 实战教程:PostgreSQL 实时分析从零搭建
后端
SelectDB21 小时前
SelectDB 实战教程:从部署到 AI 混合检索的完整实践
后端
qq_1715388521 小时前
Spring TransactionSynchronizationManager:事务同步的幕后指挥官
java·后端·spring