美团二面:Spring事务不生效的15种场景

发布时间:2026/10/8 8:26:40
美团二面:Spring事务不生效的15种场景 一、Spring 事务核心机制回顾要理解「事务为什么不生效」必须先理解「事务是怎么生效的」。很多同学背了一堆场景答案却说不清背后的原理一到换汤不换药的变体问题就露馅。所以我们先花一点篇幅把 Spring 事务的底层机制讲清楚。1.1 事务的基本概念与 ACID 特性事务是数据库操作的最小执行单元它把一组操作捆绑成一个不可分割的整体要么全部成功要么全部失败回滚。事务具备四个基本特性即 ACID原子性Atomicity事务中的操作要么全部完成要么全部不执行不存在中间状态。一致性Consistency事务执行前后数据库从一个一致性状态转换到另一个一致性状态约束不被破坏。隔离性Isolation多个事务并发执行时互不干扰每个事务都感觉不到其他事务的存在。持久性Durability事务一旦提交对数据库的修改是永久性的即使系统崩溃也不会丢失。Spring 事务本质上是建立在数据库本地事务之上的抽象。它本身不真正「实现」事务而是借助 JDBC、JPA、MyBatis 等底层技术去驱动数据库连接Connection完成事务的开启、提交和回滚。因此Spring 事务能否生效首先取决于底层数据库是否支持事务这也是后文场景 6 的伏笔。1.2 Spring 事务的两种管理方式Spring 提供了两种事务管理手段编程式事务和声明式事务。编程式事务是指开发者在代码中显式地通过 TransactionTemplate 或 PlatformTransactionManager 来控制事务边界。它的优点是控制粒度细、行为明确缺点是代码侵入性强、大量样板代码影响可读性。javaService public class OrderService { private final TransactionTemplate transactionTemplate; public OrderService(TransactionTemplate transactionTemplate) { this.transactionTemplate transactionTemplate; } public void createOrder(Order order) { transactionTemplate.execute(status - { // 业务逻辑 orderDao.insert(order); return null; }); } }声明式事务则是通过 Transactional 注解或 XML 配置来声明事务规则。开发者只需要在方法或类上打一个注解Spring 容器就会在运行期通过 AOP 织入事务管理逻辑。它代码侵入性低、使用简单是目前企业开发的主流选择也是面试考察的重点。javaService public class OrderService { Transactional public void createOrder(Order order) { orderDao.insert(order); stockDao.deduct(order.getProductId(), order.getCount()); } }这里要先建立一个重要认知声明式事务并不是魔法它的底层依然是 AOP 编程式事务。Transactional 只是把「开启事务、正常提交、异常回滚」这套模板代码交给框架自动生成。理解了这一点后面很多「不生效」的原因就豁然开朗了。1.3 Transactional 的底层原理AOP 动态代理这是整个专题中最关键的一节请务必理解透彻。Spring 声明式事务的实现核心依赖的是AOP 动态代理。当你给一个 Service 的方法加上 Transactional 之后Spring 容器在初始化该 Bean 时会发现这个 Bean 需要被事务增强。此时容器不会把原始的 Service 对象直接注入给调用方而是会创建一个代理对象。这个代理对象和原始对象实现了相同的接口JDK 动态代理或者继承了相同的类CGLIB 代理。当外部调用方调用代理对象的方法时调用链大致是这样的调用方通过依赖注入拿到的是代理对象而不是原始对象。调用代理对象的 createOrder 方法。代理对象内部的 TransactionInterceptor事务拦截器被触发。事务拦截器通过 PlatformTransactionManager 从数据库连接中开启事务。事务开启后代理对象通过反射调用原始对象的 createOrder 方法执行真正的业务逻辑。业务逻辑正常返回事务拦截器提交事务业务逻辑抛出异常则回滚事务。可以用下面的伪代码描述代理逻辑javapublic class OrderServiceProxy extends OrderService { private final OrderService target; // 原始对象 private final PlatformTransactionManager txManager; Override public void createOrder(Order order) { TransactionStatus status txManager.getTransaction(...); try { target.createOrder(order); // 调用原始对象的业务方法 txManager.commit(status); } catch (Exception e) { txManager.rollback(status); throw e; } } }这只是一个简化的示意图真实实现要复杂得多但核心思想就是一句话事务逻辑织入在代理对象上业务逻辑运行在原始对象上只有穿过代理对象的调用才会经过事务拦截器。请牢牢记住这个调用模型因为后文至少有一半的「事务不生效」场景都是因为调用「没有穿过代理对象」而导致的。最典型的就是同类内部方法自调用在原始对象内部用 this 调另一个方法这个 this 是原始对象不是代理对象所以事务拦截器根本没有执行的机会。此外还需要补充一点Spring Boot 2.x 之后AOP 默认使用CGLIB 代理即使类实现了接口默认也走 CGLIB 子类代理而在较早的版本中如果类实现了接口默认会使用 JDK 动态代理。这个差异会在后文的某些场景中体现出来。二、15 种 Spring 事务不生效场景详解下面正式进入本文的核心部分。我会按照「被代理拦截问题 → 异常处理问题 → 传播与配置问题 → 环境与边界问题 → 并发与切面问题」的逻辑把 15 个场景分为几大类逐一剖析。场景 1Transactional 注解放在非 public 方法上现象你在一个 Service 的 private 或 protected 方法上加了 Transactional执行时发现事务根本没开启业务数据异常时也没有回滚。javaService public class UserService { Autowired private UserDao userDao; Transactional private void updateUserInternal(User user) { // 错误private 方法 userDao.update(user); throw new RuntimeException(模拟异常); } public void updateUser(User user) { updateUserInternal(user); // 内部调用 } }失效原理Spring 官方文档明确规定Transactional 注解只能用于 public 方法。如果注解标注在 private、protected 或包级私有方法上Spring 不会报错但也不会为该方法的调用织入事务逻辑。原因要从代理机制说起。对于 JDK 动态代理代理对象和原始对象都实现了同一个接口而接口中的方法默认就是 publicprivate/protected 方法根本不在接口中代理对象无从拦截。对于 CGLIB 子类代理它通过继承原始类并覆写 public 方法来实现拦截但 private 方法无法被子类覆写protected 方法虽然可以被覆写但 Spring 的事务增强器默认只处理 public 方法因此同样不会生效。javapublic abstract class AbstractFallbackTransactionAttributeSource { protected boolean allowPublicMethodsOnly() { return this.publicMethodsOnly; // 默认 true只允许 public 方法 } }解决方案把 Transactional 加在 public 方法上确保外部调用的是被代理的 public 方法。javaService public class UserService { Autowired private UserDao userDao; Transactional public void updateUser(User user) { // 正确public 方法 userDao.update(user); throw new RuntimeException(模拟异常); } }面试追问如果面试官继续问「Spring 为什么不支持非 public 方法的事务」你可以从两个层面回答一是代理机制的限制private 方法无法被 JDK/CGLIB 代理拦截二是 Spring 团队的设计决策官方出于性能与行为一致性的考虑默认只代理 public 方法。虽然可以通过一些 Hack 方式让 protected 方法也生效但强烈不建议在生产环境这么做。场景 2同类内部方法自调用事务失效现象一个 public 事务方法 A 内部调用另一个 public 事务方法 B单独调用 B 时事务正常但通过 A 调用 B 时B 的事务却不生效。javaService public class OrderService { Autowired private OrderDao orderDao; Transactional public void createOrder(Order order) { orderDao.insert(order); // 操作 1 this.updateStock(order); // 同类内部调用事务失效 } Transactional(propagation Propagation.REQUIRES_NEW) public void updateStock(Order order) { stockDao.deduct(order.getProductId(), order.getCount()); } }上面的代码中createOrder 通过 this.updateStock 调用了 updateStock。开发者本意是希望 updateStock 使用 REQUIRES_NEW 开启一个独立事务即使 createOrder 回滚库存扣减也能独立提交。但实际运行时REQUIRES_NEW 根本没生效updateStock 只是在 createOrder 的事务里执行createOrder 一回滚库存扣减也被回滚了。失效原理这正是我们在 1.3 节埋下的伏笔。外部调用 createOrder 时调用链经过的是代理对象的 createOrder 方法事务拦截器正常开启事务。但进入 createOrder 之后this 指向的是原始对象而不是代理对象。用 this.updateStock 调用时调用发生在原始对象内部根本没有经过代理对象因此代理对象上的事务拦截器不会被触发Transactional(REQUIRES_NEW) 形同虚设。java// 实际执行效果 public class OrderServiceProxy extends OrderService { public void createOrder(Order order) { // 开启事务 TX-1 target.createOrder(order); // 进入原始对象 // 原始对象内部this.updateStock(order) // this 是原始对象不是代理对象事务拦截器不触发 // 提交/回滚事务 TX-1 } }解决方案解决自调用问题的核心思路是让内部调用也经过代理对象。常见方案有以下三种。方案一把被调用的方法拆到另一个 Bean 中通过依赖注入的方式调用。javaService public class OrderService { Autowired private OrderDao orderDao; Autowired private StockService stockService; // 拆到独立 Bean Transactional public void createOrder(Order order) { orderDao.insert(order); stockService.updateStock(order); // 通过注入的代理对象调用事务生效 } } Service public class StockService { Autowired private StockDao stockDao; Transactional(propagation Propagation.REQUIRES_NEW) public void updateStock(Order order) { stockDao.deduct(order.getProductId(), order.getCount()); } }方案二从 Spring 容器中获取自己的代理对象通过代理对象调用。可以注入 ApplicationContext或者直接注入自己前提是处理循环依赖问题。javaService public class OrderService { Autowired private ApplicationContext applicationContext; Autowired private OrderDao orderDao; Transactional public void createOrder(Order order) { orderDao.insert(order); OrderService proxy applicationContext.getBean(OrderService.class); proxy.updateStock(order); // 拿到代理对象再调用 } Transactional(propagation Propagation.REQUIRES_NEW) public void updateStock(Order order) { stockDao.deduct(order.getProductId(), order.getCount()); } }方案三使用 AopContext.currentProxy() 获取当前代理对象。需要先在启动类或配置类上开启 exposeProxy。javaSpringBootApplication EnableAspectJAutoProxy(exposeProxy true) // 开启暴露代理对象 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } Service public class OrderService { Autowired private OrderDao orderDao; Transactional public void createOrder(Order order) { orderDao.insert(order); ((OrderService) AopContext.currentProxy()).updateStock(order); } Transactional(propagation Propagation.REQUIRES_NEW) public void updateStock(Order order) { stockDao.deduct(order.getProductId(), order.getCount()); } }面试追问「为什么自调用会失效而通过依赖注入调用另一个 Bean 就有效」核心答案是依赖注入拿到的另一个 Bean 是容器创建的代理对象调用它一定会经过事务拦截器而 this 是原始对象绕过了代理。如果面试官继续问 AopContext 的原理可以回答exposeProxytrue 时Spring 会把当前代理对象绑定到 ThreadLocal 中AopContext.currentProxy() 就是从 ThreadLocal 里取出来。场景 3异常被 catch 吞掉事务未回滚现象事务方法内部抛出了异常但异常被 try-catch 捕获并且没有重新抛出结果事务正常提交脏数据落库。javaService public class OrderService { Autowired private OrderDao orderDao; Transactional public void createOrder(Order order) { try { orderDao.insert(order); // 模拟业务异常 throw new RuntimeException(库存扣减失败); } catch (Exception e) { log.error(创建订单异常, e); // 异常被吞掉没有继续抛出 } // 方法正常返回事务被提交 } }失效原理Spring 事务的异常回滚机制是「捕获异常 → 判断是否满足回滚条件 → 回滚或提交」。而事务拦截器只能捕获从业务方法中向外传播的异常。如果业务方法自己把异常 catch 住并消化掉那么在事务拦截器看来这个方法正常返回了自然就会执行 commit。这一点可以用简化伪代码解释javatry { target.createOrder(order); // 业务方法内部已经 catch 了异常正常返回 txManager.commit(status); // 拦截器认为执行成功提交事务 } catch (Throwable ex) { txManager.rollback(status); // 只有异常抛到这一层才会回滚 throw ex; }解决方案捕获异常后要么在 catch 块中手动设置回滚要么将异常重新抛出。方案一重新抛出异常推荐符合 Fail Fast 原则。javaTransactional public void createOrder(Order order) { try { orderDao.insert(order); throw new RuntimeException(库存扣减失败); } catch (Exception e) { log.error(创建订单异常, e); throw new RuntimeException(e); // 重新抛出触发事务回滚 } }方案二手动标记事务回滚。javaTransactional public void createOrder(Order order) { try { orderDao.insert(order); throw new RuntimeException(库存扣减失败); } catch (Exception e) { log.error(创建订单异常, e); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); // 手动标记当前事务为回滚状态事务拦截器最终会执行回滚 } }面试追问「捕获了异常但希望事务回滚有哪几种处理方式」可以回答三种第一在 catch 块中重新抛出异常第二调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()手动标记回滚第三采用编程式事务在业务代码中显式调用 rollback。面试官还可能追问「setRollbackOnly 和显式 rollback 有什么区别」可以说明 setRollbackOnly 只是设置回滚标记真正的回滚仍由事务拦截器在方法返回后执行而显式 rollback 会立即回滚。场景 4检查型异常默认不回滚现象事务方法中抛出了 IOException、SQLException 这类受检异常方法异常退出后数据库操作竟然正常提交了没有任何回滚。javaService public class OrderService { Autowired private OrderDao orderDao; Transactional public void importOrder(Order order) throws IOException { orderDao.insert(order); Files.readAllBytes(Paths.get(not-exist.json)); // 抛出 IOException } }失效原理Spring 的默认回滚规则是只有抛出 RuntimeException 或 Error 时才会回滚事务受检异常默认不会触发回滚。这是因为 Spring 认为受检异常属于业务中可以被预期和处理的异常是否回滚应该交给开发者决定。因此上述代码中 IOException 虽然向外传播到了事务拦截器但拦截器判断它不属于默认回滚类型仍然选择提交事务。解决方案在 Transactional 注解上显式指定回滚的异常类型。javaTransactional(rollbackFor Exception.class) public void importOrder(Order order) throws IOException { orderDao.insert(order); Files.readAllBytes(Paths.get(not-exist.json)); }上述写法表示对 Exception 及其所有子类都会回滚。如果只想回滚特定受检异常可以写成Transactional(rollbackFor {IOException.class, SQLException.class})。面试追问「为什么 Spring 默认只回滚 RuntimeException」可以从设计理念回答受检异常往往代表业务可感知、可恢复的情况框架不强行为你决定是否回滚而运行时异常通常表示编程错误或不可预期的问题默认回滚更符合快速失败原则。通过 rollbackFor 可以让开发者自定义这一行为。场景 5rollbackFor 配置错误现象虽然配置了 rollbackFor但业务方法抛出的异常不在配置范围内导致事务没有如预期回滚。javapublic class BizCheckedException extends Exception { } Service public class OrderService { Autowired private OrderDao orderDao; Transactional(rollbackFor RuntimeException.class) // 配置错误 public void syncOrder(Order order) throws BizCheckedException { orderDao.insert(order); throw new BizCheckedException(); // BizCheckedException 不在回滚范围内 } }失效原理rollbackFor 指定的是回滚异常类型的「白名单」。只有当实际抛出的异常是该类型或其子类时事务才会回滚。上例中 BizCheckedException 是直接继承 Exception 的受检异常不是 RuntimeException因此不满足回滚条件事务被提交。类似的坑还有配置的是同类不同包、父类与子类搞反、拼错了异常类名等情况。解决方案把 rollbackFor 配置为能够覆盖业务异常类型的父类或同时使用 rollbackForClassName 指定多个异常类名。javaTransactional(rollbackFor Exception.class) public void syncOrder(Order order) throws BizCheckedException { orderDao.insert(order); throw new BizCheckedException(); }面试追问「rollbackFor 和 rollbackForClassName 有什么区别」前者传入 Class 对象数组需要强制类型检查编译期更安全后者传入异常的完整类名字符串数组适合某些配置化或不得不延迟加载异常类的场景。两者都用来覆盖默认回滚策略。场景 6数据库引擎不支持事务现象MySQL 中创建表时使用了 MyISAM 引擎代码里虽然加了 Transactional但发生异常后数据没有回滚脏数据仍然落库。失效原理Spring 事务不是凭空产生的它最终要落到数据库连接的真实事务上。MyISAM 引擎本身不支持事务即使 Spring 代码层面正确开启了事务底层每执行一条 SQL 仍然会被立即提交回滚操作自然也不会生效。换句话说Spring 事务的边界建立在「数据库支持事务」这个前提之上。解决方案将业务表切换为支持事务的 InnoDB 引擎。sql-- 查看表引擎 SHOW TABLE STATUS LIKE t_order; -- 修改为 InnoDB ALTER TABLE t_order ENGINE InnoDB;生产环境中新建业务表时应默认采用 InnoDB并在建表规范中明确禁止使用 MyISAM 存储核心业务数据。面试追问「MyISAM 和 InnoDB 在事务支持上有哪些差异」可以回答InnoDB 支持事务、行级锁、外键而 MyISAM 不支持事务和外键只支持表级锁因此涉及订单、账户、库存等强一致性的场景必须使用 InnoDB。这个问题还经常和「Spring 事务为什么没回滚」合并考察考察候选人是否具备从框架层一直排查到数据库存储引擎的意识。场景 7没有开启事务管理或依赖不完整现象项目里加了 Transactional 注解但运行后事务完全没生效甚至连代理都没有织入。失效原理声明式事务需要事务管理器支撑。常见的配置缺失包括引入的依赖不完整例如只引入了 spring-boot-starter-web但依赖 spring-jdbc 的事务实现没有引入、数据源 Bean 没有正确装配、或者纯 Spring 项目忘记添加 EnableTransactionManagement。没有事务管理器Transactional 只会被当作一个普通注解不会产生任何事务行为。java// Spring Boot 会通过自动配置帮我们识别数据源并创建事务管理器。 // 但如果依赖或配置不完整事务管理器不会自动创建注解自然失效。 // 检查依赖中是否包含 // org.springframework:spring-jdbc 或 spring-boot-starter-jdbc // 纯 Spring XML/JavaConfig 项目还需要显式开启事务管理 Configuration EnableTransactionManagement public class AppConfig { Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }解决方案先检查依赖是否完整再确认数据源和事务管理器 Bean 是否已被 Spring 容器管理。Spring Boot 项目通常只需引入 spring-boot-starter-jdbc 或 spring-boot-starter-data-jpa同时正确配置 DataSource 即可纯 Spring 项目则要显式标注 EnableTransactionManagement。面试追问「Spring Boot 为什么很多时候不用手写 EnableTransactionManagement」可以回答Spring Boot 的事务自动配置类 TransactionAutoConfiguration 会在检测到数据源存在且ConditionalOnMissingBean(PlatformTransactionManager.class)成立时自动注册 DataSourceTransactionManager并且当 classpath 存在 spring-tx 时自动开启事务管理。理解了自动配置机制才能快速定位这种“配置缺失”类问题。场景 8多数据源下未指定事务管理器现象一个项目同时连接订单库和用户库配置了多个数据源但 Transactional 没有指定使用哪个事务管理器操作某个数据源时发现事务失效或者事务作用在错误的数据源上。失效原理当 Spring 容器中存在多个 PlatformTransactionManager Bean 时Transactional 需要明确知道用哪一个。如果不指定 transactionManager 属性Spring 会默认使用当前容器中的 primary 事务管理器。一旦业务操作的数据库连接与事务管理器绑定的 DataSource 不一致事务自然无法在目标数据库上正确生效。javaService public class OrderService { Transactional(transactionManager orderTransactionManager) public void createOrder(Order order) { orderDao.insert(order); userDao.updateUserPoint(order.getUserId()); // 假设这里需要用户库事务应该用另一个事务管理器 } }解决方案为每个数据源分别注册事务管理器并在 Transactional 上通过 transactionManager 属性精确指定。javaConfiguration public class DataSourceConfig { Bean(orderTransactionManager) public PlatformTransactionManager orderTxManager( Qualifier(orderDataSource) DataSource orderDataSource) { return new DataSourceTransactionManager(orderDataSource); } Bean(userTransactionManager) public PlatformTransactionManager userTxManager( Qualifier(userDataSource) DataSource userDataSource) { return new DataSourceTransactionManager(userDataSource); } }面试追问「为什么多数据源场景事务很难自动生效」可以回答事务的开启、连接绑定、提交回滚都必须发生在同一个数据源的连接上框架无法自动推断方法内部到底操作了哪个库所以需要开发者在业务边界上明确指定事务管理器当一次方法调用涉及多个数据源时单机本地事务无法保证跨库一致性这时要进一步考虑分布式事务方案。场景 9事务传播行为配置错误现象开发者把事务传播行为配置为 SUPPORTS、NOT_SUPPORTED 或 NEVER误以为方法里已经开启了事务结果异常发生时数据没有回滚甚至直接抛异常。javaService public class OrderService { Transactional(propagation Propagation.SUPPORTS) // 错误当前有事务才加入没有则不开启 public void createOrder(Order order) { orderDao.insert(order); throw new RuntimeException(库存扣减失败); } }失效原理事务传播行为决定了一个方法在调用时「是否加入事务」以及「如何处理已有事务」。SUPPORTS 表示当前存在事务就加入不存在就以非事务方式执行NOT_SUPPORTED 会主动挂起当前事务并以非事务方式执行NEVER 则要求当前必须没有事务否则抛异常。如果误用了这些不会真正开启事务的传播行为异常发生后自然没有事务可以回滚。解决方案先理解各传播行为的语义再按业务选择。REQUIRED默认值。当前有事务则加入没有则新建事务。REQUIRES_NEW总是新建一个独立事务并挂起当前事务。NESTED使用嵌套事务内层借由 savepoint 实现部分回滚。SUPPORTS / NOT_SUPPORTED / NEVER不主动开启事务适合只读或明确不需要事务的场景。MANDATORY必须存在事务否则抛异常。javaTransactional(propagation Propagation.REQUIRED) // 正确没有事务时新建 public void createOrder(Order order) { orderDao.insert(order); throw new RuntimeException(库存扣减失败); }面试追问「REQUIRES_NEW 和 NESTED 到底有什么区别」可以回答REQUIRES_NEW 会挂起外层事务并创建完全独立的新事务内外层事务互不影响NESTED 则依赖数据库 savepoint 机制内层事务是外层事务的一部分内层回滚只会回滚到 savepoint外层事务仍可继续提交。是否支持 NESTED 还取决于底层事务管理器和数据库是否支持 savepoint。场景 10Bean 没有被 Spring 容器管理现象在某个 Service 方法里加 Transactional 后测试发现事务不生效后来发现这个对象根本是开发者自己 new 出来的或者漏加了 Service、Component 注解。java// 错误示范 1手工 new 对象 OrderService orderService new OrderService(); orderService.createOrder(order); // 事务不生效 // 错误示范 2漏写 Service public class OrderService { ... }失效原理声明式事务的本质是 Spring 在 Bean 初始化时创建代理对象并在代理对象上织入事务拦截器。手工 new 出来的对象没有经过 Spring Bean 生命周期不会被代理Transactional 注解自然形同虚设。同样漏写 Service/Component 导致对象根本没有被放进容器即使通过 Autowired 注入也不会拿到带事务增强的代理 Bean。解决方案把需要事务的类交给 Spring 容器管理并通过依赖注入获取对象。javaService public class OrderService { Autowired private OrderDao orderDao; Transactional public void createOrder(Order order) { orderDao.insert(order); } }面试追问「为什么 Spring Boot 中自己 new 的对象使用 Autowired 字段时会报 NPE」因为 Autowired 字段注入依赖容器完成new 出来的对象不在容器管理范围内字段不会被装配。这个问题能进一步引出依赖注入、Bean 生命周期和代理生成时机等知识点。场景 11事务方法被 final 修饰现象给一个 public 方法同时加上了 Transactional 和 final 修饰符方法执行报错时却没有回滚。javaService public class OrderService { Transactional public final void createOrder(Order order) { // 错误final 方法无法被 CGLIB 代理 orderDao.insert(order); throw new RuntimeException(模拟异常); } }失效原理Spring Boot 2.x 默认使用 CGLIB 代理而 CGLIB 的实现原理是生成原始类的子类并覆写目标方法。final 方法无法被子类继承和覆写因此 CGLIB 无法对该方法做增强事务切面不会织入。虽然项目不会直接报错但事务拦截器无法被触发。解决方案去掉方法上的 final 修饰符或者将事务方法设计为可被覆写的普通 public 方法。javaService public class OrderService { Transactional public void createOrder(Order order) { // 正确去掉 final orderDao.insert(order); throw new RuntimeException(模拟异常); } }面试追问「为什么 JDK 动态代理没有 final 方法的问题」因为 JDK 动态代理基于接口实现代理类和目标类都实现同一个接口final 只影响类继承不影响接口方法的实现关系。但 JDK 代理要求目标类必须实现接口所以两者各有适用场景。CGLIB 场景下 final 方法、final 类都会导致代理失效。场景 12多线程调用导致事务失效现象在事务方法内部开启子线程执行数据库操作主线程抛出异常回滚后子线程里的操作却没有回滚或者子线程抛异常时主线程事务并未回滚。javaService public class OrderService { Transactional public void createOrder(Order order) { orderDao.insert(order); new Thread(() - { stockDao.deduct(order.getProductId(), order.getCount()); }).start(); throw new RuntimeException(主线程抛出异常); } }失效原理Spring 事务资源是通过 ThreadLocal 绑定在当前线程上的事务管理器在开启事务时会把对应的数据库连接绑定到当前线程。子线程拥有独立的线程上下文无法拿到主线程的数据库连接也就无法参与主线程的事务。因此子线程的数据库操作默认是自动提交的主线程回滚不会影响子线程子线程回滚也不会影响主线程。解决方案如果业务上确实需要并发执行数据库操作并保证整体一致性需要考虑以下思路避免在事务方法内开启子线程执行数据库写操作把并发逻辑放到事务外用更明确的一致性方案处理。使用编程式事务手动控制子线程的事务边界并在主线程等待子线程结果后统一决定提交或回滚。考虑分布式事务或补偿机制当操作跨线程、跨库、跨服务时本地事务已无法保证一致性需要引入 TCC、Saga、消息最终一致性等方案。java// 不推荐事务内开子线程 Transactional public void createOrder(Order order) { orderDao.insert(order); // 子线程不会加入主线程事务 new Thread(() - stockDao.deduct(...)).start(); } // 更合理把需要一致的写操作放在同一个事务内串行执行 Transactional public void createOrder(Order order) { orderDao.insert(order); stockDao.deduct(order.getProductId(), order.getCount()); }面试追问「ThreadLocal 在 Spring 事务中扮演什么角色」可以回答Spring 通过TransactionSynchronizationManager用 ThreadLocal 保存当前线程的事务状态和数据库连接资源保证同一个线程内的数据库操作复用同一个连接从而实现事务的绑定与传播跨线程时 ThreadLocal 无法共享所以事务也会失效。场景 13事务方法被同类内部调用且非 public这是场景 1 和场景 2 的叠加变体也是面试中常见的追问形式。现象一个 public 方法内部调用了一个 private 的事务方法开发者以为 private 方法上的 Transactional 会生效但实际上既因为非 public 被忽略又因为自调用没有经过代理双重失效。javaService public class OrderService { public void createOrder(Order order) { this.doCreate(order); // 自调用 private双重失效 } Transactional private void doCreate(Order order) { orderDao.insert(order); throw new RuntimeException(模拟异常); } }失效原理一方面private 方法不符合 Spring 事务对 public 方法的要求另一方面即使是 public 方法自调用也不会经过代理。两个条件同时成立时事务必然失效。解决方案把事务方法改为 public并通过代理对象调用或者把事务方法拆到另一个 Bean 中。javaService public class OrderService { Autowired private OrderTxService orderTxService; public void createOrder(Order order) { orderTxService.doCreate(order); // 通过代理对象调用 } } Service public class OrderTxService { Transactional public void doCreate(Order order) { orderDao.insert(order); throw new RuntimeException(模拟异常); } }面试追问「如果事务方法必须被内部调用有什么办法」可以回答把方法拆到独立 Bean、注入自身代理、使用 AopContext.currentProxy()、或者用编程式事务替代声明式事务。场景 14切面顺序导致事务未生效现象项目中同时使用了自定义 AOP 切面和 Transactional自定义切面在事务切面之前执行并吞掉了异常导致事务没有回滚。javaAspect Component Order(1) // 优先级高于事务切面 public class LogAspect { Around(execution(* com.example.service.*.*(..))) public Object around(ProceedingJoinPoint pjp) throws Throwable { try { return pjp.proceed(); } catch (Exception e) { log.error(业务异常, e); return null; // 异常被吞掉事务切面感知不到异常 } } }失效原理Spring 事务本身也是一个 AOP 切面切面的执行顺序由 Order 或 Ordered 接口决定。如果自定义切面优先级高于事务切面且在外层吞掉了异常那么内层的事务切面就无法感知到异常自然不会回滚。反过来如果自定义切面优先级低于事务切面事务切面会先处理异常行为又可能不符合预期。解决方案明确切面顺序避免自定义切面吞掉异常如果必须在自定义切面中处理异常确保异常能够继续向外传播或者手动标记事务回滚。javaAspect Component Order(Ordered.LOWEST_PRECEDENCE) // 让事务切面优先执行 public class LogAspect { Around(execution(* com.example.service.*.*(..))) public Object around(ProceedingJoinPoint pjp) throws Throwable { try { return pjp.proceed(); } catch (Exception e) { log.error(业务异常, e); throw e; // 重新抛出保证事务切面能感知异常 } } }面试追问「事务切面的优先级是多少」Spring 事务切面的默认 order 是Ordered.LOWEST_PRECEDENCE也就是优先级最低、最内层。这样设计是为了让其他切面如日志、监控在外层事务在内层便于事务切面准确感知业务异常。场景 15只读事务中执行写操作现象在Transactional(readOnly true)的方法中执行了 INSERT、UPDATE、DELETE 操作结果写操作没有生效或抛出异常。javaService public class OrderService { Transactional(readOnly true) public void createOrder(Order order) { orderDao.insert(order); // 在只读事务中执行写操作 } }失效原理readOnly true会告诉底层数据库连接这是一个只读事务。不同的数据库驱动和连接池对只读事务的处理不同有的会抛出异常有的会忽略写操作有的则会正常执行但失去事务的一些优化。总的来说只读事务应该只用于查询场景写入操作不应放在只读事务中。解决方案把写操作从只读事务方法中移出或者去掉readOnly true。java// 正确只读事务只用于查询 Transactional(readOnly true) public Order queryOrder(Long id) { return orderDao.selectById(id); } // 正确写操作使用默认可写事务 Transactional public void createOrder(Order order) { orderDao.insert(order); }面试追问「readOnly true 有哪些好处」可以回答一是语义清晰明确告诉团队这个方法只做查询二是性能优化某些数据库和连接池会对只读事务做特殊处理减少锁竞争和日志开销三是安全兜底某些驱动会拒绝只读事务中的写操作避免误写。三、15 种场景速查表编号场景核心原因一句话修复1非 public 方法加 Transactional代理无法拦截非 public 方法改为 public2同类内部自调用this 是原始对象未经过代理拆 Bean、注入自身、AopContext3异常被 catch 吞掉拦截器感知不到异常重新抛出或 setRollbackOnly4受检异常默认不回滚默认只回滚 RuntimeException/Error配置 rollbackFor5rollbackFor 配错异常类型不在白名单用 Exception.class 覆盖6数据库引擎不支持事务MyISAM 无事务能力改用 InnoDB7未开启事务管理或依赖缺失没有事务管理器补依赖、加 EnableTransactionManagement8多数据源未指定事务管理器默认用了 primary 管理器指定 transactionManager9传播行为配置错误SUPPORTS/NEVER 等不开启事务按业务选 REQUIRED/REQUIRES_NEW10Bean 未被容器管理new 出来的对象没有代理交给 Spring 管理11final 方法CGLIB 无法覆写 final 方法去掉 final12多线程调用ThreadLocal 不跨线程避免事务内开子线程13自调用 非 public双重失效改 public 走代理14切面顺序问题自定义切面吞掉异常调整 Order重新抛异常15只读事务中写操作readOnly 限制写操作写操作去掉 readOnly四、事务失效排查方法论面试中如果被追问「线上事务不生效你怎么排查」可以按下面的顺序回答体现系统性思维。先看现象是完全没有回滚还是部分回滚是提交了脏数据还是直接抛异常确认代理是否生成在方法内打印this.getClass()看是否是代理类或者用AopUtils.isAopProxy(this)判断。确认调用是否穿过代理检查是否存在自调用、new 对象、非 public 方法等情况。确认异常传播路径检查是否有 try-catch 吞异常、自定义切面吞异常、受检异常未配 rollbackFor。确认事务管理器检查是否配置了 PlatformTransactionManager、多数据源是否指定正确的事务管理器。确认数据库层检查表引擎是否支持事务、连接池是否自动提交、数据库隔离级别是否符合预期。确认传播行为检查 REQUIRES_NEW、NESTED 等传播行为是否符合业务预期。五、总结Spring 事务不生效的场景虽然多但归根到底可以归纳为几类根因代理问题非 public、final、自调用、new 对象、切面顺序本质都是事务拦截器没有被触发。异常问题异常被吞、受检异常未配 rollbackFor本质是事务拦截器没有感知到该回滚的信号。配置问题未开启事务管理、多数据源、传播行为、只读事务本质是事务边界和规则配置不符合预期。环境问题数据库引擎、多线程本质是事务能力受限于底层环境。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询