Spring事务核心全解析:隔离级别、传播行为与失效场景

发布时间:2026/10/14 23:58:35
Spring事务核心全解析:隔离级别、传播行为与失效场景 Spring 事务这个板块面试问来问去基本绕不开两个点隔离级别和传播行为。我见过太多候选人八股文背得滚瓜烂熟什么REQUIRED 表示如果当前没有事务就新建一个都能脱口而出但面试官只要稍微换个角度——那你项目里哪个方法用了 REQUIRES_NEW为什么——立刻就卡住了。这篇是 Spring 事务系列的第二篇专门把事务的核心 API 和属性讲透重点是带着大家搞明白 API 之间怎么协作、每个属性背后的设计意图、面试官从哪个角度追问、实际项目里怎么用不搞虚的。如果你正准备面试或者接手了一个事务问题频出的老项目这篇文章值得花二十分钟读完。文中所有结论我都结合了源码逻辑和本地跑过的实验不只有结论还有验证过程。1. 先理清三个API的分工TransactionDefinition、PlatformTransactionManager、TransactionStatus怎么协作很多人学 Spring 事务上来就学 Transactional 注解注解确实简单但真要理解事务行为绕不开底下的三个核心接口。搞懂这三个东西的分工面试时被问到事务底层怎么运作你就能从背结论升级成讲原理。1.1 三个接口各自扮演什么角色我用一个生活化的类比来帮助理解去银行柜台办业务你需要三样东西——业务单、柜员、回执。TransactionDefinition 就是那张业务单上面写明你要办什么这笔业务需不需要开新账号传播行为、对别人的操作有什么容忍度隔离级别、多久没办完就作废超时时间、业务中能不能写只读标志。它是一份配置描述本身不干任何活。PlatformTransactionManager 是柜员真正执行操作的人。拿到业务单后他决定是给你开个新账号还是复用之前的处理完给你回执最后根据情况盖章提交或者作废回滚。TransactionStatus 就是那张回执记录了当前这比业务的实际状态是不是新开的账号、有没有设过待作废标记、有没有保存点、能不能刷一下数据。三个接口配合起来事务才有了完整的生命周期。1.2 从源码角度看三者如何串联直接看接口定义能更直观地感受它们的分工。TransactionDefinition 的核心定义如下public interface TransactionDefinition { // 传播行为常量取值 0-6 int PROPAGATION_REQUIRED 0; int PROPAGATION_SUPPORTS 1; int PROPAGATION_MANDATORY 2; int PROPAGATION_REQUIRES_NEW 3; int PROPAGATION_NOT_SUPPORTED 4; int PROPAGATION_NEVER 5; int PROPAGATION_NESTED 6; // 隔离级别常量 int ISOLATION_DEFAULT -1; int ISOLATION_READ_UNCOMMITTED 1; int ISOLATION_READ_COMMITTED 2; int ISOLATION_REPEATABLE_READ 4; int ISOLATION_SERIALIZABLE 8; // 默认无超时限制 int TIMEOUT_DEFAULT -1; // 获取配置 int getPropagationBehavior(); int getIsolationLevel(); int getTimeout(); boolean isReadOnly(); String getName(); }注意这些常量都是 int而不是枚举。这是老版本的设计遗留Spring 为了向后兼容一直没有改。面试官如果问为什么传播行为用 int 不用枚举你可以回答为了保持接口二进制兼容早期 JDK 枚举还不成熟Spring 选择了常量定义后续为了不破坏已有实现类一直保留了这个设计。这算一个比较深的加分点。PlatformTransactionManager 就更简单了就三个方法public interface PlatformTransactionManager extends TransactionManager { TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; }整个事务生命周期的操作就浓缩在这三个方法里获取事务、提交、回滚。具体怎么获取、怎么提交由不同实现类完成Spring 不做硬性要求。TransactionStatus 接口的核心方法public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable { boolean hasSavepoint(); void flush(); } // TransactionExecution 接口 public interface TransactionExecution { boolean isNewTransaction(); // 是不是新开的事务 void setRollbackOnly(); // 标记只能回滚 boolean isRollbackOnly(); boolean isCompleted(); // 是否已结束 }实际开发中谁会用 TransactionStatus用编程式事务的时候会碰。比如 TransactionTemplate 回调里可以拿到 status 手动做 setRollbackOnlyService public class OrderService { private final TransactionTemplate transactionTemplate; public OrderService(TransactionTemplate transactionTemplate) { this.transactionTemplate transactionTemplate; } public void updateOrderAndLog() { transactionTemplate.execute(status - { try { // 业务代码更新订单、写入操作日志 doUpdateOrder(); doInsertLog(); return null; } catch (RuntimeException e) { // 手动标记回滚同时还能做点别的操作 status.setRollbackOnly(); throw e; } }); } }那 Transactional 声明式事务是不是就不用管这些 API 了不是。声明式事务底层就是通过 AOP 拦截方法把注解上的属性组装成一个 TransactionDefinition然后调用 PlatformTransactionManager 的方法。当你深入理解这三个接口之后再遇到声明式事务的异常行为排查思路会清楚很多。1.3 AbstractPlatformTransactionManager 的协调流程实际项目中绝大多数用的是 AbstractPlatformTransactionManager 这个抽象基类的逻辑。它干了这么几件事第一步调用 getTransaction()根据 TransactionDefinition 里的传播行为判断该新建事务还是加入已有事务。第二步如果当前线程 ThreadLocal 里已经绑定了事务资源走加入路径如果传播行为要求新建就挂起当前事务创建新事务。第三步业务方法执行完毕后在事务同步回调里决定 commit 还是 rollback。这里我要特别提醒一点整个流程里最容易被忽略的是事务资源绑定到当前线程这个机制。Spring 事务默认是基于 ThreadLocal 的也就是说同一个线程内的事务调用链才能共享同一个事务。后面讲多线程导致事务失效根子就在这。2. 隔离级别五个档位与三种并发问题的对照面试官喜欢在这里连问三层隔离级别是事务面试的必考项但很多人局限在背概念。我的经验是面试官真正想验证的是你对并发问题如何产生、隔离级别如何解决、数据库底层如何实现这条链路有没有完整认知。2.1 三种并发问题先搞明白不然隔离级别就是空中楼阁先说脏读。事务 A 修改了一条数据但还没提交事务 B 读到了这个未提交的数据然后 A 回滚了B 拿到的就是一坨不存在的垃圾数据。举个例子A 把商品库存从 100 改成 50还没提交B 这时查询库存看到 50下单卖了 10 件后库存减 10 变成了 40基于脏数据计算然后 A 回滚库存恢复 100。B 的操作就建立在一个错误前提上。非重复读是另一个层面的问题。事务 B 内两次读取同一行数据第一次读到的值是 100第二次读到的值变成了 90因为事务 A 在这期间修改并提交了这条数据。注意这里关键点是B 在自己的事务里同样的查询条件结果不一样了。幻读更隐蔽。事务 B 按某个条件范围查询第一次查出 10 条记录第二次查出 11 条多出来的那条是事务 A 在这期间插入并提交的。跟非重复读的区别在于非重复读针对的是同一行数据内容变化幻读针对的是结果集行数变化。这三个问题可以用一张表对应起来并发问题本质涉及操作产生原因脏读读到未提交的中间数据更新读取未提交数据被其他事务读取非重复读同一行两次读取值不同更新/删除读取行级锁无法阻止已提交修改幻读同一范围两次读取条数不同插入范围读取行级锁无法锁住新增行2.2 五个隔离级别逐一看别只看名字要理解锁的粒度TransactionDefinition 定义了五种隔离级别常量。最关键的认知是隔离级别本质上是锁的策略和可见性规则的组合级别越高并发越弱数据一致性越强。READ_UNCOMMITTED1事务可以读取其他事务未提交的数据。实现上就是读不加锁根本不检查数据是否已提交。实际项目中几乎不用除非对数据一致性零要求、对实时性变态敏感的场景。READ_COMMITTED2只能读已提交的数据。实现上读操作加的是快照读在大多数数据库中通过读已提交版本的机制实现从源头杜绝脏读。但不可重复读和幻读仍可能发生。这是 Oracle 的默认级别。REPEATABLE_READ4事务内多次读取同一数据结果一致。MySQL InnoDB 通过 MVCC多版本并发控制和间隙锁配合在这个级别下不仅解决了非重复读还顺便解决了幻读。这是 MySQL 的默认级别。SERIALIZABLE8最严格事务完全串行执行。实现上对读操作也加共享锁写操作加排他锁事务之间互不干扰。并发效率最低只有对一致性要求极高、并发量很低的场景才会用。这里面有一个经典的面试连环追问REPEATABLE_READ 到底能不能解决幻读如果你直接回答不能那在 MySQL 的语境下就错了。MySQL InnoDB 引擎在 RR 级别下通过 next-key lock记录锁 间隙锁锁住查询范围别的事务在这个间隙内插不进数据所以快照读的幻读被天然规避了。但如果是手动加锁的当前读SELECT ... FOR UPDATERR 级别同样靠间隙锁解决了幻读。所以严谨的答案是理论上 ANSI SQL 标准认为 RR 不解决幻读但 MySQL InnoDB 实际实现了防幻读能力。2.3 配置隔离级别的正确姿势与验证方法声明式事务里配置隔离级别很简单Service public class InventoryService { // 读操作多的场景可以适当放宽标准 Transactional(isolation Isolation.READ_COMMITTED, readOnly true) public ListProduct queryProducts() { return productMapper.selectList(null); } // 涉及金额、库存的核心写操作用默认级别即可 Transactional(isolation Isolation.DEFAULT) public void deductStock(Long productId, Integer count) { // 扣减库存逻辑 } }这里有个容易被忽略的细节isolation 的取值里有一个 ISOLATION_DEFAULT它的真正含义是使用数据库默认的隔离级别。你执行SELECT transaction_isolationMySQL 8.0看到 REPEATABLE-READ就是 InnoDB 的默认值Oracle 默认是 READ COMMITTED。所以同一个 Transactional(isolation Isolation.DEFAULT) 在不同数据库上行为是不一样的跨数据库迁移项目一定要重新审视事务行为。我建议你在本地用两个连接窗口实测一下脏读效果。MySQL 上先把一个连接的隔离级别临时设成 READ_UNCOMMITTED开事务后 update 一条数据不提交再用另一个连接查询就会发现确实能看到未提交的数据。亲手做过一次比背十遍概念都牢。3. 七种传播行为逐个拆解REQUIRED、REQUIRES_NEW、NESTED的实战分水岭传播行为是 Spring 事务面试的另一个大头。说实话日常 CRUD 项目里用到传播行为的场景不多但一旦涉及多服务调用、异步任务、批量处理传播行为就变成决定数据一致性的关键因素。3.1 七种枚举值速查表先用一张表把七种传播行为完整列出来标出每种行为在有事务/无事务两种前提下的表现传播行为当前无事务时当前有事务时典型场景REQUIRED新建事务加入当前事务默认值单事务内多操作SUPPORTS以无事务方式执行加入当前事务查询方法MANDATORY抛出异常加入当前事务强制要求调用方有事务REQUIRES_NEW新建事务挂起当前事务并新建日志、审计等独立提交需求NOT_SUPPORTED以无事务方式执行挂起当前事务特殊查询、长耗时不希望占事务资源NEVER以无事务方式执行抛出异常明确禁止事务环境NESTED新建事务创建保存点子事务部分回滚从面试准备角度看你需要重点掌握三个REQUIRED、REQUIRES_NEW、NESTED。因为这三者最容易混而且正好对应三种不同的需求场景。3.2 REQUIRED 和 REQUIRES_NEW 的差别一根绳子 vs 两根绳子REQUIRED 的逻辑可以理解成一根绳上的蚂蚱外部调内部内层方法不新建事务直接加入外层事务。任何一个环节挂了整个事务一起回滚。代码层面Transactional(propagation Propagation.REQUIRED) public void outerMethod() { innerService.innerMethod(); // 加入同一个事务 // 如果这里抛异常innerMethod 的操作也一起回滚 } Transactional(propagation Propagation.REQUIRED) public void innerMethod() { // 执行一个写操作 }上面这种场景如果 innerMethod 正常执行完但 outerMethod 后面的逻辑抛异常innerMethod 的写操作会被一并回滚。这是大多数业务逻辑期望的行为。REQUIRES_NEW 则是两笔独立的事务内层方法被调用时外层事务被挂起内层新建一个事务执行内层提交后外层恢复执行。内层事务提交或回滚都不影响外层事务的最终结局。典型场景是操作日志主流程事务失败要回滚但日志记录应该保留下来否则排查问题连痕迹都没有。Transactional(propagation Propagation.REQUIRES_NEW) public void saveAuditLog(String message) { logMapper.insert(new AuditLog(message)); }同事之前踩过一个大坑把日志记录方法设置成 REQUIRED结果主业务失败时日志也跟着回滚了线上出了问题什么日志都没留下排查流程硬生生拖了一天。后来改成 REQUIRES_NEW主事务回滚不影响日志落库定位问题快多了。这里我补充说明一下挂起机制当外层事务持有数据库连接时挂起意味着把当前连接的资源解绑并保存状态内层事务重新获取一个新连接。所以 REQUIRES_NEW 会导致两个数据库连接被占用连接池压力会翻倍。高并发场景下滥用 REQUIRES_NEW很容易把连接池打满这是生产环境的一个隐形雷区。3.3 NESTED 的精髓保存点 savepointNESTED 在行为上介于两者之间外层有事务时它不新建独立事务而是在当前事务里创建一个保存点内层方法回滚时只回滚到保存点外层还可以继续执行或者决定最终是否提交。Transactional(propagation Propagation.REQUIRED) public void batchProcess() { for (int i 0; i itemList.size(); i) { try { itemProcessor.processItem(itemList.get(i)); // Transactional(propagation Propagation.NESTED) } catch (BusinessException e) { // 记录第 i 条失败继续处理剩下的 errorCollector.add(i); } } // 所有成功的子项随外层统一提交 }上面这个批量导入场景就是 NESTED 的经典用法每条数据独立保存点失败的单条回滚到保存点不影响其他数据最终全部成功的数据随外层事务一起提交。如果用 REQUIRES_NEW每条数据单独成一个事务批量处理几千条就需要几千个事务性能很差而且失败数据的回滚是彻底的独立回滚语义上不如 NESTED 精确。NESTED 在底层实现上有前提需要数据库支持保存点savepoint。MySQL InnoDB、PostgreSQL、Oracle 都支持但某些老版本数据库或者特定连接池配置可能不生效。如果数据库不支持保存点Spring 会回退到 REQUIRED 的行为这一点面试时可以主动提显得你了解边界条件。还有一个容易踩的坑NESTED 内层方法的回滚操作不会把外层事务标记为只能回滚所以外层可以继续执行并最终提交。这一点跟 REQUIRED 有本质区别——REQUIRED 内层如果抛了异常即便外层 catch 住了事务也可能因为标记了 rollback-only 而最终回滚。这是很多初学者完全没想到的隐性差异。4. readOnly、timeout、rollbackFor三个属性最容易忽略的配置陷阱面试聊到事务属性很多人只盯着隔离级别和传播行为但 readOnly 和 timeout 才是实际项目中更容易出幺蛾子的配置。这三个属性每一个都有对应的坑值得单独拿一讲。4.1 readOnlytrue 不是不让你写是帮你优化读readOnly 从字面理解是只读事务但它并不是一个强约束而是给数据库和框架一个优化提示。设置 readOnly true 后Spring 会做几件事一是对 FlushMode 进行优化。在 Hibernate 环境下会把 FlushMode 设为 MANUAL意味着事务过程中不主动执行 SQL flush直到事务结束才一次性刷新减少不必要的数据库交互。二是某些数据库驱动在连接层面会把会话设为只读模式比如 MySQL 连接参数里的tx_read_only。三是从语义上提醒调用方这个方法不该有写操作。但要注意如果你在 readOnly true 的方法里执行了 insert/update/deleteMySQL 默认引擎下并不会直接报错甚至可能正常写入。这点跟某些人的理解有偏差。MySQL 的只读事务限制的是写操作吗准确说MySQL 在只读事务里执行写操作会报错但 Spring 的 readOnly 默认情况下只是给数据库发送一个 SET SESSION TRANSACTION READ ONLY 之类的会话级设置实际效果取决于驱动和数据库对只读事务的处理策略。有些场景下确实能写入成功变成隐患。那 readOnly 应该怎么用我的建议是明确只做查询的方法加上 readOnly true既是优化也是自文档化。但如果方法内部有任何写操作包括写日志表就不要加。最稳妥的检验方式是测试环境里验证一下读操作是否真的省去了 flush别凭感觉。// 推荐写法查询方法加只读标记 Transactional(readOnly true) public PageResultUserDO pageUsers(UserQuery query) { return userRepository.page(query); }4.2 timeout 生效的边界条件很多人理解错了TransactionDefinition 里的 TIMEOUT_DEFAULT 是 -1表示不限制。一旦设置了具体超时时间秒事务执行超过该时间就会被强制回滚。但这里有个关键细节timeout 的计时起点是从事务创建的那一刻开始的而不是从你看到的那行业务代码开始。如果外层方法已经跑了 3 秒才调用某个设置 timeout5 的内部方法实际留给该方法的时间只剩 2 秒。这一点在嵌套事务场景下特别容易造成莫名其妙超时。另外事务超时不一定能打断正在执行的 SQL。数据库层面如果一条 SQL 本身跑得很慢Spring 的 timeout 机制可能要到这条 SQL 返回后才能感知到超时并触发回滚。简单说Spring 管的是事务持续时间不是SQL 执行时间。要做 SQL 层面的慢查询熔断得依赖数据库自己的锁等待超时设置。实际项目里给所有事务加统一 timeout 是欠考虑的。我的经验是只给那些确实有性能风险的批量方法配置合理的超时比如批量导入、大批量更新设个 30 秒或 60 秒一旦卡住尽早回滚释放数据库连接。4.3 rollbackFor 是默认陷阱受检异常不会回滚这是面试里命中率最高的追问点。默认情况下Spring 声明式事务只对 RuntimeException 和 Error 进行回滚受检异常Checked Exception不会触发回滚。理由似乎是合理的Spring 认为受检异常是业务预期内的分支方法正常返回即可不需要回滚。但在实际项目中很多受检异常恰恰是需要回滚的比如业务校验失败、外部接口调用失败后抛出的自定义异常。如果你的自定义异常继承的是 Exception 而不是 RuntimeException那么事务默认不会回滚数据就停留在半写状态。解决办法是显式声明Transactional(rollbackFor BusinessException.class) public void payOrder(Order order) throws BusinessException { // 扣款 // 改订单状态 } Transactional(rollbackFor Exception.class) public void syncRemoteData() throws Exception { // 同步外部数据 }这里要注意rollbackFor 可以配多个异常类也可以配合 noRollbackFor 使用。比如某些异常你要回滚某些又要忽略Transactional( rollbackFor BusinessException.class, noRollbackFor {NotificationException.class, LogException.class} ) public void processOrder(...) { // 业务处理 }上面的配置含义是遇到 BusinessException 回滚遇到 NotificationException 或 LogException 不回滚。这种精细控制适合主流程失败必须回滚但附属服务失败不影响主流程的场景。我见过一个线上事故一个对账方法抛了受检异常事务没有回滚导致一批对账数据半更新。排查了半小时才发现是 rollbackFor 没配。自那以后我给自己定了条规矩所有自定义业务异常统一继承 RuntimeException然后在类上或者方法上配上 rollbackFor BusinessException.class双保险。5. 事务失效的六种高频场景每一次都是面试加分题面试官问完隔离级别和传播行为几乎必问一句你遇到过事务失效的情况吗这个问题考察的是实战意识和排查能力。我把工作中见过的和源码层面确认过的失效场景整理成六类每类都给出根因和验证方式。5.1 自调用绕过代理最常见也最容易忽视声明式事务基于 Spring AOP 代理外部调用时经过代理对象的拦截器事务注解才能生效。但如果同一个类内部方法直接调用调用的是 this 对象的方法根本没有经过代理事务注解形同虚设。Service public class UserService { Transactional public void createUser(User user) { // 这里调用 this.insertUserLog事务注解不生效 insertUserLog(user.getId()); userMapper.insert(user); } Transactional public void insertUserLog(Long userId) { logMapper.insert(new UserLog(userId, CREATE)); } }上面代码中 insertUserLog 的事务注解不生效因为它被 createUser 内部的 this 直接调用。解决办法有三种第一把内层方法拆到另一个 Service 里通过注入的代理对象调用第二在类上注入自身代理不推荐在循环依赖高发场景使用第三用 AopContext.currentProxy() 获取代理对象需要在启动类上配置 exposeProxy 属性。我的建议是第一种拆 Service 拆分职责的同时自然解决代理问题后面代码也好维护。5.2 try-catch 吞掉异常事务根本感知不到Transactional 的回滚依赖方法向外抛出 RuntimeException。如果方法内部把异常 catch 住并吞掉了方法正常返回事务生命周期管理器认为一切正常直接提交。Transactional public void batchUpdate(ListLong ids) { try { for (Long id : ids) { userMapper.updateName(id, test); } } catch (Exception e) { log.error(更新失败, e); // 没有重新抛出事务提交了前面已更新的行全部落库 } }正确的处理方式是 catch 后至少做两件事记录日志和重新抛出。如果确实不想把异常抛给外层调用方可以在 catch 代码块里用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 手动标记回滚这个方法很多人在源码里见过但没用过。5.3 方法不是 public 的代理根本拦不住Spring 官方文档明确写了 Transactional 只能应用在 public 方法上。private、protected、package-private 方法上的注解都不会生效因为 Spring AOP 的默认代理策略JDK 动态代理只能拦截 public 方法。即使配置了 CGLIBSpring 对事务切点的默认配置也不会拦截非 public 方法。这个问题的隐蔽性在于代码编译不报错运行不报错就是事务行为完全不对。我排查过好几个类似案例最终都是把方法改 public 或者重新设计调用链解决。5.4 异常类型不对受检异常默认不触发回滚这个在上面 rollbackFor 部分已经详细说过这里只强调一个排查技巧如果发现事务应该回滚没回滚先看异常类型。凡是受检异常Spring 默认不回滚。所以面试时被问到Spring 为什么默认不回滚受检异常你可以从设计意图角度回答Spring 把受检异常视为业务可预期分支把运行时异常视为程序缺陷前者希望业务代码自行补偿后者才需要数据回滚。这个回答比直接背结论好得多。5.5 多线程调用带来的事务割裂ThreadLocal 只认当前线程Spring 事务把数据库连接绑定在 ThreadLocal 上。子线程里执行的代码拿不到主线程的事务连接也就不会共享事务。常见的错误写法是Transactional public void processInParallel(ListLong ids) { ids.parallelStream().forEach(id - { // 子线程执行事务不生效 userService.updateStatus(id); }); }parallelStream 或者手动 new Thread 执行的方法事务注解一律不生效。即便生效子线程也应该有自己的事务而不该共享主线程的事务。正确的设计是主线程用事务完成主流程子线程各自处理独立任务时如果需要事务就在子线程内部方法上配置传播行为或者干脆把任务交给消息队列异步处理不要让事务跨线程。5.6 数据库表引擎不支持事务最隐蔽的失效MyISAM 引擎根本不支持事务DDL 和 DML 都是一次性落盘。如果项目里混用了 MyISAM 表即使代码层面事务配置全对对 MyISAM 表的操作也不会回滚。这个问题在老旧项目迁移时特别常见排查方法是查看建表语句或者执行SHOW TABLE STATUS WHERE Name xxx查看 Engine 字段。Spring 层面无法感知这种失效只能靠 DBA 和开发团队的表结构规范来兜底。这六类场景对应六种排查思路自调用查调用链、异常被吞查 catch 块、非 public 查方法修饰符、异常类型查继承关系、多线程查线程上下文、表引擎查 DDL。面试时你能按这套逻辑讲一个真实排查案例面试官基本不会再追问。6. 一套可以直接套用的面试应答思路前面把 API、隔离级别、传播行为、属性陷阱都捋了一遍最后分享一套我自己整理的回答框架针对谈谈你对 Spring 事务隔离/传播行为的理解这类开放题。6.1 答题框架从定义到场景到边界第一步先给定义用一两句话说明白隔离级别是什么、传播行为是什么不要展开。比如隔离级别描述的是一个事务与其他事务在数据可见性上的隔离程度Spring 提供了五种取值传播行为描述的是多个事务方法调用时事务如何扩展Spring 定义了七种策略。第二步讲关键对比。隔离级别重点对比 READ_COMMITTED 和 REPEATABLE_READ 的差异顺带提一句 MySQL InnoDB 在 RR 下通过 MVCC 和间隙锁规避了幻读传播行为重点对比 REQUIRED、REQUIRES_NEW、NESTED 三者的差异用日志记录和批量处理两个场景说明为什么选型不同。第三步讲实际项目经验。比如我们用户在订单支付场景使用了 REQUIRED 默认传播保证扣款和订单状态在同一个事务里异步通知场景使用 REQUIRES_NEW 写通知日志防止主事务回滚吞掉排查线索批量任务对单条失败使用 NESTED 实现部分成功。三步走下来既有理论又有实践还是结构化的表达。6.2 面试官连问三层的应对策略我模拟一个常见的追问链条面试官REQUIRED 传播行为下内层方法抛异常被外层 catch 住事务会回滚吗这是经典的送命题。答案分两种情况如果内层异常是 RuntimeException 且没有被外层 catch事务回滚如果被外层 catch 住了Spring 在事务同步机制中已经检测到内层事务被标记为 rollback-only即使外层 catch 住异常并正常返回最终提交时仍会抛出 UnexpectedRollbackException事务回滚。核心原因是内层方法加入同一事务调用了 setRollbackOnly这个标记会随事务传播到整个事务链。再追问那如果把传播行为改成 REQUIRES_NEW还会这样吗不会。内层新开独立事务它自己的回滚标记不影响外层事务。内层回滚后外层可以选择继续执行或者回滚两者互不干扰。这正是日志场景选择 REQUIRES_NEW 的原因。再追问NESTED 呢NESTED 内层回滚只回滚到保存点外层事务不会被标记为 rollback-only外层可以继续执行。所以 NESTED 适合批量处理中允许单条失败后继续的场景但依赖数据库的 savepoint 支持。这样一层层答下来面试官看到的不只是你背了概念而是你理解事务的实现机制边界条件这恰恰是日常编码中最需要的能力。6.3 最后分享一个面试小技巧如果面试官问到一个你没准备过的事务细节不要硬编。坦诚说这个细节我没深入验证过然后立刻把话题引导到你熟悉的验证过的场景上。比如可以说隔离级别的默认值细节我没有逐个数据库验证过但我之前在 MySQL 上实测过脏读现象整个过程是这样的……用真实的实验过程证明你的工程能力比背住所有边角料更有说服力。这也是我坚持在文章里把验证手段写出来的原因——面试官要的不是知道是做过。Spring 事务这些知识点光看一遍和自己动手跑一遍内核的掌握深度完全不同。强烈建议你花一个下午的时间把本文里的事务失效场景逐个在本地模拟出来。跑完这六种场景你再去面试事务相关的问题心态会完全不一样。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询