Spring 事务:传播行为、失效场景、回滚规则与最佳实践

只会背隔离级别还不够,Java 面试更爱问:

  • 为什么 @Transactional 没生效?
  • 为什么没回滚?
  • 传播行为怎么选?

你必须记住的 3 句话(面试直出):

  • Spring 事务的本质是 AOP 代理 + 在方法边界管理连接与提交/回滚
  • @Transactional 最常见失效原因是 自调用(同类方法互调)绕过代理
  • 回滚默认只对 RuntimeException/Error,想对受检异常回滚需要显式配置。

1. Spring 事务在做什么:把"事务边界"绑在方法上

当你写:

  • @Transactional public void foo(){...}

Spring 通过代理拦截调用,在方法前后做:

  • 开启事务(拿连接、设置隔离级别/只读等)
  • 执行业务
  • 发生异常按规则回滚,否则提交

面试加分表达:

  • Spring 事务不是数据库事务的替代品,它只是 把 JDBC 事务的 begin/commit/rollback 管理自动化

2. 传播行为:不要背全套,先抓住 3 个最常用

2.1 REQUIRED(默认)

  • 有事务就加入,没有就新建
  • 适用:绝大多数业务服务方法

2.2 REQUIRES_NEW

  • 总是新开一个事务,挂起外层事务
  • 适用:
    • 你希望"外层失败不影响内层落库"(例如记录审计日志)
    • 或希望内层失败可独立回滚

风险提示:

  • 使用不当会导致事务碎片化、吞吐下降、锁冲突更复杂

2.3 NESTED

  • 基于 savepoint(保存点)的嵌套事务
  • 适用:部分回滚,但仍希望跟随外层一起提交

注意:

  • 是否真正生效与数据库/事务管理器能力有关

3. 最常见的"事务失效"清单(线上 80% 都在这)

  • 失效 1:自调用绕过代理
    • 同一个类里 A 方法调用 B 方法,B 上的 @Transactional 不生效。

典型长相(你线上真的会遇到):

  • createOrder() 里直接调用同类的 saveOrder(),而 saveOrder() 才标了 @Transactional

更稳的修复方式(按推荐顺序):

  • 拆分 Bean:把需要事务的方法挪到另一个 Service,让调用经过代理(最推荐)

  • 自注入代理调用:通过注入自身的代理对象来调用(需要小心循环依赖与可读性)

  • 不推荐的技巧 :依赖 AopContext.currentProxy()(侵入性强,容易踩配置坑)

  • 失效 2:方法不是 public

    • 常见代理策略下,非 public 可能无法被代理。
  • 失效 3:异常被吞掉

    • catch 了异常但没抛出,Spring 认为正常结束就会提交。
  • 失效 4:多数据源/多事务管理器没指定

    • 事务开在 A 库,SQL 却跑到 B 库。
  • 失效 5:异步/新线程

    • 新线程不继承线程绑定的连接与事务上下文。

4. 回滚规则:为什么没回滚

默认规则:

  • RuntimeException / Error -> 回滚
  • Checked Exception -> 不回滚

你应该能顺口说出:

  • 受检异常需要 rollbackFor = Exception.class 才会回滚。

常见"看似回滚但其实没回滚"的场景:

  • 事务传播把内部事务独立提交(REQUIRES_NEW
  • 你在外层 catch 了异常并吞掉

再补一个高频线上边界(尤其是"写库 + 发消息/写 MQ"):

  • 事务回滚只能回滚数据库,回滚不了已经发出去的消息/调用出去的 RPC
  • 如果你用 REQUIRES_NEW 让"日志/消息"独立提交,要能接受:外层失败时日志/消息仍然存在

工程建议:

  • 需要强一致的"落库 + 发消息",优先考虑 Outbox/本地消息表/CDC 这类模式,而不是靠传播行为硬凑

5. 实战最佳实践(把事务写得更稳)

  • 原则 1:缩短事务

    • 不要在事务里做 RPC/大 IO/长时间计算。
  • 原则 2:一类方法一个事务边界

    • 把"纯查询/纯计算"与"写库"分开。
  • 原则 3:对外部接口做幂等

    • 事务回滚只能回滚数据库,回滚不了外部副作用。
  • 原则 4:明确只读事务

    • 读多场景可以 readOnly=true(配合连接/数据库优化),但不要迷信它能解决锁问题。
  • 原则 5:必要时做重试,但要识别死锁/锁等待

    • 死锁回滚是正常机制,但重试必须保证幂等。

6. 线上排查:事务相关问题怎么定位

  • 现象 1:数据没回滚

    • 看异常类型是否触发回滚
    • 看是否异常被 catch
    • 看是否自调用绕过代理
  • 现象 2:接口变慢/超时

    • 先按"锁等待/慢 SQL"排查
    • 再看是否事务过长导致持锁时间长
  • 现象 3:部分数据提交、部分没提交

    • 重点检查传播行为(尤其是 REQUIRES_NEW)与多数据源

7. 自测清单(你要能顺口讲出来)

  • Q:@Transactional 为什么经常失效?

    • A:因为事务是代理生效,最常见是同类自调用绕过代理。
  • Q:为什么抛了异常却没回滚?

    • A:默认只回滚运行时异常;受检异常需要配置 rollbackFor
  • Q:REQUIRES_NEW 适合什么场景?

    • A:需要独立提交/回滚的小事务,例如审计日志;但会增加复杂度与冲突。

8. 30 秒背诵稿

Spring 事务本质是 AOP 代理在方法边界自动管理 JDBC 事务,最常见失效是自调用绕过代理。传播行为里 REQUIRED 默认加入或新建事务,REQUIRES_NEW 会挂起外层并开启独立事务,NESTED 基于保存点做部分回滚。回滚默认只对 RuntimeException/Error 生效,受检异常需要 rollbackFor。线上问题优先检查代理调用路径、异常是否被吞、传播行为与多数据源绑定,再结合锁等待/慢 SQL 判断是否长事务导致抖动。

相关推荐
用户3167361303422 小时前
SSE消息推送前后端代码
前端·后端
搬搬砖得了2 小时前
当 GraphQL 变成“全家桶”,Stream 写成“天书”,老板变身“谜语人”:我在代码屎山里的渡劫日常
后端
默海笑2 小时前
Java 基础 12:JavaDoc 生成文档 学习笔记
后端
写Cpp的小黑黑2 小时前
React Native 项目实战指南
后端
timi先生2 小时前
语料库全栈项目部署 (Vue + Java + CQPweb)
java·前端·vue.js
sunwenjian8863 小时前
Java进阶--IO流
java·开发语言
客卿1233 小时前
滑动窗口--模板
java·算法
G探险者3 小时前
如何找到那些慢 SQL?
后端·sql
敖正炀3 小时前
线程池拒绝策略场景分析
后端