Spring @Transactional 注解全解析

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动态代理实现,核心流程如下:

  1. Spring启动时,扫描到带有@Transactional的类/方法,会为该类生成CGLIB代理对象(若类实现接口,用JDK动态代理)。

  2. 代理对象会拦截带@Transactional的方法,在方法执行前开启事务,执行后提交事务,异常时回滚事务。

  3. 只有通过"代理对象.方法()"调用,才会触发拦截;通过"原生对象(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)会让所有方法加入同一个事务,外层事务回滚,所有操作均回滚。

大厂禁止的写法(避坑)

  1. 本类@Autowired注入自己(制造循环依赖):@Autowired private BizService self;

  2. 使用AopContext.currentProxy()强转调用(侵入代码,需额外配置):((BizService)AopContext.currentProxy()).methodB();

  3. 内部方法也加@Transactional(同类自调用失效,误导他人)。

五、终极排查清单(遇到事务失效,直接对照)

  1. 是不是同类内部自调用(this.方法())?(最常见)

  2. 方法是不是public、非final、非static?

  3. 异常是不是被try-catch吃掉,没抛出?

  4. 类有没有加@Component/@Service,是不是Spring Bean?

  5. 数据库引擎是不是InnoDB?(MyISAM不支持事务)

  6. 事务传播机制是不是NOT_SUPPORTED/NEVER?

  7. 是不是@Async和@Transactional一起用?

  8. 多数据源是不是没配置事务管理器?

六、总结

@Transactional注解看似简单,实则容易踩坑,核心是要理解"代理机制"------只有通过代理对象调用,事务才会生效。

记住3个核心点,就能写出规范无bug的事务代码:

  1. 用法:多条写操作加事务,单条写、纯查询不加;必加rollbackFor = Exception.class。

  2. 失效:自调用、修饰符错误、异常被捕获、类未被Spring管理,是最常见的4个坑。

  3. 最优写法:同类多层调用,外层入口加事务、内部private方法直接调用;跨类调用,外层入口加事务即可。

掌握这些,就能彻底解决Spring事务的所有常见问题,符合大厂代码规范,避免线上因事务失效导致的数据不一致问题。

相关推荐
xiaogg36783 小时前
spring oauth2 单点登录
java·vue.js·spring
Rust研习社4 小时前
Rust + PostgreSQL 极简技术栈应用开发
开发语言·数据库·后端·http·postgresql·rust
c++之路4 小时前
C++ STL
java·开发语言·c++
河阿里4 小时前
MyBatis-Plus:MyBatis的进阶开发
数据库·mybatis
白晨并不是很能熬夜4 小时前
【RPC】第 4 篇:服务发现 — Zookeeper + 缓存容错
java·后端·程序人生·缓存·zookeeper·rpc·服务发现
EvenBoy4 小时前
IDEA中使用CodeX
java·ide·intellij-idea
日取其半万世不竭4 小时前
Minecraft Java版社区服搭建教程(Windows版)
java·开发语言·windows
sjsjsbbsbsn4 小时前
向量数据库
数据库
逸Y 仙X4 小时前
文章十六:ElasticSearch 使用enrich策略实现大宽表
java·大数据·数据库·elasticsearch·搜索引擎·全文检索