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 判断是否长事务导致抖动。

相关推荐
wand codemonkey14 分钟前
SpringbootWeb【入门】+MySQL【安装】+【DataDrip安装 】+【连接MySQL】
java·mysql·mybatis
Mahir088 小时前
Spring 循环依赖深度解密:从问题本质到三级缓存源码级解析
java·后端·spring·缓存·面试·循环依赖·三级缓存
RyFit9 小时前
SpringAI 常见问题及解决方案大全
java·ai
石山代码10 小时前
C++ 内存分区 堆区
java·开发语言·c++
绝知此事10 小时前
【算法突围 01】线性结构与哈希表:后端开发的收纳术
java·数据结构·算法·面试·jdk·散列表
无风听海10 小时前
C# 隐式转换深度解析
java·开发语言·c#
一只大袋鼠11 小时前
Git 进阶(二):分支管理、暂存栈、远程仓库与多人协作
java·开发语言·git
德思特11 小时前
从 Dify 配置页理解 RAG 的重要参数
java·人工智能·llm·dify·rag
YOU OU12 小时前
Spring IoC&DI
java·数据库·spring
один but you12 小时前
从可变参数到 emplace:现代 C++ 性能优化的核心组合
java·开发语言