1. 开篇为什么面试官总爱问 Transactional在美团、字节、阿里等大厂的一二面中Transactional 几乎是一个绕不开的高频考点。很多候选人能熟练地在 Service 方法上加上注解也能说出「声明式事务基于 AOP」「默认只对 RuntimeException 回滚」但一旦面试官追问一句「事务真正是在哪一行代码开启的」「为什么自调用会让事务失效」「REQUIRES_NEW 和 NESTED 到底差在哪」不少人就开始含糊其辞了。出现这个现象的原因并不复杂Transactional 把大量底层细节封装得太好了开发者写业务代码时只需要加一个注解Spring 就会替我们完成「获取连接、关闭自动提交、提交、回滚、释放连接」这一整套动作。封装带来便利的同时也带来了一层认知遮蔽——如果你不理解代理、AOP、事务管理器、传播行为和线程绑定这几个关键环节就很容易踩到事务「莫名其妙失效」的坑也容易在面试中露怯。这篇文章的目标就是把 Transactional 从「能用的程度」讲到「能讲清原理、能定位问题、能应对追问」的程度。我们会重点覆盖以下内容Spring 事务管理的整体架构声明式事务和编程式事务的本质区别Transactional 常用属性的含义与正确用法事务底层原理JDK 代理与 CGLIB、TransactionInterceptor 拦截链路、DataSourceTransactionManager 的工作过程七种传播行为、四种隔离级别逐一拆解并说明在 MySQL 下的表现十余个真实生产中的「事务失效」场景与避坑方案美团一面常见的连环追问与推荐回答思路结合源码的关键流程分析以及实战案例。建议读者在阅读时结合自己项目中的事务代码进行对照尤其是第七节的「常见坑」部分每一条都值得在自己的代码里做一次排查。下面我们正式开始。2. 先建立整体认知Spring 事务到底在做什么2.1 事务的核心语义在深入注解之前我们需要先明确「数据库事务」的四个基本特性也就是常说的 ACID原子性Atomicity一组操作要么全部成功要么全部失败。例如「扣减余额」和「生成订单」必须同时成功或同时失败不能只完成一半。一致性Consistency事务执行前后数据都必须处于合法状态不能违反业务规则和数据库约束。隔离性Isolation多个并发事务之间互不干扰一个事务的中间状态不应该被另一个事务读到。持久性Durability事务一旦提交其修改就是永久的即使系统宕机也不会丢失。Spring 的 Transactional 并不是自己去实现这些特性它本质上是对底层数据库事务能力的一次封装和编排。真正干活的是数据库连接Connection和数据库本身Spring 只是在合适的时机调用 commit() 或 rollback()并负责把「哪个连接绑定到哪个线程」这件事管理好。2.2 编程式事务与声明式事务在没有 Spring 之前JDBC 操作事务的代码大概是这样的javaConnection conn dataSource.getConnection(); try { // 关闭自动提交开启事务 conn.setAutoCommit(false); PreparedStatement ps1 conn.prepareStatement(update account set balance balance - 100 where id ?); ps1.setLong(1, fromUserId); ps1.executeUpdate(); PreparedStatement ps2 conn.prepareStatement(update account set balance balance 100 where id ?); ps2.setLong(1, toUserId); ps2.executeUpdate(); // 全部成功提交 conn.commit(); } catch (Exception e) { // 出现异常回滚 conn.rollback(); throw e; } finally { // 恢复自动提交并归还连接 conn.setAutoCommit(true); conn.close(); }这种写法有几个明显的问题模板代码重复每个需要事务的业务方法都要写一遍开启、提交、回滚、关闭的逻辑。异常处理容易出错新手常常忘记在 finally 中关闭连接或者忘记恢复 autoCommit。业务代码被污染事务控制代码和业务逻辑纠缠在一起可读性差。扩展性差如果未来要切换到 JPA、MyBatis 或多种数据源事务控制代码就要全部重写。Spring 给出的解决方案是声明式事务管理开发者只需要通过注解或 XML 声明「这个方法是事务方法」Spring 通过 AOP 在方法执行前后织入事务控制逻辑。于是上面的代码就简化成了javaService public class TransferService { Transactional public void transfer(Long fromUserId, Long toUserId, BigDecimal amount) { accountMapper.decreaseBalance(fromUserId, amount); accountMapper.increaseBalance(toUserId, amount); } }业务代码瞬间干净了事务控制的职责全部交给了框架。但请记住一句话声明式事务只是把模板代码从「你写的业务代码里」挪到了「Spring 生成的代理类里」它不是魔法代理里的逻辑和我们手写的 try/catch/commit/rollback 在本质上是完全一样的。理解这一点后面很多原理问题就迎刃而解了。2.3 编程式事务仍然值得了解虽然声明式事务是主流但 Spring 也提供了编程式事务的能力主要通过 TransactionTemplate 或 PlatformTransactionManager 直接调用。例如javaService public class TransferService { private final TransactionTemplate transactionTemplate; public TransferService(PlatformTransactionManager transactionManager) { this.transactionTemplate new TransactionTemplate(transactionManager); } public void transfer(Long fromUserId, Long toUserId, BigDecimal amount) { transactionTemplate.execute(status - { try { accountMapper.decreaseBalance(fromUserId, amount); accountMapper.increaseBalance(toUserId, amount); return null; } catch (Exception e) { // 显式标记回滚 status.setRollbackOnly(); throw e; } }); } }编程式事务的优点是可以精确控制事务边界例如只希望一个方法中的某几行代码处于事务中或者需要在事务内做一些特殊处理。它的缺点是代码侵入性较强模板代码又回来了。因此在实际项目中绝大多数场景用声明式事务就够了只有在「事务粒度需要精确到代码行」「需要在循环中控制多个小事务」等特殊场景下才会考虑编程式事务。面试中如果能主动提到 TransactionTemplate也能体现你对事务控制粒度的理解。3. Transactional 注解属性逐一拆解Transactional 注解上有不少属性很多开发者在项目中只用到默认值但每个属性都可能影响事务的实际行为。我们先看注解定义javaTarget({ElementType.TYPE, ElementType.METHOD}) Retention(RetentionPolicy.RUNTIME) Inherited Documented public interface Transactional { AliasFor(transactionManager) String value() default ; AliasFor(value) String transactionManager() default ; String[] label() default {}; Propagation propagation() default Propagation.REQUIRED; Isolation isolation() default Isolation.DEFAULT; int timeout() default TransactionDefinition.TIMEOUT_DEFAULT; String timeoutString() default ; boolean readOnly() default false; Class? extends Throwable[] rollbackFor() default {}; String[] rollbackForClassName() default {}; Class? extends Throwable[] noRollbackFor() default {}; String[] noRollbackForClassName() default {}; }3.1 propagation事务传播行为传播行为决定了「当前方法被调用时如果已经存在一个事务这个方法是加入该事务还是新建事务还是以非事务方式运行」。默认值是 Propagation.REQUIRED。这是面试的重点我们会在第五节用一整节详细展开七种传播行为的语义和坑。3.2 isolation事务隔离级别隔离级别决定了事务之间的数据可见性和锁竞争程度。默认值是 Isolation.DEFAULT表示使用数据库默认的隔离级别。在 MySQL InnoDB 中默认隔离级别是 REPEATABLE_READ。第六节会详细说明四个级别的差异以及和 MySQL 的对应关系。3.3 timeout事务超时时间事务超时指的是事务从开始到提交/回滚所允许的最大时间单位是秒。默认值是 -1表示不超时实际使用的是底层事务系统的默认超时时间。如果事务执行超过设定时间Spring 会将其标记为 rollback-only在提交时触发回滚。这个属性在面试和实际排查中常常被忽略但它非常重要。一个常见的故障是某个事务方法内部调用了慢 SQL 或远程调用导致事务长时间不释放数据库连接被占满。合理设置 timeout 可以避免这种「长事务拖垮连接池」的问题。例如javaTransactional(timeout 5) public void processOrder(Long orderId) { // 该方法必须在 5 秒内完成事务否则会被标记回滚 }需要注意的是这里的超时是从事务真正开始算起的如果外层已经存在事务那么超时以更严格的那个为准同时超时是基于 deadline 机制判断的不是精确的定时中断它只会在事务提交前检查是否超时并触发回滚。3.4 readOnly只读事务readOnly true 表示该方法只进行读操作不进行写操作。这个属性主要起两个作用性能优化提示一些数据库如 MySQL会对只读事务进行优化减少锁和日志开销。防御性校验Spring 在一些场景下会检查只读事务中是否发生了写操作帮助尽早暴露误用。不过要强调一点readOnly 并不是数据库层面的强约束。在 MySQL 中只读事务仍然可以执行写操作除非数据库本身做了配置它更多是一个「约定」和「优化提示」。面试时如果被问到「只读事务真的不能写吗」一个严谨的回答是Spring 只负责把这个标志传递给底层连接最终是否限制写操作取决于数据库实现MySQL InnoDB 默认并不会因为 readOnlytrue 就拒绝写语句。3.5 rollbackFor / noRollbackFor控制回滚的异常类型这是最容易踩坑的属性之一。Spring 的事务默认回滚规则是遇到 RuntimeException 和 Error 时回滚遇到受检异常Checked Exception时不回滚。但很多业务代码习惯抛出 Exception 或其子类受检异常于是就会出现「明明抛异常了数据却提交了」的现象。如果希望所有异常都触发回滚可以这样配置javaTransactional(rollbackFor Exception.class) public void createOrder(Order order) throws Exception { // 即使抛出受检异常也会回滚 }反过来如果希望某些异常不触发回滚可以使用 noRollbackForjavaTransactional(noRollbackFor BusinessWarningException.class) public void saveRecord(Record record) { // 抛出 BusinessWarningException 时事务仍然正常提交 }第七节的坑会专门讲这个属性配置不当导致「事务不回滚」的经典案例。3.6 transactionManager / value指定事务管理器当项目中配置了多个数据源、多个事务管理器时例如主从分离、分库分表需要通过 transactionManager 属性明确指定当前方法使用哪个事务管理器。如果不指定Spring 会按类型查找当存在多个 PlatformTransactionManager Bean 时可能直接报错或者使用了错误的那个。javaTransactional(transactionManager orderTransactionManager) public void createOrder(Order order) { // 使用名为 orderTransactionManager 的事务管理器 }这里要注意Spring Boot 项目中常见的 DataSourceTransactionManager 和 JPA 的 JpaTransactionManager 都属于 PlatformTransactionManager如果同时引入且没有做限定容器注入时就可能报 NoUniqueBeanDefinitionException。4. 底层原理注解是如何变成事务行为的这一节是全篇文章最核心的部分。只有把 Transactional 的底层执行链路讲清楚才能回答「原理」类的问题也才能理解后面的各种「坑」。4.1 第一步EnableTransactionManagement 开启事务能力在 Spring Boot 项目中我们通常不需要显式写 EnableTransactionManagement因为 spring-boot-autoconfigure 中的 TransactionAutoConfiguration 已经替我们做了这件事。但理解它的作用是必要的。EnableTransactionManagement 的核心作用是向容器中注册事务相关的后置处理器和 Advisor。它的关键源码如下简化示意javaImport(TransactionManagementConfigurationSelector.class) public interface EnableTransactionManagement { AdviceMode mode() default AdviceMode.PROXY; ProxyTargetClass proxyTargetClass() default false; }其中 AdviceMode.PROXY 表示使用代理方式实现事务增强这也是最常见的模式。Spring 5 之后还提供了 AdviceMode.ASPECTJ 模式它可以在编译期织入事务逻辑从而解决「自调用事务失效」的问题不过生产中使用得较少这里先按下不表。这个注解最终会向容器注册两个关键 BeanBeanFactoryTransactionAttributeSourceAdvisor事务 Advisor用来告诉 AOP 框架「哪些类或方法需要被事务代理」。TransactionInterceptor事务拦截器也就是切面逻辑的载体方法执行时真正执行「开启事务、提交、回滚」的地方。同时容器中还需要存在一个 PlatformTransactionManager Bean它是事务管理器提供 getTransaction()、commit()、rollback() 三个核心方法。Spring Boot 会根据引入的依赖自动配置合适的实现例如引入 spring-boot-starter-jdbc 后自动配置 DataSourceTransactionManager引入 JPA 后则配置 JpaTransactionManager。4.2 第二步Bean 初始化时生成代理对象当带有 Transactional 的 Bean 被 Spring 容器创建时会经过 BeanPostProcessor 的处理。其中 InfrastructureAdvisorAutoProxyCreator 会检查当前 Bean 是否匹配 BeanFactoryTransactionAttributeSourceAdvisor 的切点如果 Bean 的类或方法上标注了 Transactional就认为匹配匹配后Spring 会为这个 Bean 创建一个代理对象最终注入到其他 Bean 中的是这个代理对象而不是原始对象。这里有两个重要细节。第一个细节代理方式的选择。Spring AOP 支持 JDK 动态代理和 CGLIB 动态代理两种方式。如果目标类实现了接口并且没有显式配置 proxyTargetClass true默认使用 JDK 动态代理代理对象实现目标类相同的接口如果目标类没有实现接口或者显式配置了 proxyTargetClass true则使用 CGLIB 动态代理通过继承目标类生成子类代理Spring Boot 2.x 中由于 AOP 自动配置默认将 proxy-target-class 设置为 true所以实际项目里最常见的其实是 CGLIB 代理。第二个细节JDK 动态代理与 CGLIB 的差异。这也是面试中容易追问的点。JDK 动态代理基于接口实现运行时通过 java.lang.reflect.Proxy 生成代理类要求目标类至少实现一个接口。它不依赖第三方库但只能代理接口方法目标类中未在接口中声明的方法无法被增强。CGLIB 动态代理基于继承实现运行时生成目标类的子类因此目标类不能是 final被增强的方法也不能是 final 或 private。CGLIB 的覆盖范围更广但生成代理对象的性能开销略高于 JDK 动态代理。理解了代理对象的生成后你就能回答一个重要问题为什么 Transactional 加在 private、static 或 final 方法上不生效private 方法无法被子类继承或通过接口暴露CGLIB 和 JDK 代理都增强不到static 方法属于类而不属于实例Spring AOP 基于实例代理天然无法拦截静态方法final 方法无法被 CGLIB 重写因此也无法被事务增强。这也是后面第七节「事务失效场景」的第一个经典案例。4.3 第三步TransactionInterceptor 的拦截链路当代理对象生成后外部对目标方法的调用实际上会先进入代理类再经过 TransactionInterceptor。它的核心逻辑在 TransactionInterceptor.invoke() 方法中简化后的执行流程如下javapublic Object invoke(MethodInvocation invocation) throws Throwable { Class? targetClass getTargetClass(invocation); // 1. 获取事务属性读取 Transactional 的配置 TransactionAttributeSource tas getTransactionAttributeSource(); TransactionAttribute txAttr tas.getTransactionAttribute(method, targetClass); // 2. 获取事务管理器 PlatformTransactionManager tm determineTransactionManager(txAttr); // 3. 开启事务返回 TransactionInfo TransactionInfo txInfo createTransactionIfNecessary(tm, txAttr, methodIdentification); Object retVal null; try { // 4. 执行真正的业务方法 retVal invocation.proceedWithInvocation(); } catch (Throwable ex) { // 5. 根据事务属性判断是否需要回滚 completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { // 6. 清理线程上的事务资源 cleanupTransactionInfo(txInfo); } // 7. 正常返回时提交事务 commitTransactionAfterReturning(txInfo); return retVal; }这段代码虽然简化过但已经展示了声明式事务最核心的骨架。我们逐一拆解其中几个关键方法。第一步getTransactionAttribute() 读取事务属性。Spring 会从方法、类、父类、接口等位置解析 Transactional 注解。如果方法和类上都标注了注解方法级别的配置优先于类级别配置方法更具体的属性会覆盖类上的默认值。第二步determineTransactionManager() 确定事务管理器。如果注解中显式指定了 transactionManager 或 value就按名称查找对应 Bean否则按类型查找容器中唯一的 PlatformTransactionManager。多个事务管理器存在时如果注解没有指定就会抛出无法确定使用哪一个的异常。第三步createTransactionIfNecessary() 开启事务。这里会进入 AbstractPlatformTransactionManager.getTransaction() 的核心逻辑根据传播行为决定是新建事务、加入已有事务还是以非事务方式执行。整个拦截链路中最重要的其实是第三步因为它包揽了传播行为的判断、连接获取、自动提交关闭、线程绑定等一系列操作。我们下面继续展开。4.4 第四步DataSourceTransactionManager 的完整工作过程在基于 JDBC 的项目中默认的事务管理器是 DataSourceTransactionManager。它的 doGetTransaction()、doBegin()、doCommit()、doRollback() 实现了事务生命周期管理的全部细节。当外层不存在事务、传播行为是 REQUIRED 时事务的开启过程大致如下javaprotected void doBegin(Object transaction, TransactionDefinition definition) { DataSourceTransactionObject txObject (DataSourceTransactionObject) transaction; Connection con null; try { if (!txObject.hasConnectionHolder()) { // 1. 从连接池获取一个数据库连接 con obtainDataSource().getConnection(); txObject.setConnectionHolder(new ConnectionHolder(con), true); } // 2. 如果当前连接处于自动提交模式则关闭自动提交 if (con.getAutoCommit()) { txObject.setMustRestoreAutoCommit(true); con.setAutoCommit(false); } // 3. 设置只读属性 if (definition.isReadOnly()) { con.setReadOnly(true); } // 4. 设置事务隔离级别 Integer previousIsolationLevel DataSourceUtils.prepareConnectionForTransaction(con, definition); txObject.setPreviousIsolationLevel(previousIsolationLevel); // 5. 将连接绑定到当前线程 if (txObject.isNewConnectionHolder()) { TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder()); } } catch (Throwable ex) { // 开启失败时释放连接 DataSourceUtils.releaseConnection(con, this.dataSource); throw new CannotCreateTransactionException(Could not open JDBC Connection for transaction, ex); } }这段源码揭示了几个非常重要的细节很多面试题都能从这里找到答案事务是与线程绑定的Spring 通过 TransactionSynchronizationManager 把数据源和连接绑定到当前线程因此在同一个事务方法内、同一个线程下通过同一个数据源执行的 SQL会共享同一条连接、处于同一个事务中。这就是为什么「在同一个线程里调用另一个事务方法能加入当前事务」的根本原因。事务开启的本质就是关闭自动提交数据库连接默认 autoCommittrue每条 SQL 执行后自动提交。Spring 在开启事务时调用 con.setAutoCommit(false)之后所有 SQL 都处于同一个未提交事务中直到最后统一提交或回滚。连接是从事务开始时占用的从事务开启到提交/回滚期间连接一直不会被归还给连接池。因此事务方法耗时越长占用连接的时间就越长这正是「长事务容易拖垮连接池」的底层原因。doCommit() 和 doRollback() 则完成最后的收尾工作javaprotected void doCommit(DefaultTransactionStatus status) { DataSourceTransactionObject txObject (DataSourceTransactionObject) status.getTransaction(); Connection con txObject.getConnectionHolder().getConnection(); try { con.commit(); } catch (SQLException ex) { throw translateException(JDBC commit, ex); } } protected void doRollback(DefaultTransactionStatus status) { DataSourceTransactionObject txObject (DataSourceTransactionObject) status.getTransaction(); Connection con txObject.getConnectionHolder().getConnection(); try { con.rollback(); } catch (SQLException ex) { throw translateException(JDBC rollback, ex); } }提交或回滚完成后Spring 还会在 doCleanupAfterCompletion() 中恢复连接的 autoCommit、隔离级别、只读属性等状态并解除线程绑定、把连接归还给连接池。这个清理动作发生在 finally 逻辑中因此即使业务方法抛出异常连接资源也不会泄漏。4.5 一次完整的方法调用时序总结把上面的步骤串起来一次带有 Transactional 的方法调用完整时序是外部 Bean 注入的是 Spring 生成的代理对象调用目标方法时请求进入代理对象代理对象把调用交给 TransactionInterceptorTransactionInterceptor 读取 Transactional 属性并确定事务管理器事务管理器根据传播行为判断是新建事务、加入已有事务还是以非事务方式执行新建事务时由 DataSourceTransactionManager 获取连接、关闭自动提交、设置隔离级别和只读属性并把连接绑定到当前线程业务方法通过数据源执行 SQL由于连接已关闭自动提交且与线程绑定这些 SQL 处于同一个事务中业务方法正常返回时事务管理器执行 commit()抛出异常时根据异常类型决定执行 rollback() 还是继续提交事务结束后恢复连接状态、解除线程绑定、归还连接。如果面试中被问到「事务是在哪一行代码开启的」一个准确且能拉开差距的回答是注解本身不开启事务真正开启事务的是事务拦截器中的 TransactionAspectSupport.createTransactionIfNecessary()对于 JDBC 项目来说最终落到 DataSourceTransactionManager.doBegin() 中获取连接并执行 setAutoCommit(false)。5. 七种事务传播行为逐一拆解传播行为是 Transactional 面试中的重中之重。所谓传播行为回答的是一个问题当前方法被调用时如果调用方已经存在事务这个方法是加入该事务、新建事务还是以非事务方式运行Spring 定义了七种传播行为它们位于 org.springframework.transaction.annotation.Propagation 枚举中。我们用一个「转账 记录日志」的经典场景来理解每一种行为transfer() 是外层事务方法saveLog() 是内层被调用方法。5.1 REQUIRED默认行为有事务就加入没有就新建REQUIRED 是最常用的传播行为也是默认值。它的语义是如果当前已经存在事务则加入当前事务如果当前没有事务则新建一个事务。示例javaTransactional(propagation Propagation.REQUIRED) public void transfer(Long fromUserId, Long toUserId, BigDecimal amount) { accountMapper.decreaseBalance(fromUserId, amount); // 内层加入当前事务 logService.saveLog(fromUserId, toUserId, amount); accountMapper.increaseBalance(toUserId, amount); } Transactional(propagation Propagation.REQUIRED) public void saveLog(Long fromUserId, Long toUserId, BigDecimal amount) { logMapper.insertLog(fromUserId, toUserId, amount); }在上面的代码中transfer() 和 saveLog() 处于同一个事务。如果 increaseBalance() 抛出异常整个事务回滚saveLog() 插入的日志也会被一并回滚。这就是 REQUIRED 最大的特点一荣俱荣一损俱损。5.2 SUPPORTS有事务就加入没有就以非事务方式运行SUPPORTS 的语义是如果当前存在事务则加入当前事务如果当前没有事务则以非事务方式运行不像 REQUIRED 那样新建事务。javaTransactional(propagation Propagation.SUPPORTS) public User getUser(Long userId) { return userMapper.selectById(userId); }这种传播行为通常用在只读查询方法上它表示「我本身不强制要求事务但如果你已经开了事务我也跟着你一起」。例如一个查询方法既可能被事务方法调用也可能被非事务方法调用。5.3 MANDATORY必须在事务中运行否则抛异常MANDATORY 的语义是如果当前存在事务则加入当前事务如果当前没有事务则直接抛出异常拒绝执行。javaTransactional(propagation Propagation.MANDATORY) public void reduceStock(Long skuId, Integer count) { stockMapper.reduce(skuId, count); }这个行为通常用于强依赖事务的业务方法例如扣库存、扣余额等必须被上层事务包裹的操作。如果某个开发者不小心在非事务环境下调用了它框架会立即抛出 IllegalTransactionStateException从而避免数据被错误提交。5.4 REQUIRES_NEW总是新建独立事务并挂起当前事务REQUIRES_NEW 是除了 REQUIRED 之外第二重要的传播行为也是面试官最爱追问的。它的语义是无论当前是否存在事务都新建一个独立事务如果当前已经存在事务则把当前事务挂起待内层事务结束后再恢复。javaTransactional public void transfer(Long fromUserId, Long toUserId, BigDecimal amount) { accountMapper.decreaseBalance(fromUserId, amount); // 内层使用独立事务挂起外层事务 logService.saveMandatoryLog(fromUserId, toUserId, amount); accountMapper.increaseBalance(toUserId, amount); } Transactional(propagation Propagation.REQUIRES_NEW) public void saveMandatoryLog(Long fromUserId, Long toUserId, BigDecimal amount) { logMapper.insertLog(fromUserId, toUserId, amount); }此时 saveMandatoryLog() 与外层 transfer() 处于两个互相独立的事务。即使外层事务最终回滚内层日志事务也已经提交日志记录不会丢失。这正是 REQUIRES_NEW 的典型用途日志记录、审计记录、消息补偿等场景业务主流程失败了也希望记录过程信息不被回滚。但它也有明显的代价新建独立事务通常意味着再获取一条新的数据库连接占用更多连接资源外层事务被挂起内层事务提交后外层事务再继续可能出现「内层成功、外层失败」的部分成功状态必须结合业务判断是否可接受在高并发下如果事务方法串联过多 REQUIRES_NEW连接资源会迅速被耗尽。面试中一个常被追问的点是REQUIRES_NEW 挂起事务后外层事务与内层事务能看到对方的数据吗答案取决于数据库隔离级别。内层事务和外层事务使用不同的连接因此在大多数隔离级别下内层事务看不到外层事务尚未提交的修改。这也是它和 NESTED 的核心区别之一。5.5 NESTED嵌套事务基于保存点实现NESTED 的语义是如果当前存在事务则在当前事务中创建一个嵌套事务保存点内层事务回滚时只回滚到保存点外层事务不受影响如果当前没有事务则新建一个事务此时行为等价于 REQUIRED。javaTransactional public void transfer(Long fromUserId, Long toUserId, BigDecimal amount) { accountMapper.decreaseBalance(fromUserId, amount); try { logService.saveNestedLog(fromUserId, toUserId, amount); } catch (Exception e) { // 内层回滚到保存点外层可以继续 } accountMapper.increaseBalance(toUserId, amount); } Transactional(propagation Propagation.NESTED) public void saveNestedLog(Long fromUserId, Long toUserId, BigDecimal amount) { logMapper.insertLog(fromUserId, toUserId, amount); }NESTED 与 REQUIRES_NEW 的核心区别事务数量REQUIRES_NEW 创建两个完全独立的事务NESTED 只有一个事务内层基于保存点回滚行为REQUIRES_NEW 内层回滚不影响外层外层回滚不影响内层NESTED 内层回滚后外层可以继续提交但外层回滚时内层也会被一起回滚连接数量REQUIRES_NEW 需要新连接NESTED 共用同一个连接实现机制REQUIRES_NEW 通过挂起外层事务实现NESTED 通过 JDBC 的 Savepoint 实现。NESTED 只在使用 DataSourceTransactionManager 或 JtaTransactionManager 时才支持使用 JpaTransactionManager 时可能不被支持。5.6 NEVER以非事务方式运行有事务就抛异常NEVER 的语义是如果当前存在事务则直接抛出异常如果当前没有事务则以非事务方式运行。javaTransactional(propagation Propagation.NEVER) public void sendNotification(String userId, String msg) { // 该方法绝不允许在事务中执行 }这种传播行为常用于那些明确不希望被事务包裹的操作比如发送消息、调用远程接口等。因为如果在事务中执行远程调用的耗时会导致数据库连接长时间被占用甚至可能导致远程端和数据库端数据不一致。5.7 NOT_SUPPORTED以非事务方式运行有事务就挂起NOT_SUPPORTED 的语义是如果当前存在事务则挂起当前事务以非事务方式执行当前方法。javaTransactional(propagation Propagation.NOT_SUPPORTED) public void doRemoteCall(String param) { // 该方法以非事务方式执行避免长事务 remoteService.call(param); }这种传播行为常用于把耗时的远程调用、文件操作等从事务中剥离出来避免连接被长时间占用。5.8 七种传播行为对比表传播行为当前有事务当前无事务使用场景REQUIRED加入当前事务新建事务默认绝大多数业务方法SUPPORTS加入当前事务非事务运行查询方法MANDATORY加入当前事务抛异常必须被事务包裹的方法REQUIRES_NEW挂起当前事务新建独立事务新建事务日志、审计、补偿等独立场景NESTED创建保存点嵌套事务新建事务需要部分回滚的场景NEVER抛异常非事务运行明确不允许事务的方法NOT_SUPPORTED挂起当前事务非事务运行非事务运行耗时操作从事务中剥离6. 四种隔离级别与 MySQL 下的表现Transactional 的 isolation 属性对应数据库的四种隔离级别默认值是 Isolation.DEFAULT即使用数据库的默认隔离级别。在 MySQL InnoDB 下默认是 REPEATABLE_READ。隔离级别脏读不可重复读幻读READ_UNCOMMITTED可能可能可能READ_COMMITTED不会可能可能REPEATABLE_READ不会不会InnoDB 下基本避免SERIALIZABLE不会不会不会需要特别说明的是Spring 的隔离级别只是在开启事务时设置连接上的隔离级别实际隔离行为由数据库实现决定。比如在 MySQL 中设置了 REPEATABLE_READInnoDB 会通过 MVCC 和 Next-Key Lock 实现如果换成 Oracle同样的隔离级别可能会有不同的行为。7. 十余个真实生产中的「事务失效」场景这是本文最实用的一节。下面每一条都是实际项目中踩过的坑建议逐条对照自己的代码。7.1 自调用导致事务失效javaService public class OrderService { public void createOrder(Order order) { // 这里调用的是 this.saveOrder不是代理对象 saveOrder(order); } Transactional public void saveOrder(Order order) { orderMapper.insert(order); } }原因事务是通过代理对象实现的外部调用走代理代理内部调用走原始对象导致 Transactional 失效。解决注入自己、用 AopContext.currentProxy()、把事务方法拆到另一个 Bean。7.2 private / final / static 方法上的 Transactional 失效原因CGLIB 无法重写 final/private 方法static 方法不属于实例都无法被代理增强。解决改为 public 实例方法。7.3 异常被 catch 掉导致不回滚javaTransactional public void createOrder(Order order) { try { orderMapper.insert(order); throw new RuntimeException(模拟异常); } catch (Exception e) { // 异常被吃掉Spring 认为方法正常返回不会回滚 log.error(异常, e); } }解决不要吞异常或者在 catch 中手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();。7.4 抛受检异常不回滚原因默认只对 RuntimeException 和 Error 回滚。解决配置Transactional(rollbackFor Exception.class)。7.5 事务方法内开新线程原因事务信息是绑定在 ThreadLocal 上的新线程无法共享。解决不要在事务方法内直接开新线程如果必须用编程式事务在新线程中显式管理。7.6 传播行为配置错误比如把内层方法配置为 REQUIRES_NEW但实际希望的是加入外层事务或者反之导致内层回滚时外层也被回滚。解决根据业务语义选择正确的传播行为。7.7 只读事务中发生写操作原因readOnly 只是提示不是强约束。解决确保只读方法不做写操作或者去掉 readOnly。7.8 多数据源下事务管理器未指定原因多个 PlatformTransactionManager Bean 存在Spring 无法确定用哪个。解决用Transactional(transactionManager xxx)明确指定。7.9 事务超时设置不合理原因事务中调用了慢 SQL 或远程调用导致连接长时间占用。解决设置 timeout、拆分事务、把远程调用从事务中剥离。7.10 数据库不支持事务比如使用 MyISAM 引擎本身不支持事务。解决改用 InnoDB。7.11 Spring 版本或配置问题比如没有开启EnableTransactionManagement或者 AOP 切点被覆盖。解决检查配置确认事务 Advisor 生效。7.12 事务方法被其他 AOP 覆盖比如自定义 AOP 切面在事务切面之前执行导致异常被拦截事务感知不到。解决调整切面顺序或者确保事务切面优先级最高。8. 美团一面常见追问与推荐回答追问 1Transactional 是加在类上好还是方法上好回答都可以。加在类上表示类中所有 public 方法都使用事务配置加在方法上则方法级配置优先。实践中通常加在需要事务的具体方法上粒度更细。追问 2Transactional 能保证原子性吗回答只要所有操作都在同一个事务内、都通过同一个数据源执行、异常类型触发回滚就能保证原子性。但如果跨数据源、跨线程或异常被吞掉原子性会被破坏。追问 3事务什么时候真正开启回答注解本身不开启事务真正开启的是 TransactionInterceptor 通过 TransactionAspectSupport.createTransactionIfNecessary() 触发最终由 DataSourceTransactionManager.doBegin() 获取连接并执行 setAutoCommit(false)。追问 4REQUIRES_NEW 和 NESTED 的区别回答REQUIRES_NEW 新建独立事务外层被挂起内层和外层互不影响NESTED 基于保存点在外层事务中创建嵌套事务内层回滚到保存点外层可以继续提交但外层回滚时内层也会被回滚。追问 5长事务有什么危害回答长事务会长时间占用数据库连接导致连接池耗尽会长时间持有锁影响并发会积累 undo log拖慢 purge会导致主从延迟。解决方法是拆小事务、把远程调用从事务中剥离、设置 timeout。追问 6事务和线程有什么关系回答Spring 事务与线程绑定通过 TransactionSynchronizationManager 把连接绑定到当前线程的 ThreadLocal。同一个线程内通过同一个数据源执行的操作共享同一个事务跨线程则无法共享。追问 7Transactional 能加在接口上吗回答可以但不推荐。Spring 官方建议加在具体类或方法上因为接口上的注解在 JDK 动态代理下可能不生效且接口注解会耦合事务语义。9. 总结回到最初的问题Transactional 的原理和常见的坑。一个完整的回答应该包含四层原理层Transactional 通过 AOP 代理实现调用链是「代理对象 → TransactionInterceptor → PlatformTransactionManager → Connection」真正开启事务的是 DataSourceTransactionManager.doBegin() 中的 setAutoCommit(false)。属性层propagation 决定事务传播行为isolation 决定隔离级别rollbackFor 决定回滚异常类型readOnly 是提示不是约束timeout 防止长事务。坑层自调用、private/final/static 方法、异常被吞、受检异常、跨线程、传播行为配置错误、多数据源、长事务等每一个都可能导致事务失效。实践层把 Transactional 加在 public 方法上配置 rollbackFor不要把远程调用放在事务里不要吞异常多数据源要显式指定事务管理器。
企业数字化 ERP 产品动态
相关推荐
阿里一面:MySQL 单表数据最大不要超过多少行?为什么? 一、先把结论说清楚这道题表面上在问一个数字,实际上问的是你对 InnoDB 存储引擎、B 树索引结构、磁盘 IO 和容量规划的整体理解。先说结论:MySQL 官方并没有规定单表最多不能超过多少行。 真正会被拿出来当“标准答案”的 2000 万行,并不是 … · 2026/11/3 8:30:33
面试问烂的 Spring MVC 执行过程 摘要: Spring MVC 几乎每年都会被问烂,但真正能把从 DispatchServlet 入口到视图渲染、异常处理、参数绑定、返回值解析这条链路讲清楚的人并不多。本文以一次 HTTP 请求为主线,结合 Spring Framework 源码,逐层拆解 DispatcherSe… · 2026/11/13 11:53:22
前端路由与History API(进阶篇)最佳实践与踩坑记录 本文深入探讨前端路由与History API(进阶篇),涵盖背景分析、原理剖析、实战步骤、配置示例、优化建议和避坑指南。在前端性能优化领域,前端路由与History API(进阶篇)是开发者和技术负责人持续关注的核心议… · 2026/11/3 4:44:00
青山电工证在哪里办怎么考过不白交培训费 青山电工证在哪里办怎么考过不白交培训费 怕考不过白交培训费?别急,先搞清楚青山电工证在哪里办,电子证书怎么查,再一步步来准备,稳稳拿到证才是关键。 为什么你得知道青山电工证在哪里办… · 2026/11/13 11:53:00
应急办电工证查询怎么查?电子证书怎么拿?怕考不过白交培训费的看过来 应急办电工证查询怎么查?电子证书怎么拿?怕考不过白交培训费的看过来 你是不是也这样?交完培训费,心里直打鼓,怕考不过白搭钱,更怕以后干活没证干不了?别急,今天我就从洛阳在职建筑工人的角度,说说怎么查应急办电工证查询、怎么拿电子证书,还有那些… · 2026/11/13 11:53:00
建筑电工证考难不网上查询报考条件与考试时间 建筑电工证考难不网上查询报考条件与考试时间 你是不是正在找工作,听说没有建筑电工证根本进不了工地?别急,今天就给你讲讲 建筑电工证考难不 ,还有怎么查报名条件、考试时间,全是干货,看完你就知道该怎么准备了。… · 2026/11/13 11:53:00
工地太忙根本没时间复习考试?芦山县低压电工证考前押题帮你搞定 工地太忙根本没时间复习考试?芦山县低压电工证考前押题帮你搞定 工地太忙根本没时间复习考试,低压电工证考试内容又多又杂,眼看考试日期临近,连翻书的时间都没有,这事儿谁没经历过?特别是芦山县的电工朋友们,面对频繁的项目变更和工期压力,复习真的成… · 2026/11/13 11:53:00
高压电工作业证书复审怎么搞?考前押题资料真能救命 高压电工作业证书复审怎么搞?考前押题资料真能救命 干电工这行,没证就等于没饭碗。特别是高压电工作业证书,一旦过期不复审,不仅不能上岗,连面试资格都保不住。很多人为了能顺利复审,到处找 考前押题… · 2026/11/13 11:53:00
聊城电工证报名在哪?实操考试心里没底怕挂科?考前押题帮你稳过 聊城电工证报名在哪?实操考试心里没底怕挂科?考前押题帮你稳过 实操考试心里没底怕挂科?考前押题是关键。聊城电工证报名在哪?很多人在报名前都会遇到这样的问题。特别是初次报考的小伙伴,面对考试流程、报名地点、材料准备等,一时间不知从何下手。别担… · 2026/11/13 11:52:00
电工证丢失还能补吗?千万别踩坑,丢了证等于失业 电工证丢失还能补吗?千万别踩坑,丢了证等于失业 你正在找工作,但手里的电工证突然丢了,这下怎么办?别急,先别慌, 电工证丢失还能补吗 ?答案是肯定的,但 千万别踩坑 ,补办流程如果不了解,可能会耽误你上岗、甚至影响到你的收入。… · 2026/11/12 14:09:01
电力电缆作业证直播报名入口与办理流程良心建议 电力电缆作业证直播报名入口与办理流程良心建议 你是不是也担心学历不高报不了名?别急,今天就给你讲讲 电力电缆作业证直播 的报名流程和那些你必须知道的干货, 良心建议 在这里,别错过。 电力电缆作业证直播报名需要什么条件?… · 2026/11/13 5:40:45
英德电工证报考费多少钱,怕考不过白交培训费?看完这篇别再踩坑 英德电工证报考费多少钱,怕考不过白交培训费?看完这篇别再踩坑 怕考不过白交培训费?英德电工证报考费多少钱,你真的了解吗?现在市面上培训机构五花八门,价格参差不齐,很多人一不小心就花了冤枉钱,结果还考不过。别急,今天就来给你说清楚英德电工证的… · 2026/11/12 23:27:43
维修电公用考电工证吗 在哪考最靠谱 维修电公用考电工证吗 在哪考最靠谱 工地太忙根本没时间复习考试,你是维修电工,天天在项目上跑,连抽个空背个题都难。那问题来了—— 维修电公用考电工证吗 ?答案是: 必须考 。尤其是在当前施工行业,没有电工证,连进场都可能被拦下。而且,… · 2026/11/13 0:00:08
电工证是不是可以登高?含金量和证书范围你必须知道的事 电工证是不是可以登高?含金量和证书范围你必须知道的事 证书过期了不知道怎么复审延期,这事儿很多人遇到过,尤其是像你这样在郑州金水区做劳务班组负责人,手里握着几十个工人的证书,稍微一疏忽,就可能影响工程进度和公司信誉。特别是电工证是不是可以登… · 2026/11/13 0:00:08
鸡西电工证哪家下证快官方报名入口怎么找 鸡西电工证哪家下证快官方报名入口怎么找 你是不是也担心 学历不高怕报不上名 ?别急,今天我就来跟你说说, 鸡西电工证哪家下证快 ,还有 官方报名入口 到底怎么找,别让那些“代办”“代考”耽误了你的机会。… · 2026/11/13 0:00:08
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/13 6:12:21
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/13 6:11:51
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/12 21:15:21