Spring 声明式事务 Transactional 完整详解面试开发必备前言Spring 声明式事务是日常开发中最常用、最核心、最容易踩坑的技术点尤其在秒杀、订单、转账等高并发 数据一致性业务中至关重要。本文基于官方标准原理覆盖核心原理、代理机制、事务失效场景、AOP 关联、本类调用坑、锁与事务顺序帮你彻底吃透可作为长期查阅手册。一、什么是 Spring 声明式事务1. 定义Spring 声明式事务是Spring AOP 基于动态代理实现的事务管理机制。开发者只需要添加Transactional注解即可自动实现开启事务提交事务异常回滚事务事务传播、隔离级别控制无需手动编写connection.setAutoCommit(false)等原生代码实现业务与事务解耦。2. 核心优点无侵入业务代码与事务代码完全分离不改动核心业务逻辑简洁易用一个注解即可完成事务配置降低开发成本统一管理事务的开启、提交、回滚由 Spring 统一控制便于维护和扩展二、Spring 声明式事务 底层实现原理最重要1. 核心基于 AOP 动态代理Spring 事务不是魔法它的本质是AOP 切面 事务增强逻辑动态代理 承载事务功能的对象执行流程核心必记Spring 启动时扫描所有带有Transactional注解的类/方法为带有注解的目标类生成动态代理对象Proxy代理对象在目标方法执行前后自动织入事务逻辑开启、提交、回滚直观流程示意// 代理对象调用方法的完整流程代理对象.method(){1.开启事务connection.setAutoCommit(false);2.try{目标对象.method();// 执行开发者编写的业务逻辑3.提交事务connection.commit();}catch(Exceptione){4.回滚事务connection.rollback();}}关键补充贴合核心坑点很多开发者会踩一个核心误区在本类中直接调用带有Transactional注解的方法误以为事务会生效实则不然。Transactional注解的本质是 Spring AOP 定义的一个事务切面标识其事务增强逻辑开启、提交、回滚需要通过 Spring 生成的代理对象调用才能触发若在本类中直接通过this调用注解方法不会经过代理对象也就不会触发切面的事务增强此时事务注解相当于无效。需要特别说明的是原句“事务注解的本质是一个切面类”表述不够严谨准确来说Transactional是一个切面标识注解Spring 会通过该注解识别需要织入事务切面的方法真正的事务切面逻辑由 Spring 内部的TransactionInterceptor事务拦截器实现而非注解本身是切面类。但原句核心逻辑“代理对象调用才生效本类直接调用不经过切面”完全正确是日常开发中最常见的事务失效场景。2. 两种代理策略Spring 自动选择JDK 动态代理当目标类实现了接口时Spring 会使用 JDK 动态代理生成代理对象基于接口代理代理对象实现目标类的所有接口并重写接口中的方法织入事务逻辑。CGLIB 代理当目标类没有实现接口时Spring 会使用 CGLIB 代理基于子类继承生成目标类的子类作为代理对象重写目标类的方法织入事务逻辑。补充Spring Boot 2.x 版本后默认开启 CGLIB 代理即使目标类实现了接口也会优先使用 CGLIB 代理可通过配置修改。3. 最重要结论刻在脑子里Spring 声明式事务只有通过【代理对象】调用才生效直接调用目标对象this 关键字事务完全失效这是最经典、最容易踩的坑三、Transactional 执行流程标准流程1.客户端调用目标方法 → 实际进入代理对象2.代理对象开启事务设置事务属性、获取数据库连接、关闭自动提交3.代理对象调用目标对象的真实方法执行业务逻辑如扣库存、创建订单4.若业务逻辑无异常 → 代理对象提交事务释放连接5.若业务逻辑抛出异常 → 代理对象回滚事务释放连接6.事务执行完毕返回结果给客户端四、Spring 事务生效的 4 个必要条件缺一不可只有同时满足以下 4 个条件Transactional 注解才会生效否则事务失效方法必须是 public 修饰Spring 事务代理只对 public 方法进行增强private、protected、default 修饰的方法事务不生效。原因是 Spring AOP 底层依赖 JDK 动态代理或 CGLIB 代理private 方法无法被重写无法织入事务逻辑。目标类必须被 Spring 管理目标类必须添加 Service、Component 等注解被 Spring 容器扫描并实例化否则无法生成代理对象事务也无法生效。若手动 new 一个目标类对象调用其注解方法事务必然失效。必须通过代理对象调用如前文所述Transactional基于 AOP 切面实现只有通过 Spring 生成的代理对象调用注解方法才能触发切面的事务增强本类内部this直接调用不会经过代理事务失效。异常需被 Spring 捕获默认情况下Spring 只对RuntimeException和Error类型的异常进行事务回滚若异常被手动 catch 且未重新抛出事务不会回滚若需对所有异常回滚需配置rollbackFor Exception.class。五、最经典坑本类内部调用this调用事务失效1. 失效代码示例日常开发高频错误ServicepublicclassOrderService{// 无事务注解的方法publicvoidcreateOrder(LongvoucherId){// this 是当前 OrderService 的真实对象不是代理对象this.doCreateOrder(voucherId);// 本类直接调用事务失效}// 有事务注解的方法TransactionalpublicvoiddoCreateOrder(LongvoucherId){// 扣库存seckillService.update().setSql(stock stock - 1).eq(voucher_id,voucherId).update();// 创建订单VoucherOrderordernewVoucherOrder();order.setVoucherId(voucherId);orderService.save(order);}}结果事务失效若扣库存成功但创建订单失败库存不会回滚导致数据不一致。失效原因createOrder()内部调用doCreateOrder()时使用的是this关键字this指向的是 OrderService 的真实对象而非 Spring 生成的代理对象。真实对象没有被 AOP 织入事务增强逻辑调用其注解方法时不会开启、提交或回滚事务事务注解完全无效。2. 解决方案获取代理对象调用秒杀业务常用步骤 1开启代理对象暴露在 Spring 配置类或 Service 类上添加EnableAspectJAutoProxy(exposeProxy true)作用是将 Spring 生成的代理对象暴露到 AopContext 中便于手动获取。EnableAspectJAutoProxy(exposeProxytrue)// 开启代理暴露ServicepublicclassOrderService{// 业务方法...}步骤 2手动获取代理对象调用事务方法ServicepublicclassOrderService{publicvoidcreateOrder(LongvoucherId){// 获取当前类的 Spring 代理对象OrderServiceproxy(OrderService)AopContext.currentProxy();// 用代理对象调用事务方法 → 事务生效proxy.doCreateOrder(voucherId);}TransactionalpublicvoiddoCreateOrder(LongvoucherId){// 扣库存、创建订单事务生效seckillService.update().setSql(stock stock - 1).eq(voucher_id,voucherId).update();VoucherOrderordernewVoucherOrder();order.setVoucherId(voucherId);orderService.save(order);}}补充说明若不开启exposeProxy true调用AopContext.currentProxy()会抛出IllegalStateException异常提示“Cannot find current proxy: Set ‘exposeProxy’ property on Advised to ‘true’ to make it available.”。六、AOP 与事务到底是什么关系一句话总结AOP 是机制事务是 AOP 的一种具体增强功能核心关系图必记关键关联点没有 AOP就没有 Spring 声明式事务Spring 事务的实现完全依赖 AOP 的动态代理和切面织入能力若禁用 AOPTransactional 注解会完全失效。代理对象是 AOP 与事务的桥梁代理对象承载了 AOP 织入的事务逻辑是事务生效的核心载体。Transactional 是 AOP 切面的“标识”Spring 通过该注解识别需要织入事务切面的方法无需开发者手动编写切面逻辑。七、Transactional 常用配置开发必备Transactional 注解支持多种配置参数可根据业务需求灵活调整核心配置如下Transactional(rollbackForException.class,// 所有异常都回滚推荐配置propagationPropagation.REQUIRED,// 事务传播级别默认isolationIsolation.READ_COMMITTED,// 事务隔离级别默认timeout3,// 事务超时时间单位秒超过时间自动回滚readOnlyfalse// 是否为只读事务查询操作可设为true提升性能)1. 事务传播机制常用面试高频事务传播机制定义了“当一个事务方法调用另一个事务方法时事务如何传递”核心常用的 4 种Propagation.REQUIRED默认如果当前存在事务就加入当前事务如果当前没有事务就新建一个事务。最常用如订单创建时扣库存和创建订单共用一个事务Propagation.REQUIRES_NEW无论当前是否存在事务都新建一个独立的事务原事务暂停新事务执行完毕后原事务继续执行。如日志记录无论订单事务是否成功日志都必须保存Propagation.NESTED嵌套事务在当前事务内部新建一个子事务子事务回滚不影响父事务但父事务回滚会带动子事务回滚。如订单创建时先扣库存再创建订单库存扣减失败则订单不创建订单创建失败可回滚库存Propagation.SUPPORTS支持当前事务如果当前存在事务就加入事务如果当前没有事务就以非事务方式执行。很少用适合查询操作2. 事务隔离级别解决并发问题事务隔离级别用于解决并发场景下的脏读、不可重复读、幻读问题MySQL 默认隔离级别是 READ_COMMITTEDSpring 事务默认也是该级别READ_UNCOMMITTED最低隔离级别允许读取未提交的数据会出现脏读、不可重复读、幻读。不推荐使用READ_COMMITTED开发常用允许读取已提交的数据可避免脏读会出现不可重复读、幻读。MySQL 默认Spring 默认REPEATABLE_READ可重复读保证同一事务内多次读取同一数据结果一致可避免脏读、不可重复读会出现幻读。InnoDB 引擎通过 MVCC 机制避免幻读SERIALIZABLE最高隔离级别串行执行所有事务可避免所有并发问题但性能极差适合并发量极低的场景。不推荐使用3. 其他常用配置rollbackFor指定需要回滚的异常类型默认只回滚 RuntimeException 和 Error推荐配置rollbackFor Exception.class确保所有异常都能回滚。timeout事务超时时间超过指定时间单位秒Spring 会自动回滚事务避免事务长时间占用数据库连接。readOnly是否为只读事务查询操作可设为 trueSpring 会优化事务性能避免不必要的事务操作增删改操作必须设为 false默认。八、事务失效 10 大场景面试必考结合前文内容总结日常开发中最常见的 10 种事务失效场景帮你快速避坑非 public 方法private、protected、default 修饰的方法事务不生效Spring 只增强 public 方法。final/private 方法final 方法无法被代理对象重写AOP 无法织入事务逻辑事务失效private 方法同理。本类内部调用this 调用最经典场景this 是真实对象不经过代理事务失效。异常被 catch 吃掉异常被手动 catch 且未重新抛出Spring 无法捕获异常无法触发回滚事务失效。数据库不支持事务如 MySQL 的 MyISAM 引擎不支持事务即使添加 Transactional 注解也不会有事务效果推荐使用 InnoDB 引擎。目标类未被 Spring 管理目标类未添加 Service、Component 等注解未被 Spring 扫描为 Bean无法生成代理对象事务失效。事务传播级别配置错误如配置为 Propagation.NOT_SUPPORTED不支持事务即使添加注解也会以非事务方式执行。多线程调用一个事务方法调用另一个线程的事务方法两个方法不在同一个事务中事务无法共享会出现事务失效如异步方法调用。类未被代理未开启 AOP 代理未加 EnableAspectJAutoProxy或目标类是 final 类无法被 CGLIB 代理无法生成代理对象事务失效。异常类型不匹配默认只回滚 RuntimeException 和 Error若抛出 checked 异常如 IOException未配置 rollbackFor事务不会回滚。九、高并发关键锁与事务的顺序秒杀必看在秒杀、订单等高并发场景中锁与事务的顺序直接影响数据一致性若顺序错误会导致一人多单、超卖等问题。1. 错误顺序高频错误会导致并发安全问题TransactionalpublicResultcreateOrder(LongvoucherId){LonguserIdUserHolder.getUser().getId();// 错误先开启事务再加锁synchronized(userId.toString().intern()){// 一人一单判断intcountquery().eq(user_id,userId).eq(voucher_id,voucherId).count();if(count0){returnResult.fail(您已经购买过一次了);}// 扣库存booleansuccessseckillService.update().setSql(stock stock - 1).eq(voucher_id,voucherId).gt(stock,0).update();if(!success){returnResult.fail(库存不足);}// 创建订单VoucherOrderordernewVoucherOrder();order.setVoucherId(voucherId);order.setUserId(userId);orderService.save(order);returnResult.ok(order.getId());}}错误原因先开启事务再加锁执行完业务逻辑后锁会先释放而事务可能还未提交事务提交需要时间。此时其他线程获取锁后查询到的是未提交的事务数据脏读会导致一人多单、超卖。2. 正确顺序必须记住秒杀安全核心publicResultcreateOrder(LongvoucherId){LonguserIdUserHolder.getUser().getId();// 正确先加锁再开启事务通过代理调用事务方法synchronized(userId.toString().intern()){// 获取代理对象调用事务方法OrderServiceproxy(OrderService)AopContext.currentProxy();returnproxy.doCreateOrder(voucherId);}}// 事务方法单独抽取由代理对象调用TransactionalpublicResultdoCreateOrder(LongvoucherId){LonguserIdUserHolder.getUser().getId();// 一人一单判断intcountquery().eq(user_id,userId).eq(voucher_id,voucherId).count();if(count0){returnResult.fail(您已经购买过一次了);}// 扣库存乐观锁防止超卖booleansuccessseckillService.update().setSql(stock stock - 1).eq(voucher_id,voucherId).gt(stock,0).update();if(!success){returnResult.fail(库存不足);}// 创建订单VoucherOrderordernewVoucherOrder();order.setVoucherId(voucherId);order.setUserId(userId);orderService.save(order);returnResult.ok(order.getId());}正确逻辑顺序刻在脑子里核心优势锁包住整个事务保证事务提交完成后再释放锁避免其他线程读取未提交的脏数据确保高并发场景下的数据一致性一人一单、不超卖。十、终极总结最强记忆版核心原理Spring 声明式事务 AOP 动态代理Transactional 是 AOP 切面标识代理对象织入事务逻辑。生效关键只有通过代理对象调用事务才生效this 调用真实对象事务失效。本类调用解决方案开启 EnableAspectJAutoProxy(exposeProxy true)通过 AopContext.currentProxy() 获取代理对象。生效三要素public 方法 Spring 管理的 Bean 代理对象调用。高并发顺序锁 → 代理调用 → 事务 → 业务 → 提交事务 → 释放锁。避坑重点避免 10 大事务失效场景尤其注意本类调用、异常被 catch、非 public 方法。十一、Spring 事务面试题 20 道含标准答案基础题必背问题Spring 声明式事务的核心原理是什么答案基于 AOP 动态代理实现通过 Transactional 注解标识需要织入事务逻辑的方法Spring 生成代理对象在目标方法前后自动织入开启、提交、回滚事务的逻辑。问题Transactional 注解作用在类上和方法上有什么区别答案作用在类上该类中所有 public 方法都会生效作用在方法上只有该方法生效方法上的配置会覆盖类上的配置。问题Spring 事务的两种代理方式是什么区别是什么答案JDK 动态代理基于接口代理对象实现接口和 CGLIB 代理基于子类生成目标类的子类区别JDK 代理要求目标类实现接口CGLIB 代理无要求但目标类不能是 final 类。问题Spring 事务生效的必要条件有哪些答案4 个方法是 public 修饰、目标类被 Spring 管理、通过代理对象调用、异常被 Spring 捕获。问题Transactional 注解默认回滚哪些异常答案默认只回滚 RuntimeException运行时异常和 Error错误checked 异常如 IOException不会回滚。进阶题面试高频问题本类内部调用事务方法为什么事务会失效如何解决答案原因本类调用使用 this 关键字指向真实对象不经过代理对象AOP 无法织入事务逻辑解决方案开启 EnableAspectJAutoProxy(exposeProxy true)通过 AopContext.currentProxy() 获取代理对象用代理对象调用事务方法。问题事务传播机制中REQUIRED 和 REQUIRES_NEW 的区别是什么答案REQUIRED有事务则加入无则新建共用一个事务REQUIRES_NEW无论是否有事务都新建独立事务原事务暂停新事务执行完毕后原事务继续。问题为什么 final 方法的事务会失效答案Spring 事务依赖代理对象重写目标方法织入事务逻辑final 方法无法被重写代理对象无法织入事务逻辑导致事务失效。问题异常被 catch 后事务为什么不回滚答案Spring 事务回滚的前提是捕获到异常若异常被手动 catch 且未重新抛出Spring 无法捕获异常无法触发回滚机制。问题readOnly true 有什么作用什么时候使用答案作用标记事务为只读事务Spring 会优化事务性能避免不必要的事务操作如禁止增删改使用场景纯查询操作无增删改逻辑。高阶题秒杀/高并发场景问题高并发场景下锁与事务的顺序为什么不能颠倒答案若先开启事务再加锁锁会先释放事务可能未提交其他线程获取锁后会读取未提交的脏数据导致一人多单、超卖正确顺序是先加锁再开启事务确保事务提交后再释放锁。问题分布式场景下Spring 本地事务为什么会失效如何解决答案失效原因分布式场景下多个服务不在同一个数据库本地事务无法跨服务生效解决方案使用分布式事务如 Seata、TCC。问题如何避免高并发场景下的事务超时答案1. 合理设置 timeout 参数避免事务长时间占用连接2. 优化业务逻辑减少事务执行时间3. 拆分大事务为小事务降低执行耗时。问题Spring 事务隔离级别中READ_COMMITTED 能解决什么问题不能解决什么问题答案能解决脏读不能解决不可重复读和幻读MySQL InnoDB 引擎通过 MVCC 机制可避免幻读。问题多线程调用事务方法事务为什么会失效答案多线程调用时每个线程有独立的事务上下文两个线程的事务无法共享一个线程的事务提交/回滚不会影响另一个线程导致事务失效。拓展题深度面试问题Spring 事务的传播机制中NESTED 和 REQUIRES_NEW 的区别是什么答案NESTED 是嵌套事务子事务依赖父事务父事务回滚子事务也回滚子事务回滚不影响父事务REQUIRES_NEW 是独立事务与父事务无关父事务回滚不影响新事务。问题如何手动控制 Spring 事务编程式事务答案通过 TransactionTemplate 或 PlatformTransactionManager 手动控制如使用 TransactionTemplate 的 execute 方法在回调中执行业务逻辑无需注解。问题Spring Boot 中Transactional 注解为什么不需要手动开启 AOP 代理答案Spring Boot 自动配置中默认开启 AOP 代理AutoConfiguration 中已集成 EnableAspectJAutoProxy 相关配置无需手动开启。问题MyISAM 引擎为什么不支持事务答案MyISAM 引擎是面向查询的引擎不支持事务、行锁设计初衷是追求查询性能不具备事务的 ACID 特性InnoDB 引擎支持事务和行锁适合业务场景。问题如何排查 Spring 事务失效问题答案1. 检查方法是否为 public2. 检查目标类是否被 Spring 管理3. 检查是否通过代理对象调用4. 检查异常是否被 catch 或类型是否匹配5. 检查数据库引擎是否支持事务6. 检查事务配置传播级别、rollbackFor 等是否正确。适合人群Java 开发工程师日常开发查阅面试备战者重点掌握原理、失效场景、面试题秒杀/订单/支付业务开发者重点掌握锁与事务顺序想彻底弄懂 Spring 事务原理的开发者本文永久保存遇到事务问题随时查阅