Spring @Transactional 注解全解析:用法、失效场景与大厂最优实践
在Spring开发中,@Transactional注解是声明式事务的核心,也是开发者日常使用频率极高的注解。但很多人在使用时,常会遇到"注解加了,事务却不生效"的坑,甚至不清楚什么时候该用、什么时候不该用,以及如何写才符合大厂规范。
本文将从「用法、失效场景、核心原理、大厂最优写法」四个维度,彻底讲透@Transactional注解,帮你避开所有常见坑,写出规范、高效、无bug的事务代码。
一、@Transactional 核心用法(基础必懂)
1. 基本使用
@Transactional注解可用于类或public方法上,用于声明该类/方法需要被事务管理,Spring会自动完成"开启事务→执行方法→提交事务/回滚事务"的流程。
最常用标准写法(必记):
java
@Service public class BizService { // 标准写法:指定rollbackFor,避免异常不回滚问题 @Transactional(rollbackFor = Exception.class) public void doBiz() { // 多条增删改操作(原子性要求) insertData(); updateData(); deleteData(); } }
2. 关键属性(常用3个)
-
rollbackFor :指定需要回滚的异常类型,最常用
rollbackFor = Exception.class。默认只回滚RuntimeException和Error, checked异常(如IOException)不会回滚,必须显式指定。 -
propagation:事务传播机制,默认REQUIRED(有事务则加入,无则新建),日常开发无需修改,特殊场景(如嵌套事务)可调整。
-
readOnly:设为true表示只读事务,仅用于查询场景,可优化数据库性能(避免开启写事务)。
3. 什么时候该用?什么时候不用?
核心原则:有多条写操作(增/删/改),需要原子性(要么全成功、要么全回滚),就必须加;单条写操作、纯查询,坚决不加。
| 场景 | 是否加@Transactional | 说明 |
|---|---|---|
| 多条增删改(如订单创建+扣库存) | 是 | 保证原子性,避免部分成功、部分失败 |
| 单条增/删/改 | 否 | 数据库单条SQL本身就是原子性,加注解多余且耗性能 |
| 纯查询(查列表、查详情) | 否 | 查询无需提交/回滚,加注解会开启无用事务 |
| 复杂多表联查(大数据量) | 可选 | 可加@Transactional(readOnly = true)优化性能 |
| 无DB操作(仅参数校验、调用第三方接口) | 否 | 无事务需要管理,加注解无意义 |
二、@Transactional 失效场景(高频坑,必避)
事务失效的核心本质只有一个:没有通过Spring生成的代理对象调用带事务的方法,或代理机制被破坏。以下是100%会遇到的失效场景,按出现频率排序。
1. 同类内部方法调用(自调用,最常见)
这是最容易踩的坑!同一个类中,无事务方法调用有事务方法,事务必失效。
一个方法加了事务注解,那么spring会对这个类生成一个对应的代理对象,放入spring容器。
如果同类自己调用自己,使用的this对象。不是用的代理对象。this对象调用其他方式不会对其他方法增强的。只有代理对象才会对要调用的方法增强。所以事务会失效。
只有代理对象里面才有方法被事务加持。
@Service public class UserService { // 无事务方法 public void methodA() { // 自调用:this.methodB(),不走代理,事务失效 methodB(); } // 有事务方法 @Transactional(rollbackFor = Exception.class) public void methodB() { // 数据库操作 } }
原因:methodA直接调用methodB,是原生对象(this)调用,绕过了Spring代理,AOP无法拦截事务逻辑。
注意:哪怕两个方法都加@Transactional,自调用依然失效!
2. 方法修饰符错误(private/final/static)
// 失效:private方法,动态代理无法重写 @Transactional private void save() {} // 失效:final方法,动态代理无法重写 @Transactional public final void save() {} // 失效:static方法,不属于对象实例,代理无法拦截 @Transactional public static void save() {}
规则:@Transactional只能用在public、非final、非static方法上。
3. 异常被try-catch"吃掉"
@Transactional(rollbackFor = Exception.class) public void save() { try { // 数据库操作 insertData(); } catch (Exception e) { // 异常被捕获,未抛出,Spring无法感知异常,不会回滚 e.printStackTrace(); } }
原因:Spring事务默认只有"未捕获的异常"才会触发回滚,捕获异常后不抛出,事务会正常提交。
4. 类未被Spring管理(无@Component/@Service等)
// 失效:未加@Service,不是Spring Bean,AOP无法代理 public class UserService { @Transactional public void save() {} }
原因:只有Spring管理的Bean,才会被AOP代理,事务注解才会生效。
5. 事务传播机制配置错误
若传播机制设为NOT_SUPPORTED或NEVER,事务会直接不开启:
// 失效:非事务运行,挂起当前事务 @Transactional(propagation = Propagation.NOT_SUPPORTED) public void save() {}
6. 数据库不支持事务
MySQL的MyISAM引擎不支持事务,即使加了@Transactional,也不会回滚;需改用InnoDB引擎。
7. 其他冷门失效场景
-
@Async + @Transactional:异步线程与主线程不属于同一个事务,事务无法传递。
-
多数据源未配置事务管理器:Spring Boot单数据源自动配置,多数据源需手动配置PlatformTransactionManager。
-
切面顺序问题:自定义切面在事务切面之前执行,且捕获了异常,事务切面无法感知异常,不会回滚。
三、核心原理(懂原理,不踩坑)
@Transactional基于Spring AOP动态代理实现,核心流程如下:
-
Spring启动时,扫描到带有@Transactional的类/方法,会为该类生成CGLIB代理对象(若类实现接口,用JDK动态代理)。
-
代理对象会拦截带@Transactional的方法,在方法执行前开启事务,执行后提交事务,异常时回滚事务。
-
只有通过"代理对象.方法()"调用,才会触发拦截;通过"原生对象(this).方法()"调用,会绕过拦截,事务失效。
关键补充:只要类中任意一个public方法加了@Transactional,整个类都会被生成代理对象,放入Spring容器。哪怕其他方法没加注解,注入的依然是代理对象(只是无事务逻辑)。
四、大厂最优写法(无循环依赖、无失效、代码规范)
结合前面的失效场景和原理,大厂最推荐的写法,核心是「避免自调用、简化注解、清晰职责」,分两种场景说明。
场景1:同一个类中,A→B→C 多层调用,均有写库操作(需整体原子性)
最优方案:只给最外层入口方法加事务,内部方法改为private,直接调用(无需注入自己、无需AopContext,无循环依赖)。
@Service public class BizService { // 唯一事务入口:只给最外层public方法加注解 @Transactional(rollbackFor = Exception.class) public void methodA() { // A自身写库操作 insertA(); // 内部直接调用private方法,无代理问题,事务生效 methodB(); } // 内部方法:private,不加事务注解 private void methodB() { // B自身写库操作 insertB(); methodC(); } // 内部方法:private,不加事务注解 private void methodC() { // C自身写库操作 insertC(); // 任意环节抛异常,整体回滚 if (true) { throw new RuntimeException("异常回滚"); } } // 私有数据库操作方法 private void insertA() { /* mapper操作 */ } private void insertB() { /* mapper操作 */ } private void insertC() { /* mapper操作 */ } }
优势:
-
无循环依赖:无需@Autowired注入自己,彻底规避隐患。
-
事务不失效:外层入口开启事务,内部方法共享同一个事务上下文。
-
代码规范:事务注解只在入口,职责清晰,无冗余注解。
场景2:跨类调用(A→B→C,分属不同Service)
最优方案:只给最外层入口方法加事务,内部跨类调用天然走代理,无需额外处理。
// 入口Service:只加事务 @Service public class AService { @Autowired private BService bService; @Transactional(rollbackFor = Exception.class) public void methodA() { insertA(); bService.methodB(); // 跨类调用,走代理,事务生效 } private void insertA() { /* mapper操作 */ } } // 中间Service:不加事务 @Service public class BService { @Autowired private CService cService; public void methodB() { insertB(); cService.methodC(); // 跨类调用,走代理 } private void insertB() { /* mapper操作 */ } } // 最内层Service:不加事务 @Service public class CService { public void methodC() { insertC(); // 抛异常,整体回滚 throw new RuntimeException("异常回滚"); } private void insertC() { /* mapper操作 */ } }
原因:跨类调用天然通过代理对象调用,事务传播机制(默认REQUIRED)会让所有方法加入同一个事务,外层事务回滚,所有操作均回滚。
大厂禁止的写法(避坑)
-
本类@Autowired注入自己(制造循环依赖):
@Autowired private BizService self; -
使用AopContext.currentProxy()强转调用(侵入代码,需额外配置):
((BizService)AopContext.currentProxy()).methodB(); -
内部方法也加@Transactional(同类自调用失效,误导他人)。
五、终极排查清单(遇到事务失效,直接对照)
-
是不是同类内部自调用(this.方法())?(最常见)
-
方法是不是public、非final、非static?
-
异常是不是被try-catch吃掉,没抛出?
-
类有没有加@Component/@Service,是不是Spring Bean?
-
数据库引擎是不是InnoDB?(MyISAM不支持事务)
-
事务传播机制是不是NOT_SUPPORTED/NEVER?
-
是不是@Async和@Transactional一起用?
-
多数据源是不是没配置事务管理器?
六、总结
@Transactional注解看似简单,实则容易踩坑,核心是要理解"代理机制"------只有通过代理对象调用,事务才会生效。
记住3个核心点,就能写出规范无bug的事务代码:
-
用法:多条写操作加事务,单条写、纯查询不加;必加rollbackFor = Exception.class。
-
失效:自调用、修饰符错误、异常被捕获、类未被Spring管理,是最常见的4个坑。
-
最优写法:同类多层调用,外层入口加事务、内部private方法直接调用;跨类调用,外层入口加事务即可。
掌握这些,就能彻底解决Spring事务的所有常见问题,符合大厂代码规范,避免线上因事务失效导致的数据不一致问题。