Spring事务的传播机制

一、最常用的三种传播机制

1. REQUIRED(默认,最常用)

这是Spring的默认传播行为,也是使用最多的。行为规则:
  • 如果当前存在事务,就加入这个事务
  • 如果当前没有事务,就新建一个事务
实际场景:比如我有一个转账业务,调用了扣款和加款两个方法:
  • 转账方法开启了事务A
  • 扣款方法用REQUIRED,会加入事务A
  • 加款方法用REQUIRED,也会加入事务A
  • 三个方法在同一个事务中,任何一个失败,全部回滚
这就保证了转账的原子性,要么全成功,要么全失败。这是最符合我们日常业务需求的。重要特点:外层事务回滚,内层也会回滚;内层抛异常,外层也会回滚(除非捕获并处理)。

2. REQUIRES_NEW(独立事务)

这个也很常用,特别是在需要独立提交的场景。行为规则:
  • 无论当前是否存在事务,都会新建一个事务
  • 如果当前存在事务,会把当前事务挂起
  • 新事务和外层事务完全独立,互不影响
实际场景:我在项目中最常用的场景是记录操作日志:假设用户修改订单,我需要记录修改日志:
  • 修改订单方法开启了事务A
  • 记录日志方法用REQUIRES_NEW,会暂停事务A,开启新事务B
  • 日志记录完成,事务B提交,然后恢复事务A
  • 即使订单修改失败回滚了,日志依然保存成功
这样做的好处是:无论业务成功失败,操作日志都要记录下来,方便后续审计和排查问题。重要特点:两个事务完全独立,外层回滚不影响内层,内层回滚也不影响外层。但要注意数据库连接池的消耗,因为同时要维护两个事务。

3. SUPPORTS(支持事务)

行为规则:
  • 如果当前存在事务,就加入这个事务
  • 如果当前没有事务,就以非事务方式执行
实际场景:适合查询操作。比如查询用户信息:
  • 如果在一个事务流程中调用查询,可以读到事务中未提交的数据(可重复读)
  • 如果单独调用查询,不需要事务,减少开销
说实话,这个传播级别用得不多,因为查询操作一般不加事务注解。

二、不常用但需要理解的四种

4. MANDATORY(强制要求事务)

行为规则:
  • 如果当前存在事务,就加入这个事务
  • 如果当前没有事务,就抛出异常
实际场景:适合那些必须在事务中执行的方法。比如某些核心的数据库更新操作,强制要求调用方必须开启事务。我在实际项目中很少用,因为用REQUIRED就够了。MANDATORY更多是一种防御性编程,明确告诉调用方:你必须在事务中调用我。

5. NOT_SUPPORTED(不支持事务)

行为规则:
  • 如果当前存在事务,就把事务挂起
  • 总是以非事务方式执行
实际场景:适合那些不应该在事务中执行的操作,比如发送邮件、调用第三方接口。举个例子:
  • 用户下单成功后发送邮件通知
  • 发送邮件方法用NOT_SUPPORTED
  • 即使在事务中调用,也会暂停事务,以非事务方式发送邮件
  • 这样邮件发送的耗时不会占用数据库事务时间
但我更推荐把这类操作做成异步,而不是用NOT_SUPPORTED。

6. NEVER(禁止事务)

行为规则:
  • 总是以非事务方式执行
  • 如果当前存在事务,就抛出异常
实际场景:比MANDATORY更严格,强制不能在事务中调用。实际项目中几乎不用,因为如果真的不能在事务中执行,更应该把这个逻辑独立出来,而不是靠这个传播级别来限制。

7. NESTED(嵌套事务)

这个比较特殊,也是面试的重点。行为规则:
  • 如果当前存在事务,就在嵌套事务中执行
  • 如果当前没有事务,行为同REQUIRED,创建新事务
什么是嵌套事务:
  • 使用数据库的Savepoint保存点机制
  • 内层事务是外层事务的一部分
  • 内层可以独立回滚到保存点,不影响外层
  • 但外层回滚,内层也会回滚
与REQUIRES_NEW的区别:这是面试常问的!NESTED:
  • 内层是外层的一部分,用的是同一个物理事务
  • 外层回滚,内层必然回滚
  • 内层回滚,可以只回滚到保存点,外层可以继续
  • 只用一个数据库连接
REQUIRES_NEW:
  • 内层是完全独立的事务
  • 外层回滚,不影响内层(已经提交了)
  • 内层回滚,不影响外层
  • 需要两个数据库连接
实际场景:批量处理数据,允许部分失败:比如批量导入用户数据:
  • 外层事务处理整个批量操作
  • 每条数据用NESTED处理
  • 某条数据失败,只回滚这条,记录失败原因
  • 其他数据继续处理
  • 最后外层事务统一提交
注意事项:NESTED依赖数据库的Savepoint机制,不是所有数据库都支持。MySQL的InnoDB支持,但某些老版本或其他数据库可能不支持。

三、实际项目中的使用建议

根据我的实践经验:使用频率排序:
  1. REQUIRED(90%的场景) - 默认就用它
  1. REQUIRES_NEW(8%的场景) - 日志记录、独立操作
  1. NESTED(1%的场景) - 批量处理允许部分失败
  1. 其他几种(1%的场景) - 几乎不用
常见的业务场景:场景1:订单流程(REQUIRED)
  • 创建订单
  • 扣减库存
  • 扣减余额
  • 增加积分
全部用REQUIRED,在同一个事务中,保证原子性。场景2:操作审计(REQUIRES_NEW)
  • 用户修改敏感数据
  • 记录审计日志用REQUIRES_NEW
  • 无论业务成功失败,日志都要保存
场景3:批量操作(NESTED)
  • 批量导入、批量更新
  • 允许部分失败,记录失败信息
  • 成功的部分要提交

四、常见的坑和误区

误区1:内部调用事务失效

这是最常见的问题!如果在同一个类中,methodA调用methodB,methodB的事务注解会失效。因为没有走代理,直接是this.methodB()。解决方案:
  • 把methodB提取到另一个Service
  • 或者自己注入自己(不优雅)
  • 或者用AspectJ的编译时织入

误区2:异常被捕获导致不回滚

事务只对RuntimeException和Error默认回滚,对受检异常(CheckedException)不回滚。而且如果你catch了异常,事务也不会回滚。我之前就遇到过:一个转账方法,里面catch了异常只打了个日志,结果扣款成功了但加款失败了,事务没回滚,造成了数据不一致。正确做法:
  • 要么不catch,让异常抛出去
  • 要么catch后手动抛出RuntimeException
  • 或者用rollbackFor指定回滚的异常类型

误区3:REQUIRES_NEW的连接池问题

REQUIRES_NEW会暂停外层事务,开启新事务,需要两个数据库连接。如果连接池配置太小,高并发时可能导致连接池耗尽,产生死锁。我在生产环境就遇到过这个问题:连接池只配了10个连接,高峰期出现大量REQUIRES_NEW,导致连接等待超时。解决方案:
  • 合理评估连接池大小
  • 减少REQUIRES_NEW的使用
  • 考虑用异步方式替代

误区4:NESTED的数据库支持问题

不是所有数据库都支持Savepoint。使用前要确认数据库版本是否支持。而且NESTED只能用在支持JDBC的情况下,如果用JTA分布式事务,NESTED不可用。

五、面试加分项:事务隔离级别

面试官可能会顺便问事务的隔离级别,这是配合传播机制一起使用的。Spring支持5种隔离级别:
  • DEFAULT:使用数据库默认隔离级别(MySQL是REPEATABLE_READ)
  • READ_UNCOMMITTED:读未提交,脏读、不可重复读、幻读都可能发生
  • READ_COMMITTED:读已提交,避免脏读(Oracle默认)
  • REPEATABLE_READ:可重复读,避免脏读和不可重复读(MySQL默认)
  • SERIALIZABLE:串行化,最高级别,性能最差
实际项目中,一般用数据库的默认隔离级别,很少手动指定。除非有特殊需求,比如报表查询用READ_UNCOMMITTED提高性能,金融场景用SERIALIZABLE保证强一致性。

总结

核心要点:
  • REQUIRED是默认且最常用的,90%场景用它
  • REQUIRES_NEW用于需要独立提交的场景,如日志记录
  • NESTED用于批量操作允许部分失败
  • 其他几种很少用,了解即可
关键是理解:
  • 传播机制解决的是事务方法互相调用的问题
  • 要避免内部调用导致事务失效
  • 要注意异常处理,不要catch后不抛出
  • REQUIRES_NEW要注意连接池大小

在实际项目中,我们更应该关注的是事务边界的设计,在合适的地方开启事务,控制事务粒度,避免大事务,而不是过度使用各种传播级别。保持简单,用好REQUIRED和REQUIRES_NEW就能解决大部分问题。

 

 

Spring事务传播机制对比表

传播机制当前存在事务当前无事务是否新建物理事务外层回滚对内层影响内层回滚对外层影响使用频率典型场景
REQUIRED<br>(默认) 加入当前事务 新建事务 否(共用) 内层一起回滚 外层也回滚 ⭐⭐⭐⭐⭐<br>90% 普通业务操作<br>保证整体原子性
REQUIRES_NEW 挂起当前事务<br>新建独立事务 新建事务 是(独立) 不影响<br>(已提交) 不影响外层 ⭐⭐⭐⭐<br>8% 操作日志记录<br>独立提交的操作
NESTED 在嵌套事务中执行<br>(Savepoint) 新建事务<br>(同REQUIRED) 否(共用,但有保存点) 内层一起回滚 可以捕获处理<br>回滚到保存点 ⭐⭐<br>1% 批量操作<br>允许部分失败
SUPPORTS 加入当前事务 非事务方式执行 内层一起回滚 外层也回滚 ⭐<br><1% 查询操作
NOT_SUPPORTED 挂起当前事务<br>非事务方式执行 非事务方式执行 无影响 无影响 ⭐<br><1% 发送邮件<br>调用外部接口
MANDATORY 加入当前事务 抛出异常 内层一起回滚 外层也回滚 ⭐<br>几乎不用 强制要求在<br>事务中调用
NEVER 抛出异常 非事务方式执行 - 无影响 ⭐<br>几乎不用 强制不能在<br>事务中调用

posted @ 2026-01-25 20:27  菜鸟~风  阅读(0)  评论(0)    收藏  举报