第五篇:Spring事务管理——@Transactional的底层实现与失效场景
前言
在前面的文章中,我们拆解了Spring AOP的底层原理——动态代理和切面编程。现在,我们来看AOP最经典的应用:事务管理。
你每天用着@Transactional,往Service方法上一加,事务就自动开启了。但面试中,事务是Spring最深的水区之一:
“@Transactional是怎么实现的?它和AOP是什么关系?”
“为什么同一个类中方法互相调用,事务会失效?”
“事务传播行为有哪些?REQUIRES_NEW和NESTED有什么区别?”
“什么情况下@Transactional会失效?至少说出三种场景。”
这些问题考察的不是"会不会加注解",而是"理不理解事务拦截的底层机制"。本文从事务拦截链出发,逐层拆解Spring事务管理的实现原理和经典失效场景。
本文核心问题:
@Transactional是怎么实现的?TransactionInterceptor做了什么?- 事务传播行为有哪些?REQUIRED、REQUIRES_NEW、NESTED分别适用什么场景?
- 为什么同一个类中方法互相调用,事务会失效?
@Transactional注解在哪些场景下会失效?至少说出四种失效场景及其根本原因。- 编程式事务和声明式事务各有什么优劣?为什么@Transactional是默认选择?
- Spring事务和数据库事务是什么关系?Spring在这里加了什么?
读完本文,你将对Spring事务管理拥有从实现原理到避坑指南的完整理解。
一、@Transactional是怎么实现的?——TransactionInterceptor
疑问:加上@Transactional注解,方法就能自动开启事务。Spring是怎么做到的?
回答:和AOP一样——Spring为标注了@Transactional的Bean创建代理对象,在代理对象中由TransactionInterceptor拦截方法调用,在方法前后管理事务。
1.1 事务拦截的本质
你写的Service: @Transactional public void createOrder(Order order) { orderMapper.insert(order); // ← 直接写数据库 } 实际运行时(AOP代理): TransactionInterceptor.invoke(proxy, method, args): transactionManager.begin() // 开启事务 try: method.invoke(target, args) // 调用你的createOrder方法 transactionManager.commit() // 提交事务 catch: transactionManager.rollback() // 回滚事务 throw关键是TransactionInterceptor——它在代理对象中拦截了@Transactional注解的方法,把事务管理逻辑(开启、提交、回滚)包裹在业务方法前后。业务方法内部不需要写任何事务代码,Spring通过AOP把事务逻辑从业务代码中完全剥离。
1.2 事务拦截的触发条件
Spring根据以下规则从容器管理的Bean中筛选出需要被事务拦截的类或方法:
- 类或方法上标注了
@Transactional - 如果是类级别标注,则类中所有public方法都受事务管理
- 如果是方法级别标注,只有标注的方法受事务管理,类中未标注的方法不受影响
1.3 事务的创建过程
TransactionInterceptor获取事务时,核心参数包括事务传播行为、隔离级别、超时时间、只读标志和回滚规则。这些参数从@Transactional注解中读取,传递给事务管理器。事务管理器根据传播行为判断是新建事务还是加入已有事务,然后设置隔离级别和超时控制,最终返回事务状态对象。整个创建过程在TransactionInterceptor内部完成,业务代码完全无感知。
二、事务传播行为——REQUIRED、REQUIRES_NEW、NESTED的区别
疑问:事务传播行为是什么?REQUIRED和REQUIRES_NEW有什么区别?
回答:传播行为定义了一个事务方法被另一个事务方法调用时,事务应该如何传播——是加入调用方的事务,还是新建一个独立的事务。
2.1 七种传播行为
| 传播行为 | 含义 | 调用方有事务 | 调用方无事务 |
|---|---|---|---|
| REQUIRED(默认) | 需要事务 | 加入已有事务 | 新建事务 |
| REQUIRES_NEW | 需要新事务 | 挂起原事务,新建独立事务 | 新建事务 |
| NESTED | 嵌套事务 | 创建保存点(Savepoint) | 新建事务 |
| SUPPORTS | 支持事务 | 加入已有事务 | 无事务执行 |
| NOT_SUPPORTED | 不支持事务 | 挂起原事务,无事务执行 | 无事务执行 |
| MANDATORY | 强制需要事务 | 加入已有事务 | 抛异常 |
| NEVER | 不允许事务 | 抛异常 | 无事务执行 |
2.2 REQUIRED vs REQUIRES_NEW
场景:订单创建成功时需要记录一条操作日志。创建订单失败时,日志不能因为事务回滚而丢失。 REQUIRED(错误方案): createOrder() { // 外层事务A orderMapper.insert(); // 写订单 logService.log(); // 写日志——使用REQUIRED,加入外层事务 } 如果log()抛异常 → 整个事务A回滚 → 订单插入和日志插入都回滚 → 错误 REQUIRES_NEW(正确方案): createOrder() { // 外层事务A orderMapper.insert(); // 写订单 logService.log(); // 写日志——使用REQUIRES_NEW,挂起事务A,新建事务B } 如果log()抛异常 → 事务B回滚,事务A不受影响 → 订单创建成功,日志丢失 如果createOrder()抛异常 → 事务A回滚,事务B已提交 → 订单回滚,日志保留REQUIRED和REQUIRES_NEW的本质区别:REQUIRED让所有操作共享同一个事务,同生共死;REQUIRES_NEW把内层操作从外层事务中隔离出来,独立提交或回滚。后者适合日志记录、通知发送等"无论主流程成功与否都应记录"的辅助操作。
2.3 REQUIRES_NEW vs NESTED
NESTED和REQUIRES_NEW都会开启新事务,但隔离方式不同:NESTED在已有事务中使用保存点(Savepoint),内层回滚时可以回滚到保存点而不影响外层事务;REQUIRES_NEW则是完全独立的新事务,直接挂起外层事务。NESTED依赖JDBC的Savepoint机制,仅支持JDBC且需要特定数据库支持(如MySQL的InnoDB引擎)。REQUIRES_NEW没有这个限制,任何支持事务的环境都能使用,但会占用额外的数据库连接。
三、@Transactional失效的四种经典场景
疑问:明明加了@Transactional,为什么事务没有生效?
回答:@Transactional依赖AOP代理来实现,所有绕过代理的调用都会导致事务失效。
3.1 失效场景一:自调用——同一个类中方法互相调用
@ServicepublicclassOrderService{@TransactionalpublicvoidcreateOrder(Orderorder){orderMapper.insert(order);// 有事务}publicvoidbatchCreate(List<Order>orders){for(Orderorder:orders){this.createOrder(order);// ← 事务失效!}}}为什么失效?
this.createOrder()调用的是原始对象的方法(this指针),完全绕过了Spring生成的代理对象。事务拦截器在代理对象中生效,而this引用指向的是原始对象——事务拦截器根本没有机会拦截这次调用。batchCreate本身没有事务,它通过this调用的createOrder也不会有事务。
解决方案:将createOrder方法移到另一个Service中,从容器中注入那个Service后调用;或者通过AopContext.currentProxy()获取当前类的代理对象再调用自己的方法。
3.2 失效场景二:非public方法
@ServicepublicclassOrderService{@TransactionalprivatevoidcreateOrderInternal(Orderorder){// ← private方法,事务失效!orderMapper.insert(order);}}为什么失效?
Spring AOP默认使用CGLIB动态代理——CGLIB通过继承目标类生成子类,在子类中重写父类方法并插入拦截逻辑。private方法无法被继承、无法被重写——CGLIB对它无能为力。同理,final方法也无法被重写,事务同样不会生效。
3.3 失效场景三:异常类型不匹配
@TransactionalpublicvoidcreateOrder(Orderorder){try{orderMapper.insert(order);}catch(Exceptione){log.error("创建订单失败",e);// 吞掉异常,不抛出}}Spring默认只在遇到RuntimeException和Error时回滚事务。CheckedException(如IOException、SQLException)默认不回滚,除非通过@Transactional(rollbackFor = Exception.class)显式指定。而这里异常被try-catch吞掉后,不仅默认的回滚条件没有触发,事务拦截器也无从得知异常发生——它看到方法正常返回了,直接提交了事务。
3.4 失效场景四:数据库引擎不支持事务
CREATETABLEtb_order(idBIGINTPRIMARYKEY,...)ENGINE=MyISAM;-- MyISAM不支持事务!MyISAM引擎不支持事务——@Transactional加了也没用,因为底层数据库根本不支持事务的ACID特性。Spring事务管理依赖数据库的事务能力,引擎不支持则Spring的一切事务逻辑都是空转。确保使用InnoDB引擎(MySQL 5.5+默认)。
3.5 失效场景速查表
| 失效场景 | 根本原因 | 解决方案 |
|---|---|---|
| 自调用 | this调用绕过了AOP代理 | 抽到另一个Service中,或通过AopContext.currentProxy()获取代理对象 |
| 非public方法 | CGLIB无法重写私有/受保护方法 | 改为public方法 |
| 异常被吞掉 | 事务拦截器没有感知到异常发生 | 抛出异常让拦截器捕获,或用TransactionAspectSupport手动回滚 |
| 异常类型不匹配 | 默认只对RuntimeException和Error触发回滚 | @Transactional(rollbackFor = Exception.class) |
| 数据库引擎不支持 | 底层根本不支持事务 | 使用InnoDB引擎 |
四、编程式事务 vs 声明式事务
疑问:既然@Transactional这么好用,为什么还需要编程式事务?
回答:@Transactional适合大多数场景——事务范围和方法边界一致。但当需要在方法内部手动控制事务边界时,编程式事务更灵活。
4.1 声明式事务(@Transactional)
@TransactionalpublicvoidprocessOrder(Orderorder){// 整个方法是一个事务,同生共死orderMapper.insert(order);inventoryService.deduct(order);paymentService.pay(order);}优点:简洁,事务和业务代码彻底分离。缺点:事务范围固定(整个方法),无法在方法内灵活调整。
4.2 编程式事务(TransactionTemplate)
publicvoidprocessOrder(Orderorder){// 第一步:创建订单(必须成功)transactionTemplate.execute(status->{orderMapper.insert(order);returnnull;});// 第二步:扣库存(独立事务,失败不影响订单创建)try{transactionTemplate.execute(status->{inventoryService.deduct(order);returnnull;});}catch(Exceptione){log.error("扣库存失败,但订单已创建",e);}// 第三步:发送通知(异步,不需要事务)notificationService.send(order);}优点:事务边界灵活,可在方法内精细控制。缺点:事务代码和业务代码混在一起,不如@Transactional简洁。
4.3 什么时候用编程式事务?
- 方法内需要多个不同传播行为的事务
- 事务提交或回滚后需要继续执行其他操作,而不是整个方法终止
- 涉及异步、消息队列等复杂场景,需要手动控制事务边界
绝大多数场景用@Transactional就够了。当它不够灵活时,需要知道有编程式事务这个更灵活的工具。
五、Spring事务和数据库事务的关系
疑问:Spring事务和数据库事务是什么关系?Spring在这里加了什么?
回答:Spring事务不是凭空产生的——它底层依赖数据库的事务能力。Spring做的不是在数据库之上再造一套事务机制,而是统一管理"何时开启、何时提交、何时回滚"这套协调逻辑。
@Transactional → Spring层面的声明式事务 ↓ Spring的事务管理器(PlatformTransactionManager) ↓ 数据库连接(Connection.setAutoCommit(false)) ↓ 数据库事务(InnoDB的ACID能力) Spring加了三层价值: 1. 统一抽象:不同数据库的事务API不同,Spring屏蔽了差异 2. 传播行为:控制事务在多个方法调用间的传递方式 3. 声明式注解:@Transactional让事务和业务代码彻底分离Spring事务本质是对数据库事务的封装和增强。底层还是用的数据库原生事务,但Spring提供了传播行为、声明式注解、统一抽象等额外的管理能力。没有Spring,你仍然可以通过Connection.setAutoCommit(false/true)手动控制事务——Spring让这个过程自动化、声明化。
六、面试中这样回答
面试官:“@Transactional是怎么实现的?”
回答框架:
“和AOP的原理一样——Spring为标注了@Transactional的Bean生成代理对象。在代理对象中,TransactionInterceptor拦截方法调用,在方法前后通过事务管理器开启和提交/回滚事务。业务方法内部不需要写任何事务代码,事务逻辑通过AOP从业务代码中剥离。这也解释了为什么自调用会失效——this调用绕过了代理对象,事务拦截器无法生效。”
面试官:“@Transactional在什么情况下会失效?至少说三种。”
回答:
“第一,自调用——同一个类中方法互调,this调用绕过了AOP代理。第二,非public方法——CGLIB无法重写私有和受保护方法。第三,异常被try-catch内部吞掉——事务拦截器完全感知不到异常,直接提交。第四,异常类型不匹配——@Transactional默认只对RuntimeException和Error回滚,CheckedException需要显式指定rollbackFor。第五,数据库引擎不支持事务——比如MyISAM,Spring事务管理无法在缺乏事务能力的引擎上生效。”
总结
- @Transactional通过TransactionInterceptor+AOP代理实现——事务管理逻辑在代理对象中包裹业务方法,业务代码与事务逻辑彻底分离
- 传播行为定义事务在方法调用间的传递规则——REQUIRED同生共死,REQUIRES_NEW独立提交。日志记录等辅助操作需要REQUIRES_NEW隔离失败影响
- 自调用是最容易踩的坑——this调用绕过代理,事务拦截器完全不知道你调用了它标注的方法。解决方式:抽到另一个Service中,或通过AopContext.currentProxy()获取代理对象
- 异常被吞掉导致事务不回滚——Spring只在抛出异常时感知到失败。try-catch内部消化异常后,拦截器看到方法正常返回,直接提交事务
- 声明式事务适合大多数场景;需要方法内精细控制事务边界时用编程式事务。两者都是对数据库事务的封装和增强,底层依赖数据库的事务能力
- 完整的事务失效清单:自调用绕过代理、非public方法、异常被吞掉、异常类型不匹配、MyISAM引擎——每次排查事务失效时,按这个清单逐个排查
下一篇预告:Spring原理(六)——Spring用到了哪些设计模式?从单例、工厂到代理、模板方法,拆解Spring框架源码中的经典设计模式应用,串联前五篇的IoC、AOP、MVC、自动配置、事务管理知识。
