
Spring事务这个东西平时写业务看不起眼一到面试就成了翻车重灾区。面试官随口一句“事务的传播行为有哪几种”能把不少人问得只剩条件反射式的默背再追问一句“REQUIRED和REQUIRES_NEW里层异常后外层为什么不生效”很多人就开始眼神飘忽了。上一篇我把Spring AOP和代理机制从头到尾捋了一遍这篇顺势把Spring事务的核心API和属性拆开揉碎讲清楚隔离级别、传播行为、注解失效场景、面试高频追问一步到位。这不是一份面向零基础的概念科普更像一份“从原理到面试再到实际避坑”的完整笔记。在Spring家族里事务能力一直是连接业务代码和数据库的一个关键开关你要是没搞懂PlatformTransactionManager、TransactionDefinition、TransactionStatus到底是谁在干活那配置再多注解也只是一知半解。本文核心围绕Spring事务核心API、事务隔离级别、事务传播行为几个重点展开先讲清楚抽象设计再逐个拆属性最后落地到代码和排查经验。适合正在准备Spring面试的Java开发也适合写业务时被各种事务“玄学”坑过的人。1. 事务抽象的三个主角先搞清楚谁在管事务很多人用过Transactional却说不清Spring事务的底层API是谁。真相是Spring事务的整套体系建立在一个很经典的设计上统一抽象屏蔽差异。不管你是连MySQL、Oracle还是用MyBatis、JDBC、JPA对Spring事务来说都只需要面对三个核心接口。1.1 PlatformTransactionManager事务的总指挥PlatformTransactionManager是整个事务体系的发动机它定义了三个最核心的方法getTransaction(TransactionDefinition definition)按照给定的定义获取或创建事务返回一个TransactionStatus。commit(TransactionStatus status)提交事务。rollback(TransactionStatus status)回滚事务。这三个方法就是Spring事务的“三板斧”。实际工作中你接触到的DataSourceTransactionManager、JpaTransactionManager、HibernateTransactionManager都是PlatformTransactionManager在不同技术栈下的具体实现。DataSourceTransactionManager的底层逻辑就是拿到数据库连接关闭自动提交执行完再根据状态决定commit还是rollback。有个细节值得注意getTransaction返回的TransactionStatus不只是“事务状态”它还承担着“事务是否新创建”“是否已完成”“是否只读”等职责。Spring的声明式事务实际上就是在进入方法前通过拦截器调用getTransaction在方法结束后调用commit或rollback整个过程对业务代码完全透明。1.2 TransactionDefinition事务的说明书TransactionDefinition就是“怎么开事务”的说明书。它定义了事务的传播行为、隔离级别、超时时间、只读标志、回滚规则以及事务名称。打开源码你会发现它里面定义了一堆常量传播行为常量PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED等对应0、3、6等整数。隔离级别常量ISOLATION_DEFAULT、ISOLATION_READ_UNCOMMITTED等对应-1、1、2、4、8。这些常量看起来冷冰冰但理解了它们你就能理解Transactional注解里的那些属性到底映射到哪里。注解里的propagation、isolation、timeout、readOnly最终都会被转换成TransactionDefinition的实现对象交给PlatformTransactionManager使用。1.3 TransactionStatus事务的运行状态表TransactionStatus用来保存事务执行过程中的状态核心方法有isNewTransaction()判断当前事务是不是新创建的而不是加入到外层已有事务。isCompleted()判断事务是否已经结束。setRollbackOnly()标记事务只能回滚哪怕外层方法想提交也不行。flush()强制刷新底层会话让数据库执行未提交的SQL。setRollbackOnly这个方法是理解Spring事务嵌套的一个关键。后面讲传播行为时会遇到“明明catch了异常外层提交还是报UnexpectedRollbackException”的经典场景就是它惹的祸。所以你可以把这三者的关系理解为TransactionDefinition告诉你事务长什么样PlatformTransactionManager负责执行TransactionStatus记录执行到哪一步了。1.4 连接与线程绑定这是事务设计的地基在讲属性之前必须先理解一个背景Spring事务与数据库连接绑定的方式是ThreadLocal。DataSourceTransactionManager在getTransaction时会把数据库连接存到当前线程的ThreadLocal里后续同一个线程里的SQL操作都能复用这个连接从而共享同一个事务。这个设计有一个重要推论事务是线程私有的。如果你在事务里新开一个线程去执行另一个数据库操作那个线程拿不到当前线程的事务连接SQL不会参与这个事务甚至可能因为连接池的并发问题造成隐式提交。这不是Spring的孤例而是绝大多数框架处理事务的共性思路数据库事务本质上是和连接绑定的连接又和线程绑定了。2. TransactionDefinition属性全解六个属性决定事务行为现在重点看TransactionDefinition里那些定义事务行为的属性。有人总觉得属性只有隔离级别和传播行为实际上完整的属性有六个传播行为、隔离级别、超时时间、只读标志、回滚规则、事务名称。2.1 传播行为Propagation事务之间如何“相处”传播行为解决的核心问题是当某个事务方法被另一个事务方法调用时事务该如何参与。Spring定义了7种传播行为其中重点掌握前4种就够应付绝大多数开发场景。7种传播行为对照传播行为含义外部无事务时外部有事务时REQUIRED支持当前事务没有则新建新建事务加入当前事务SUPPORTS有事务则加入无则非事务执行非事务执行加入当前事务MANDATORY强制要求已有事务抛异常加入当前事务REQUIRES_NEW无论有无都要新开事务新建事务挂起当前事务新建独立事务NOT_SUPPORTED以非事务方式执行非事务执行挂起当前事务非事务执行NEVER强制无事务非事务执行抛异常NESTED嵌套事务依赖保存点新建事务嵌套在当前事务中内部回滚只回滚到保存点举一个生活化类比你参加公司聚餐REQUIRED就是“有局就进没局自己组”REQUIRES_NEW是“不管外面有没有局自己非要单独开一桌”NESTED是“在同一桌吃饭但自己点了份独立的菜吃坏肚子只退自己这份”。这个类比虽然粗糙但思路是对的。2.2 隔离级别Isolation并发读写的安全等级隔离级别解决的是并发事务下数据一致性的问题。四个标准隔离级别外加一个“跟随数据库默认”的DEFAULT。这个属性太多面试官爱追问后面用专门一节来讲这里先记住一个结论Spring的隔离级别是对数据库行为的映射真正生效还取决于数据库自身是否支持。2.3 超时时间Timeout事务最长存活时间当事务执行时间超过设定值Spring会强制回滚并抛出TransactionTimedOutException。这个属性在TransactionDefinition中以秒为单位实际工作中用得不算多但设计长事务系统时需要关注。默认值为TIMEOUT_DEFAULT即-1表示不超时。2.4 只读标志readOnly优化与约束的双重作用readOnly标记当前事务是只读事务。底层实现上Spring会对该标志做两件事一是提示底层资源和连接池可以尝试优化二是防止在只读事务中执行写操作。但一定要注意一个误区和坑readOnly不等于强制只读它更多是给底层的提示和约束。你可以把readOnly理解为“给数据库的礼貌建议”但不是绝对的法律禁止。实际操作中设置readOnly true后如果代码里执行了insert/update/delete大部分数据库会报错或忽略该设置但MySQL的InnoDB在不同配置下表现可能略有差异。2.5 回滚规则Rollback Rules什么异常才会回滚这是面试最爱考、实际最容易踩坑的属性。Spring的默认回滚规则是只对RuntimeException和Error回滚受检异常CheckedException默认不会回滚。为什么这样设计Spring的思路是受检异常通常代表“业务逻辑上可处理的失败”比如你调一个外部接口对方返回了一个可预期的业务错误码这不该让整个事务回滚而RuntimeException代表程序本身的bug或不可预知异常属于事务需要兜底的场景。但这个默认行为在实际业务中经常让人吐槽很多人在事务方法里写了“抛出一个受检异常”结果数据没回滚排查半天才发现原因。所以你在实际开发中通常会在Transactional上显式指定rollbackFor Exception.class把一切异常都纳入回滚范围。2.6 事务名称调试与排查的线索Transactional注解里有name属性TransactionDefinition接口也有getName方法。事务名称主要用在事务管理器的日志和监控中方便排查问题时定位事务边界。很多程序员忽略这个属性但线上排查时如果每个事务都有明确的名字效率会高很多。3. 隔离级别深入拆解五个级别与三个经典问题隔离级别这块面试喜欢从“脏读、不可重复读、幻读三种异常现象”开始问再让你说出四种隔离级别分别解决哪些问题。很多背过八股的人都能说个大概但要能讲透原理并能结合实际场景就得看下面这些。3.1 三种读异常到底是什么先定义清楚三个概念。脏读一个事务读到了另一个事务尚未提交的数据。比如事务A把张三余额改成100还没提交事务B读到了100然后A回滚了B就拿着一个不存在的100在干活。这是绝对不可接受的。不可重复读一个事务内同一个查询语句两次查询结果不一致。原因是另一个事务在两次查询之间提交了修改。比如事务A查价格是100事务B把价格改成200并提交事务A再查就变成200了。问题在于“同一事务内结果不稳定”。幻读一个事务内同一个范围查询两次查询返回的行数不一样。另一个事务在两次查询之间插入或删除了记录。比如事务A查价格大于100的商品第一次查到3条事务B插入一条价格150的商品并提交事务A再查就变成4条。幻读和不可重复读的区别在于不可重复读侧重“同一行的值变了”幻读侧重“行数变了”。3.2 四个隔离级别对照表隔离级别脏读不可重复读幻读READ_UNCOMMITTED读未提交可能可能可能READ_COMMITTED读已提交不会可能可能REPEATABLE_READ可重复读不会不会可能SERIALIZABLE串行化不会不会不会级别逐级升高并发性能逐级下降。READ_UNCOMMITTED基本没人用因为脏读的危害对业务系统太大。SERIALIZABLE并发性能太差实际项目中也极少启用。生产环境最常见的是READ_COMMITTED和REPEATABLE_READ。这里有一个很关键的点MySQL的InnoDB引擎默认隔离级别是REPEATABLE_READ而Oracle默认是READ_COMMITTED。所以你在Spring里如果不设置隔离级别使用默认的ISOLATION_DEFAULT时实际生效级别取决于数据库。很多面试官就喜欢用这个细节来分辨你是真的懂还是只会背Spring配置。3.3 REPEATABLE_READ如何解决幻读MySQL是在REPEATABLE_READ级别下通过MVCC多版本并发控制和Next-Key Lock来尽可能消除幻读现象的。MVCC的快照读保证了普通SELECT语句在事务内的多次查询结果一致解决了一部分幻读问题。但对于当前读比如SELECT ... FOR UPDATE、UPDATE、DELETEMySQL通过Next-Key Lock记录锁间隙锁的组合来锁定扫描范围内的间隙阻止其他事务在范围内插入新记录从而避免幻读。所以“MySQL的REPEATABLE_READ基本解决了幻读”这个说法在大众层面是成立的但严格来说它解决的是特定场景下的幻读并非绝对意义上的串行化。面试时如果能把这个区别讲出来面试官会眼前一亮。3.4 隔离级别不是越高越好一个常见误区是“既然SERIALIZABLE最安全我就全部用它”。实际生产环境基本不会这样做因为隔离级别本质上是在一致性和并发性能之间做权衡。SERIALIZABLE会把涉及到的数据全部锁住读也要加锁并发一高就是灾难。正确的做法是结合业务场景选择统计类报表、账单查询这类读多写少的场景可以考虑READ_COMMITTED需要保证多次读结果一致的核心业务比如订单状态查询使用默认的REPEATABLE_READ就足够了实在有极端一致要求的场景再考虑SERIALIZABLE或者通过锁机制自行控制。4. 传播行为逐个拆解从REQUIRED到REQUIRES_NEW传播行为是Spring事务面试题的重灾区因为它涉及“事务嵌套”这种复杂但真实存在的业务场景。我一个个拆开讲重点说清楚“内部事务异常后外部事务到底会不会跟着回滚”。4.1 REQUIRED默认行为也是最容易“背锅”的行为REQUIRED是Spring的默认传播行为。它的规则是如果当前没有事务就新建一个事务如果当前已经有事务就加入当前事务。很多人在同一类内部写A方法调B方法两个方法都标了Transactional然后内部B事务抛了异常外部A catch住了结果发现整个事务还是回滚了。这就是REQUIRED的经典陷阱。原因在于B方法加入A方法的事务后B的异常被Spring事务拦截器感知到它会调用TransactionStatus.setRollbackOnly()把事务标记为只能回滚。A方法即使catch住异常外层在提交时发现事务已经被标记为rollbackOnly就会抛出UnexpectedRollbackException数据照样不回滚。所以我经常跟同事说不要在事务方法内部用try-catch把异常吞掉你吞掉的不是异常是回滚的资格。如果你明确知道“这个异常不影响主流程我不需要回滚”那应该考虑把这段代码移到事务外面或者使用REQUIRES_NEW。4.2 REQUIRES_NEW开新事务和外面彻底分离REQUIRES_NEW的规则是无论当前有没有事务都开启一个新事务如果当前已有事务先把当前事务挂起。挂起的意思是外层事务的连接暂时被保存事务中断等新事务结束后再恢复。新事务拥有自己的连接、自己的commit和rollback与外层事务完全独立。内部事务失败回滚不影响外层事务继续提交反之外层事务回滚也不会让内部那个已经提交的新事务跟着回滚。这种场景最典型的是审计日志、积分记录、消息推送等操作主业务失败回滚但日志和消息已经发出去了不能跟着回滚。当然代价是性能开销因为挂起和恢复本身就是成本。这里需要注意REQUIRES_NEW内部事务用的数据库连接是重新从连接池取的所以它依赖连接池至少有两个连接可用。如果连接池只有1个连接REQUIRES_NEW会卡在等待连接上。4.3 NESTED嵌套事务依赖保存点NESTED和REQUIRES_NEW最大的区别是它不开启独立事务而是在当前事务里创建一个保存点Savepoint。内部逻辑执行失败时事务回滚到保存点而不是回滚整个事务。再补充一个细节NESTED对外层事务的影响是有限的。如果外层事务在内部事务执行后提交整个链路没问题如果内部嵌套事务需要回滚它只回滚到保存点但如果外层事务最终失败回滚那么包括嵌套事务在内的所有操作都会整体回滚因为嵌套事务本质上还是外层事务的一部分。NESTED和REQUIRES_NEW的区别是个高频面试题。一句话总结REQUIRES_NEW是彻底独立的新事务NESTED是外层事务的嵌套子事务前者内部提交后不受外部控制后者回滚范围受保存点限制最终命运还跟着外层走。4.4 SUPPORTS、NOT_SUPPORTED、MANDATORY、NEVER怎么理解SUPPORTS有事务就加入没事务就以非事务方式跑。适合那些“有没有事务都无所谓”的查询方法。NOT_SUPPORTED当前有事务时挂起它以非事务方式执行。适合那些不希望占用事务连接、又不需要一致性的操作。MANDATORY当前没有事务就直接抛异常。适合那些“必须在一个事务里被调用否则说明调用链有bug”的方法。NEVER当前有事务就抛异常。强制要求非事务环境适合一些不允许在事务里执行的操作。这四种在日常业务里用得相对少但面试时能准确说出它们的适用场景会让面试官觉得你不只是背定义。5. 编程式事务TransactionTemplate实操记录很多人以为Spring事务只能靠Transactional注解玩这其实是被声明式事务惯坏了。声明式事务虽然方便但它在事务边界不可控、需要对同一方法内不同代码段精细控制时会显得很笨重。这时候就该编程式事务上场TransactionTemplate就是推荐方案。5.1 核心API与配置TransactionTemplate是Spring对编程式事务的封装。使用时先注入或手动new一个TransactionTemplate然后注入PlatformTransactionManagerConfiguration public class TransactionConfig { Bean public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) { TransactionTemplate template new TransactionTemplate(); template.setTransactionManager(transactionManager); // 可选设置传播行为、隔离级别、超时时间、只读标志 template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); template.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); template.setTimeout(10); return template; } }有了模板后业务代码里调用execute或executeWithoutResultService public class OrderService { Resource private TransactionTemplate transactionTemplate; public void createOrder(Order order) { transactionTemplate.executeWithoutResult(status - { // 事务内的业务逻辑 orderDao.insert(order); itemDao.updateStock(order.getItemId(), -1); // 如果判断需要回滚 // status.setRollbackOnly(); }); } }execute接受TransactionCallback返回结果适合需要从事务方法返回业务值的场景executeWithoutResult适合没有返回值的场景。相比原生写手动getTransaction、commit、rollbackTransactionTemplate帮你把状态管理、异常处理都做好了代码要干净得多。5.2 TransactionTemplate的独特优势TransactionTemplate最大的优势是“事务边界精确到代码块”。Transactional只能从进入某个方法开始到方法结束而TransactionTemplate可以在一个方法里让前半段不开启事务中间某一段独立事务后面再根据业务判断开启新事务。这在编写批处理任务、消息消费逻辑时特别有用。另外TransactionTemplate的execute方法内部遇到RuntimeException会自动回滚这点和Transactional默认规则一致你也可以通过rewriteException或者自定义TransactionCallback来调整异常处理。5.3 什么场景推荐编程式事务我自己的经验是对于常规增删改查的Service层声明式事务Transactional足够且优雅。但碰到以下场景我会首先考虑TransactionTemplate一个方法里需要“部分操作无论如何都执行部分操作出错要回滚”需要动态决定是否使用事务比如根据开关参数选择不同传播行为在循环里对一批任务分别开启独立事务做到单个任务失败不影响其他任务需要把事务边界做得比方法更小的场景。很多老项目里还能见到手写“TransactionTemplate 回调”的代码这其实说明作者对事务的理解到了“编程式”这个层。6. Transactional源码级扫盲声明式事务的真相Transactional是声明式事务的门面底层是AOP。Spring通过TransactionInterceptor这个AOP拦截器在事务方法执行前开启事务执行后根据异常情况决定提交或回滚。本质上和你手动调用PlatformTransactionManager没有区别只是帮你把流程自动化了。6.1 注解属性与TransactionDefinition的映射Transactional的核心属性都能在TransactionDefinition里找到对应项注解属性对应TransactionDefinition概念默认值propagation传播行为REQUIREDisolation隔离级别DEFAULTtimeout事务超时时间-1不超时readOnly只读标志falserollbackFor / noRollbackFor回滚规则Runtime异常回滚受检异常不回滚value / transactionManager指定事务管理器默认按类型装配值得注意的是Spring会查找注解信息构造一个RuleBasedTransactionProperties其中的回滚规则由rollbackForClass和noRollbackForClass两个集合组成。实际判断回滚时会在这两个集合中做匹配匹配不到再走默认规则。6.2 Spring事务通知的底层链路Transactional方法被调用时链路大致是调用进入代理对象。TransactionInterceptor拦截方法解析Transactional注解得到事务属性。调用transactionManager.getTransaction(definition)开启事务。通过事务的invocation.proceed()执行真正的业务代码。业务代码正常结束提交事务抛出异常根据回滚规则决定回滚或提交。这里面最容易被忽略的是步骤2Transactional是从目标方法上解析的不是从接口上解析的。如果你把注解放在接口方法上而目标类实现方法上没有某些配置下可能出现注解不被识别的问题。实际开发中我建议把Transactional直接放在实现类的方法上而不是接口上。6.3 事务失效的十个常见场景接下来是避坑核心。下面这十种场景我每一类都在项目里见过真实案例。场景原因解决方案类未被Spring管理用了new创建对象交给IoC容器管理方法非public底层代理无法拦截改成public自调用this调用不走代理注入自身代理或拆到其他Beanfinal方法CGLIB无法生成子类覆盖避免final异常被try-catch吞掉事务拦截器看不到异常抛出或手动setRollbackOnly抛出的受检异常未配rollbackFor默认不检查受检异常配置rollbackFor Exception.class数据库引擎不支持事务MyISAM等改用InnoDB多线程调用事务连接绑定主线程ThreadLocal避免事务内开子线程传播行为设置不当NOT_SUPPORTED/NEVER会关闭事务检查propagation设置注解写在了private方法或被重写的方法上代理拦截不到放在public方法上6.4 自调用失效的深入解释自调用是面试官最爱问的失效场景。所谓自调用是指同一个类内部另一个方法调用这个方法。比如Service public class UserService { public void outer() { // 相当于 this.inner() inner(); } Transactional public void inner() { // 数据库操作 } }当外部调用outer时进入的是UserService的代理对象outer方法里的inner()调用实际指向的是this也就是目标对象本身而不是代理对象。因此Transactional的拦截器根本没有机会触发事务自然不生效。解决方式一般是注入自身的代理引用或者把inner方法拆到另一个Service中。拆出去更符合单一职责也更利于复用。另外要注意JDK动态代理和CGLIB的差别。Spring默认如果目标类实现了接口使用JDK动态代理如果没有接口使用CGLIB代理。JDK动态代理只能拦截接口方法CGLIB可以拦截类方法。这也是为什么在Spring Boot中的事务方法即使类不实现接口也能正常工作因为默认CGLIB。7. 高频面试题速查与实战经验最后一部分把面试里常问的问题做成速查感觉顺带分享一些我在真实项目里总结的经验。7.1 面试题速查Q1Spring事务默认的传播行为和隔离级别是什么 A默认传播行为是REQUIRED默认隔离级别是DEFAULT即跟随底层数据库的默认隔离级别。Q2Transactional在什么条件下才会生效 A这类方法要被Spring的AOP代理管理方法要public异常要被事务拦截器感知到且符合回滚条件。Q3REQUIRED和REQUIRES_NEW的区别 AREQUIRED是加入已有事务没有则新建REQUIRES_NEW是无论有无都新开一个独立事务并把已有事务挂起。内部异常时REQUIRED会把事务标记为rollback-only最终可能导致外层UnexpectedRollbackExceptionREQUIRES_NEW内部回滚不影响外部。Q4为什么事务方法内部开新线程新线程里的数据库操作不参与事务 A事务连接保存在主线程的ThreadLocal中子线程拿不到主线程的事务连接自然无法共享事务。Q5MySQL的REPEATABLE_READ会不会有幻读 A在InnoDB引擎下通过MVCC和Next-Key Lock多数普通读场景下幻读被避免但并非绝对比如某些当前读仍可能触发幻读。Q6事务注解放在接口上还是实现类上 A建议放在实现类方法上因为Spring从目标方法解析注解放接口上存在不被识别的可能。7.2 事务设计上的几条实战建议事务尽量放在Service层不要放在Controller层。Controller层属于表现层它的职责是接收请求和返回响应把事务放在那里会让事务边界乱套也不利于单元测试。事务方法内不要做远程调用和耗时IO。事务期间数据库连接一直被占用如果你在事务里调外部HTTP接口连接会一直挂在那里直到外部接口响应。外部接口慢10秒连接就占10秒连接池很容易被打满。要控制事务粒度。一个事务方法里尽量只做“必须一起成功或一起失败”的操作。如果两个操作之间没有强一致要求把事务拆细能大幅提升并发能力和排查效率。多用自定义异常而非受检异常。业务代码里抛出业务异常时建议继承RuntimeException配合rollbackFor Exception.class可以避免不少回滚不生效的坑。7.3 一个值得反复实验的现场测试我强烈建议你在本地写一个Demo实验一下“REQUIRED里层抛异常外层catch”这个经典场景。实验步骤很简单创建ServiceA方法a调用ServiceB的b方法两个方法都标Transactional(propagation REQUIRED)。b方法里先插入一条记录然后抛RuntimeException。a方法里用try-catch包住b的调用catch住后继续执行最后正常结束。观察最终数据是否被插入留意控制台是否出现UnexpectedRollbackException。第一次跑通这个实验你对Spring事务传播行为的理解就超过七成靠背八股的人。7.4 我个人的体会这几年看过的项目里事务相关的线上问题十个有八个出在“异常被吞”“自调用导致注解失效”“事务里做远程调用”这三类上。面试时背会这些概念只是第一步真正值钱的是你大脑里有没有可视化的事务调用链进入代理、开启事务、执行业务、异常判断、提交回滚、连接放回池子。把这些环节在脑子里跑顺了任何事务问题在你面前都是透明的。这篇文章把Spring事务的核心API、事务属性、隔离级别、传播行为、编程式事务以及最常见的失效场景都过了一遍。下一步如果你要往深了走分布式事务是绕不开的话题像订单与库存这种跨库的强一致场景、分布式事务协调器的设计就是第三篇可以展开的内容。先把单机事务这块地基打牢再往分布式去路会顺畅很多。