Spring 事务这层皮很多人用了一两年Transactional往方法上一丢就觉得万事大吉。可真到线上出问题的时候——事务没回滚、同方法自调用失效、多数据源跑错事务管理器——才意识到自己根本不了解它背后的机制。这篇文章要把 Spring Boot 事务的动态增强链路彻底拆开代理是怎么生成的TransactionInterceptor和PlatformTransactionManager这些人头部门到底怎么协作回滚规则按什么逻辑判定。适合两种人看一种是面试前想把 Spring 事务底层讲清楚的同学另一种是线上正被事务不生效折磨的排障选手。1. 从注解声明到动态代理事务增强的整体设计思路1.1 为什么 Spring 事务要走动态增强这条路最原始的事务管理长什么样你肯定见过Connection conn null; try { conn dataSource.getConnection(); conn.setAutoCommit(false); // 业务代码 A // 业务代码 B conn.commit(); } catch (Exception e) { if (conn ! null) { conn.rollback(); } throw e; } finally { if (conn ! null) { conn.close(); } }这段代码把事务开启、提交、回滚、资源清理这些横切逻辑和业务逻辑揉在一起。十个方法要写十遍二十个数据源要写二十种配置维护成本直接起飞。Spring 要解决的核心问题就是把事务这个横切关注点从业务代码里剥离出来。怎么剥离最理想的情况是业务代码只写我要开事务至于连接怎么拿、事务怎么提交、异常怎么回滚全都交给框架在方法执行前后自动织入。这就派生出两条技术路线一条是编译期静态织入比如 AspectJ 的 LTWLoad-Time Weaving或者 CTWCompile-Time Weaving另一条就是 Spring 的选择——运行期动态代理。Spring 选了动态代理核心原因有三个对业务代码零侵入不需要给类加什么特殊父类也不需要编译器插件适配任意已存在的 Bean容器在 Bean 初始化之后通过后置处理器再包一层代理即可和 IoC 容器天然契合所有 Bean 都从容器拿容器只要能返回一个增强版对象调用方完全感知不到。代价也很直接动态代理依赖通过容器获取对象这个前提一旦你绕开代理直接用原始对象比如同类内部this调用增强逻辑就失效同时多了一层反射或字节码调用性能上有微小损耗在超高 TPS 的方法上能感受到差异。1.2 JDK 动态代理与 CGLIB两种增强机制的取舍动态代理具体怎么落地Spring 提供了两套方案。JDK 动态代理是基于接口的。它要求目标类必须实现至少一个接口代理对象在运行期通过Proxy.newProxyInstance动态生成实现同样的接口然后所有方法调用都会进入InvocationHandler.invoke。如果目标类没有接口JDK 方案直接罢工。CGLIB 则是基于字节码生成。它运行时生成目标类的子类通过重写父类方法来拦截调用不要求实现接口。Spring Boot 2.x 默认策略是spring.aop.proxy-target-classtrue也就是优先走 CGLIB 方案实际是 Spring 内嵌的 ASM 字节码操作能力。这两套方案在实际工程中的差异值得注意。JDK 代理更轻只要目标类有接口它对接口方法的拦截非常干净但如果你在实现类里新增了一个接口之外的方法JDK 代理是拦不住的因为代理对象只暴露接口方法。CGLIB 没有这个限制但它是靠生成子类实现的所以final类、final方法都没法被增强——子类不能覆盖 final 方法。这也是面试里高频的一个点为什么Transactional加在 final 方法上不生效答案就是 CGLIB 代理根本无法覆盖 final 方法。实操中的经验是日常项目别过度纠结选哪种Spring Boot 默认 CGLIB 即可。但如果你和别人协作写了一个公共 Starter里面有大量Bean方法返回接口类型同时内部实现类还有非接口的公共方法这时要特别留意代理暴露的行为边界。遇到方法没走代理的诡异问题时先想办法确认当前对象到底是什么类型的实例这一步能定位八成问题。1.3 核心组件职责速览在动手看源码之前先把几个干活的人头认清楚组件职责TransactionInterceptor事务拦截器AOP 中的MethodInterceptor实现是所有声明式事务的入口TransactionAspectSupport事务基础支撑类管理TransactionInfo、触发回滚/提交的核心逻辑PlatformTransactionManager事务管理器抽象屏蔽底层资源差异提供getTransaction/commit/rollbackTransactionAttribute事务属性定义包含传播行为、隔离级别、超时、只读、回滚规则TransactionSynchronizationManager事务同步管理器用ThreadLocal维护当前线程的事务资源BeanFactoryTransactionAttributeSourceAdvisor事务增强器负责把哪些方法需要事务翻译成 AOP 的切点这个分工一眼就能看出 Spring 的设计思路增强器负责找目标拦截器负责执行增强事务管理器负责跟资源打交道事务同步管理器负责把事务状态绑到当前线程。后面的第 2、3 节就从这三个维度展开聊。2. 核心组件逐个拆开看拦截器、事务管理器与同步机制2.1 TransactionInterceptor 的调用入口TransactionInterceptor实现了 AOP 联盟的MethodInterceptor接口核心方法就一个invoke所有被事务增强的方法执行时都会先经过这里Override Nullable public Object invoke(MethodInvocation invocation) throws Throwable { Class? targetClass AopUtils.getTargetClass(invocation.getThis()); return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation); }看到AopUtils.getTargetClass没它拿的是目标类而不是代理类。为什么因为事务属性解析需要基于真实业务类上的注解信息如果拿的是代理类注解可能根本不在上面。这一点非常容易踩坑后面第 4 节讲排查时会再提。invokeWithinTransaction是真正的核心逻辑我把它简化成几个关键分支protected Object invokeWithinTransaction(Method method, Class? targetClass, InvocationCallback invocation) { TransactionAttributeSource tas getTransactionAttributeSource(); TransactionAttribute txAttr tas.getTransactionAttribute(method, targetClass); PlatformTransactionManager tm determineTransactionManager(txAttr); // 构造 TransactionInfo内部会调用事务管理器的 getTransaction 开启事务 TransactionInfo txInfo createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal; try { retVal invocation.proceedWithInvocation(); // 执行业务方法 } catch (Throwable ex) { completeTransactionAfterThrowing(txInfo, ex); // 异常处理 throw ex; } finally { cleanupTransactionInfo(txInfo); } commitTransactionAfterReturning(txInfo); // 正常提交 return retVal; }这段逻辑看起来简单但它背后藏着几个细节值得展开说。第一determineTransactionManager是找管理器的过程。如果Transactional上没有写管理器名称Spring 会去容器中查找。如果容器里只有一个PlatformTransactionManager实现那把默认的拿来用如果有多个且你忘了指定名称就会抛出NoUniqueBeanDefinitionException。很多多数据源工程遇到事务怎么不生效的第一现场就在这。第二createTransactionIfNecessary这一步不是一定会真的开启事务。它依赖事务传播行为。比如一个方法传播行为是PROPAGATION_MANDATORY但当前线程压根不存在事务这里会直接抛IllegalTransactionStateException如果是PROPAGATION_REQUIRED发现当前线程已有事务就什么都不做直接复用。第三completeTransactionAfterThrowing里也不是一见到异常就回滚。这里要做回滚规则判断默认只有RuntimeException和Error才回滚普通的受检异常是不会触发回滚的。这是全文章最高频的为什么我的事务没回滚答案。2.2 PlatformTransactionManager屏蔽资源差异的事务管家PlatformTransactionManager是 Spring 事务的最顶层抽象接口只有三个方法public interface PlatformTransactionManager extends TransactionManager { TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; }getTransaction负责开启事务或者说获取当前事务commit和rollback负责结束事务。它最大的价值是屏蔽了底层资源到底是什么这个差异。JDBC 是ConnectionJPA 是EntityManagerJMS 是Session它们在事务模型上很相似但 API 完全不同Spring 用一个统一抽象把它们包了起来。日常项目最常见的三个实现实现类适用场景DataSourceTransactionManagerJDBC / MyBatis / 直接操作DataSourceJpaTransactionManagerJPA / HibernateJtaTransactionManager跨多个资源分布式事务JTASpring Boot 的自动配置逻辑很直接只要 classpath 里有DataSource和事务管理器相关类它就会自动注册一个DataSourceTransactionManager并把容器里唯一的DataSource塞进去。所以最简单的项目你甚至不用写任何配置Transactional就能用。但要注意自动配置只处理只有一个 DataSource的乖巧场景。一旦你配置了多个数据源就必须给每个数据源配上自己的事务管理器然后通过Transactional(xxxTransactionManager)显式指定。我见过一个真实项目两个数据源共用了同一个事务管理器结果主库的写操作绑到了从库连接上最后数据分布一团乱。排查下来就是没指定管理器名称导致的。2.3 TransactionSynchronizationManager线程里的事务户口本这个类平时不显山露水但它是理解事务传播、事务隔离、包括多线程事务为什么失效的关键。它的本质是一个基于ThreadLocal的仓库维护了当前线程和事务相关的一切资源当前线程绑定的数据源连接resourcesMap当前线程是否有活动事务事务同步回调列表synchronizations事务名称、只读状态、隔离级别等。DataSourceTransactionManager在开启事务时会调用TransactionSynchronizationManager.bindResource把数据库连接绑定到当前线程。后续业务方法里不管通过 MyBatis 还是 JdbcTemplate 拿连接都会先从这个 ThreadLocal 里找。找到了就用同一个连接这样事务的 ACID 才有保障找不到就新开一个连接——这也是为什么事务方法里开新线程就会脱离事务的根本原因。// 事务管理器中获取资源的逻辑简化版 Object resource TransactionSynchronizationManager.getResource(dataSource); if (resource null) { // 没有绑定连接说明当前线程没有事务新建连接 }我的一个实际经验是排查子线程数据写进去了但主线程回滚了这类问题时先去查这个 ThreadLocal 的绑定关系。子线程拿到的是完全独立的连接和主线程的事务没有半毛钱关系等待主线程回滚是不可能把子线程的写入撤销的。要解决就得用编程式事务在子线程里单独管理或者引入真正支持跨线程传播的事务性消息方案。2.4 TransactionAttribute事务规则的四维定义TransactionAttribute是TransactionDefinition的增强版它除了定义传播行为、隔离级别、超时时间、只读标志之外还增加了一个关键能力——回滚规则的描述。先看一段常见的注解配置Transactional( propagation Propagation.REQUIRED, isolation Isolation.READ_COMMITTED, timeout 30, readOnly false, rollbackFor BizException.class, noRollbackFor IgnorableException.class ) public void doBiz() { ... }传播行为propagation决定了方法在执行时怎么处理事务边界。REQUIRED是默认值意思是有事务就加入没有就新建。这里面有很微妙的语义如果一个内外方法都是REQUIRED内层方法抛出异常整个事务都要回滚不管内层是不是 catch 住了。很多人以为 catch 住异常就能局部回滚但事务边界是跟着最外层走的内层一旦让事务状态标记为rollback-only外层自己commit也提交不上去最终报UnexpectedRollbackException。这个问题出现频率极高后面第 4 节单独讲。隔离级别isolation映射的是数据库层的事务隔离能力。READ_COMMITTED能解决脏读但解决不了不可重复读REPEATABLE_READ能解决不可重复读但在 MySQL 默认的 InnoDB 引擎下要和 MVCC 配合才能做到真正可重复读。你在事务级别配的隔离最终是要下发给数据库连接去执行的。有一点容易被忽略隔离级别的生效依赖底层数据库的支持如果连接本身已经被人为修改过隔离级别注解上配置的优先级不一定能覆盖。回滚规则rollbackFor是另一个重灾区。Spring 默认只对RuntimeException和Error回滚。你把一个IOException往上抛默认是不回滚的。我见过有人这样写Transactional public void uploadFile() throws IOException { // 写入数据库 // 上传文件 throw new IOException(上传失败); }结果数据库记录没有回滚。原因就是IOException是受检异常默认不触发回滚。正确做法是显式声明rollbackFor IOException.class或者把异常包成RuntimeException。3. 从 Bean 创建到事务提交核心流程的完整链路3.1 EnableTransactionManagement 到底做了什么Spring Boot 项目里事务能力通常是自动装配的但你手动加上EnableTransactionManagement时理解它的内部动作会对排查问题有极大帮助。这个注解的核心是Import(TransactionManagementConfigurationSelector.class)。TransactionManagementConfigurationSelector会导入两个关键配置类AutoProxyRegistrar注册InfrastructureAdvisorAutoProxyCreator这是一个BeanPostProcessor它的任务是扫描容器中所有Advisor给目标 Bean 生成代理ProxyTransactionManagementConfiguration这里会注册三个重要 Bean——BeanFactoryTransactionAttributeSourceAdvisor、TransactionAttributeSource负责解析Transactional、TransactionInterceptor。这三个 Bean 的分工可以理解为TransactionAttributeSource负责读懂哪些方法需要事务BeanFactoryTransactionAttributeSourceAdvisor负责把切点增强打包成一个 AdvisorTransactionInterceptor负责真正执行增强逻辑。而InfrastructureAdvisorAutoProxyCreator则像是一个调度中心等到业务 Bean 创建完成后它再统一决定要不要套一层代理。Spring Boot 的自动装配里其实已经包含了这套逻辑所以大多数项目你没有手动加EnableTransactionManagement事务也是能用的。3.2 Bean 实例化后代理是如何套上去的整个动态增强的起点是InfrastructureAdvisorAutoProxyCreator它的执行时机在所有 Bean 完成实例化和属性填充之后、正式返回给调用方之前。看这段伪代码逻辑// BeanPostProcessor 的核心回调 public Object postProcessAfterInitialization(Object bean, String beanName) { // 判断这个 Bean 是否有匹配的 Advisor ListAdvisor advisors findEligibleAdvisors(beanClass, beanName); if (!advisors.isEmpty()) { return createProxy(bean, beanName, advisors); } return bean; }findEligibleAdvisors内部就是拿BeanFactoryTransactionAttributeSourceAdvisor的切点去匹配当前 Bean 的方法。这里的关键是匹配范围切点只匹配公共方法private方法不会命中final方法不会命中。为什么因为 Spring AOP 的切点表达式在函数访问级别上有软性限制而 CGLIB 代理又无法覆盖 final 方法。所以这两类方法上的Transactional注定是摆设。createProxy内部有一个ProxyFactory它会根据目标类和代理配置选择 JDK 动态代理还是 CGLIB 代理。生成的代理对象会直接替换掉原先的 Bean 实例并被放入容器。从这一刻起容器里拿出来的对象就已经不是原始对象了而是套了一层拦截逻辑的升级版。你在业务代码中通过Autowired注入这个 Bean拿到的就是代理对象但如果类内部用this调用这个this指向的是原始对象代理链直接绕过了。3.3 一次事务调用的完整执行链路从外部调用一个带事务的方法开始整条链路的时序是这样的调用方持有的是代理对象调用doBiz()进入JdkDynamicAopProxy或CGLIB的拦截器链链条中命中TransactionInterceptor.invoke进入事务增强逻辑TransactionInterceptor从TransactionAttributeSource获取方法对应的TransactionAttribute解析出传播行为、隔离级别、回滚规则决定PlatformTransactionManager调用getTransactionDataSourceTransactionManager.doGetTransaction从TransactionSynchronizationManager的ThreadLocal中检查当前线程有没有绑定连接如果REQUIRED且有绑定直接复用如果没有从DataSource拿新连接关闭自动提交把连接绑定到ThreadLocal代理对象调用原始对象的业务方法如果业务方法正常返回TransactionInterceptor调用commitDataSourceTransactionManager执行connection.commit()然后清理资源如果业务方法抛出异常进入completeTransactionAfterThrowing先判断异常是否符合回滚规则RuntimeException/Error/rollbackFor指定的异常符合则调用connection.rollback()不符合则仍然提交无论提交还是回滚finally块里都要cleanupTransactionInfo并解绑ThreadLocal资源保证连接能回到连接池避免事务状态串到下一个请求。这里有一个很多人忽略但特别重要的细节事务的提交/回滚不总是发生在方法调用结束时。因为事务传播的存在最外层方法可能调用了多个内部事务方法内部方法只是参与同一个事务。所以真正的 commit/rollback 判定是在最外层事务创建者那里完成的。内部方法如果标记了REQUIRED它执行完后并不会提交而是把事务状态继续往上传。只有最外层创建事务的那个方法返回后整个事务才进入提交阶段。如果中间某个内部方法把事务标记成了rollback-only最外层提交时就会抛出UnexpectedRollbackException。3.4 事务同步回调机制Spring 事务还提供了一个扩展点TransactionSynchronization。它可以在事务提交前、提交后、回滚后等时点回调业务逻辑。典型场景是事务提交后再发消息事务回滚后清理缓存。TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { Override public void afterCommit() { // 事务提交成功后才发消息 } Override public void afterCompletion(int status) { // 事务结束提交或回滚后统一处理 } });这个机制在 本地事务消息发送 一致性场景里非常有用。事务没提交你就发 MQ 消息消费者立刻去查库往往会查到旧数据改成afterCommit后只有真正提交了才发消息能从根源上降低消息和数据不一致的概率。4. 事务不生效的经典现场与排查技巧实录4.1 四大失效场景注解没生效的幕后黑手第一个场景同类自调用。这也是我在各种项目里遇到最多的Service public class OrderService { public void doOuter() { // 一大堆业务逻辑 this.doInner(); // 自调用 } Transactional public void doInner() { ... } }doOuter直接调doInner时this是原始对象根本不会经过代理链。注解在doInner上再怎么写都是白搭。解决办法常见三种把事务方法拆到另一个 Service 里注入调用或者注入自己Autowired或Lazy注入代理对象或者用AopContext.currentProxy()注意要配置EnableAspectJAutoProxy(exposeProxy true)。第二个场景私有方法。Spring AOP 的切点只匹配外部可访问的方法私有方法既不会被切点命中子类代理也覆盖不了。检查一下你的Transactional是不是落在private方法上改成public或protected视代理模式而定。第三个场景方法被final修饰。CGLIB 生成子类后无法重写父类的 final 方法等同于没有增强。如果类本身是final连子类都生成不了。第四个场景异常被捕获或异常类型不符合回滚规则Transactional public void doBiz() { try { innerService.realWork(); } catch (Exception e) { log.error(捕获异常事务却回滚不了, e); } }这里业务方法把异常吞了事务管理器完全感知不到自然不会回滚。反过来如果你抛的是IOException这类受检异常又没有配rollbackForSpring 默认也不会回滚。还有更隐蔽的遇到数据库约束冲突导致的DataIntegrityViolationException是运行时异常正确回滚但如果你 catch 的是SQLException并且自己重新包装成一个受检异常抛出去又没配rollbackFor整个事务就悄悄提交了。4.2 嵌套事务与 rollback-only 的幽灵嵌套事务是另一个高频踩坑地。经典的PROPAGATION_REQUIRED嵌套内层方法抛异常被外层 catch 住看起来外层没有异常应该正常提交结果一提交就报UnexpectedRollbackException。原因在上面第 3.3 节里已经埋了伏笔内层方法运行时抛异常会让事务的状态被标记为rollback-only这个标记会一直跟着事务走。外层 catch 住了异常但事务管理器在最外层提交时发现这个事务已经被标记为只能回滚于是拒绝提交抛出UnexpectedRollbackException。要解决这个困境可以考虑把内层方法上的传播行为改成REQUIRES_NEW让它在一个新事务里独立执行、独立提交/回滚。这样内层失败只影响内层外层的 catch 才有意义。代价是内层和外层不再是同一个数据库事务一致性边界被缩放到了各自范围内。设计时要想清楚到底是不是真的要内层失败不影响外层。很多时候单纯 catch 住异常想让外围代码继续跑的想法本身就是违反事务语义的。4.3 多数据源与事务管理器绑定陷阱多数据源工程里的坑表面上看是事务没回滚实际上是从一开始就绑错了事务管理器。Transactional(businessTransactionManager) public void doBiz() { ... }如果你有两个数据源businessDataSource和logsDataSource容器里对应有两套DataSourceTransactionManager。Transactional上没有标注管理器名称时Spring 会尝试按类型从容器找找到多个就报NoUniqueBeanDefinitionException。你以为它随机拿了一个其实它直接启动失败或者绕过了事务。显式指定后你还要确认 MyBatis / JPA 的SqlSessionFactory和EntityManagerFactory用的是和事务管理器绑定的同一个DataSource。一旦DataSource错位事务管理器管理的连接和业务代码使用的连接不是同一个妥妥地不生效。排查这种问题最直接的办法是打印DataSourceTransactionManager的日志看它obtainDataSource得到的连接是不是跟SqlSession里的连接同一个。我习惯在事务开启后打一行日志输出TransactionSynchronizationManager.getResource(dataSource)是否为空。为空就说明这个数据源根本没和当前事务绑定。4.4 排查工具与实战日志定位法事务问题的排查光靠肉眼看代码是不够的建议直接用工具辅助定位。第一开启 Spring 框架的事务日志。在application.yml里加logging: level: org.springframework.transaction: DEBUG org.springframework.jdbc.datasource: DEBUG开启后可以完整看到事务的开启、提交、回滚日志。重点看两行Initiating transaction commit表示走到了提交分支Initiating transaction rollback表示走到了回滚分支。如果业务方法抛了异常但你发现日志里压根没走到回滚说明异常类型没被判定为回滚异常或者压根没有走代理。第二查看代理对象的真实类型。在业务代码里临时打一个日志System.out.println(orderService.getClass().getName());如果输出的类名是以$$EnhancerBySpringCGLIB$$结尾说明走了 CGLIB 代理如果是一堆$Proxy说明走了 JDK 动态代理如果输出就是原始类名OrderService那大概率代理没生成成功要么方法不符合切点要么这个 Bean 是被手动 new 出来的绕过容器管理的对象。第三利用断点观察。在TransactionInterceptor.invokeWithinTransaction打个断点看txAttr是否为 null。如果方法上有Transactional但txAttr是 null说明切点没有匹配到优先检查方法是否为 public、类是否被 Spring 管理。第四如果你用的是新版 Spring Boot可以依赖actuator的beans端点看 Bean 定义核对注入的类是否为代理类。但注意别在生产环境裸奔这个端口安全优先。4.5 一个综合案例从日志到定位的完整过程拿一个真实场景串一下排查思路。某项目反馈订单创建接口偶发库存扣了但订单没落库疑似事务回滚异常。第一步先看接口方法Transactional写在了 public 方法上没有指定rollbackFor接口抛出的异常是BizException而BizException继承的是RuntimeException。这一步符合默认回滚规则。第二步开启日志后复现发现日志里出现了Initiating transaction rollback说明回滚逻辑确实执行了。但问题在于回滚的是整个事务包括库存和订单。业务方预期是库存扣减不影响订单回滚这说明REQUIRED传播导致两者在同个事务里没有达成预期的部分提交。第三步重新梳理需求后发现库存扣减逻辑在事务方法内部调用了一个内部方法该方法上标注了Transactional(propagation Propagation.REQUIRES_NEW)。但由于是同类自调用this调用完全绕过了代理REQUIRES_NEW根本没有生效。这才是问题的根因。第四步修复方案是把库存扣减方法挪到独立的 Bean 中让外部 Bean 调用或者通过Lazy注入自身代理。改完后日志显示库存方法进入了全新事务外层回滚不再连累库存。这个案例里踩的坑是典型的经典问题叠加先是不了解回滚规则然后是不清楚自调用会绕过代理最后是传播行为配置了但没生效。排查过程里每一条知识缺一不可。5. 事务框架的扩展边界与实践中的取舍5.1 编程式事务什么时候该用声明式事务Transactional最大的优势是使用简单但它的劣势也很明显事务边界只能在方法级别粒度不够细。比如一个方法里前一半逻辑不需要事务后一半逻辑才需要事务这时候把整个方法丢进Transactional会白白占用数据库连接和事务。TransactionTemplate能解决这个问题Autowired private TransactionTemplate transactionTemplate; public void doBiz() { // 前一半逻辑非事务 transactionTemplate.execute(status - { // 后一半逻辑事务中执行 return null; }); }TransactionTemplate的底层其实就是PlatformTransactionManager的封装内部走的是和声明式事务一样的事务同步管理器。区别在于它是编程式地手动开启和提交没有 AOP 的额外成本。对于方法内部需要灵活分段、或者需要在子线程里独立控制事务的场景TransactionTemplate是比Transactional更可控的选择。5.2 回滚策略的精细化配置回滚规则不是只有rollbackFor这一个参数。Spring 支持同时配置哪些异常回滚和哪些异常不回滚Transactional( rollbackFor BizException.class, noRollbackFor IgnorableException.class )noRollbackFor的优先级高于rollbackFor。也就是说如果同一个异常类型同时命中两个规则最后以noRollbackFor为准。这个配置在处理某些业务异常不影响事务继续的场景时很好用但建议在代码注释里写清楚为什么某个异常不需要回滚否则后人接手时会一头雾水。还有一个值得提的点TransactionAspectSupport内部对异常是否回滚走的是RuleBasedTransactionAttribute的逻辑。它会先遍历rollbackFor规则再遍历noRollbackFor规则有个Depth的概念异常继承链的深度。如果两个规则都匹配比较哪个规则匹配的深度更近更具体的异常类型距离更近的规则生效。这个细节我花了好一会儿才看懂简单概括就是类型越具体的规则越有决定权。5.3 事务超时与连接池的博弈Transactional(timeout 10)表示事务最长执行 10 秒。默认情况下这个超时值会被设置到底层数据库连接上。但这里有一个坑超时计时是从事务开始算起的如果你在事务里等着外部接口调用 3 秒后才打数据库这 3 秒也计入超时。更隐蔽的是如果连接池里的连接已经被上一个事务污染比如隔离级别、超时时间没被正确重置新事务可能继承到旧的超时设置。这种问题非常难查表现是偶发的前 10 秒数据库操作正常某一次突然所有 SQL 都报查询超时。排查思路是先确认连接池的配置比如 HikariCP有没有开启connection-init-sql或者事务后的 reset。HikariCP 默认会自动重置连接的 autoCommit、隔离级别等但自定义超时不一定被重置。遇到诡异超时问题可以考虑在事务开始时手动执行SET SESSION innodb_lock_wait_timeout x但这属于兜底方案不建议常规使用。6. 踩过几次坑之后的个人体会事务这块光靠跑 demo 是跑不出体感的线上出几次事故反而理解得更深刻。我现在排查事务问题时习惯先问三个问题方法是不是被代理调用的异常类型走到回滚分支了吗连接是不是跟当前事务管理器绑定的这三个问题能覆盖掉绝大多数注解不生效的现场。还有个细节说给新手听写Transactional前先想想这个方法会被谁调用、调用方是不是从容器拿的 Bean、方法内部有没有this调用、异常会不会被吞掉。这四件事想清楚了事务基本不会给你挖坑。比起反复调节传播行为先保证代理链是通的框架才能按你设想的规则工作。真遇到排查不动的情况也别慌着改代码。先开日志、看代理类型、确认管理器绑定一步一步缩小范围。Spring 事务的每一个组件都在源码里、在日志里留下了痕迹只要愿意把断点打在TransactionInterceptor上答案往往就在那一个txAttr是 null 还是非 null 之间。