@Transactional失效的情况总结
前言
@Transactional失效是实际开发中非常容易踩的坑,本文结合实际项目经验总结了常见的失效场景和解决方案。
一、最常见的:同类内部调用(占80%的坑)
这是最容易犯的错误,也是开发中最常遇到的问题。
问题描述
比如说,我有一个UserService类,里面有两个方法:methodA和methodB。methodA没加事务注解,methodB加了@Transactional。如果我在methodA里直接调用methodB,那methodB的事务就不会生效。
原因分析
因为Spring的事务是基于动态代理实现的。从外部调用methodB时,调用的是代理对象,代理对象会启动事务;但是在类内部,methodA调用methodB实际上是通过this.methodB()调用的,this是当前对象本身,不是代理对象,所以绕过了代理,事务就失效了。
解决方案
- 最简单的办法就是把methodB提取到另一个Service类里
- 或者自己注入自己(虽然有点奇怪但确实可以work)
- 再或者用AspectJ的编译时织入,但这个比较复杂
实际项目中,我们都是拆分成不同的Service,这样代码职责也更清晰。
二、方法不是public的
这个也很容易犯错。
问题描述
如果你把@Transactional加在private、protected或者default(包级私有)的方法上,事务不会生效。
原因分析
还是因为代理机制。Spring默认用的是CGLIB代理,它是通过生成子类来实现的。子类只能覆盖父类的public方法,private和protected方法子类要么访问不到,要么访问受限,所以代理不了。
实际案例
之前有个同事写了一个private的保存方法,加了@Transactional,结果数据保存了一半出异常了也没回滚。调试了好久才发现是方法修饰符的问题。
解决方案
把方法改成public就行了。如果实在不想暴露这个方法,那就重新设计一下,把它提取到另一个内部使用的Service里。
三、异常被吞了(捕获了但没抛出)
这个坑非常隐蔽,很多人都踩过。
问题描述
你在方法里写了try-catch,捕获了异常但只是打印了个日志,没有往外抛,那事务就不会回滚。
原因分析
Spring的事务管理器是通过捕获异常来决定要不要回滚的。如果你把异常吞掉了,Spring根本不知道出问题了,就不会回滚。
实际案例
我之前做转账功能,扣款成功了,加款的时候失败了,我当时catch了异常只是打了个日志,结果钱被扣了但没加到对方账户,造成了资金不平。这个bug上线后被客户投诉了,幸好金额不大。
解决方案
- 要么就不要catch异常,让它自然抛出去
- 要么catch了之后,处理完再throw出去,比如抛个RuntimeException或者自定义的业务异常
四、抛的是受检异常(CheckedException)
这个比较容易被忽略。
问题描述
Spring默认只对RuntimeException和Error进行回滚,对于CheckedException(就是那种必须要catch或者在方法签名上throws的异常),默认不回滚。
原因分析
因为受检异常通常认为是可以预期和恢复的业务异常,不一定要回滚事务。这是Spring的一个设计理念。
解决方案
在@Transactional注解上加一个参数:rollbackFor = Exception.class,这样所有异常都会回滚。或者具体指定你要回滚的异常类型。
我现在基本都会习惯性地加上rollbackFor = Exception.class,避免遗漏。
五、数据库引擎不支持事务
这个现在比较少见了,但面试还是要说一下。
问题描述
MySQL有多种存储引擎,InnoDB支持事务,但MyISAM不支持事务。如果你的表用的是MyISAM引擎,加了@Transactional也没用。
实际情况
现在MySQL默认都是InnoDB了,基本不会遇到这个问题。但如果是很老的项目,或者从别的地方迁移过来的数据库,可能会有MyISAM表。
检查方法
可以在数据库里执行show create table命令,看一下ENGINE是什么。如果是MyISAM,改成InnoDB就行了。
六、没有被Spring管理(没有@Service等注解)
这个属于比较低级的错误,但新手容易犯。
问题描述
如果你的类没有加@Service、@Component这些注解,或者不是通过@Bean方式注册到Spring容器的,那Spring根本不管理这个类,@Transactional自然也不会生效。
常见场景
比如你自己new了一个Service对象,而不是通过@Autowired注入的,这个对象就不是Spring管理的,事务肯定不生效。
解决方案
- 确保类被Spring容器管理,要么加@Service等注解,要么在配置类里用@Bean注册
- 使用的时候一定要通过@Autowired或者@Resource注入,不要自己new
七、事务传播类型设置不当
这个需要对事务传播机制有一定理解。
问题描述
如果你把propagation设置成了NOT_SUPPORTED、NEVER这种,那方法就不会在事务中执行。或者设置成REQUIRES_NEW,会开启独立事务,可能不是你想要的效果。
实际案例
我见过有同事为了解决某个问题,把propagation改成了NOT_SUPPORTED,结果方法里的数据库操作没了事务保护,出现了数据不一致。
建议
如果不是特别清楚传播机制,就不要乱改,用默认的REQUIRED就好。需要修改的场景其实很少,主要是记录日志、批量处理这些特殊情况。
八、多线程调用
这个坑比较隐蔽,但实际项目中确实会遇到。
问题描述
Spring的事务是和线程绑定的,事务信息存在ThreadLocal里。如果你在一个事务方法里开了新线程,新线程里的数据库操作不会在原来的事务中。
实际案例
我之前做一个批量导入功能,为了提高性能,在事务方法里用了线程池并行处理。结果发现出错的时候,部分数据回滚了,部分数据提交了,数据乱了。
原因分析
因为新线程拿不到主线程的事务上下文,每个线程是独立的事务(如果有的话),或者根本没事务。
解决方案
- 要么不用多线程
- 要么把事务放到子线程里,每个子线程独立事务
- 要么用分布式事务框架
总之,不能天真地以为开了多线程还能共享事务。
九、方法是final的(CGLIB代理时)
这个和第二点原因类似。
问题描述
如果用的是CGLIB代理(没有接口的情况),目标方法被声明为final,那这个方法无法被子类覆盖,事务就不会生效。
常见原因
有些开发者习惯把不希望被重写的方法标记为final,但如果这个方法需要事务,就会有问题。
解决方案
把final去掉。如果实在需要防止子类重写,那就定义一个接口,用JDK动态代理。
十、类是final的
这个更直接。
问题描述
如果你的Service类本身被声明为final,那CGLIB根本没法为它创建子类,代理就创建不了,所有事务注解都失效。
常见场景
不常见,但如果你从其他框架迁移代码过来,或者用了某些代码生成工具,可能会遇到。
解决方案
把final class改成普通class。
十一、没有配置事务管理器或者没开启事务支持
这个属于配置问题。
问题描述
Spring Boot项目一般会自动配置,但如果是传统Spring项目,需要手动配置TransactionManager,或者加@EnableTransactionManagement注解。如果这些配置缺失,@Transactional不会生效。
检查方法
- 启动时看看Spring的日志,会提示有没有注册事务管理器
- 或者故意制造一个异常,看看会不会回滚,就知道事务有没有生效了
十二、隔离级别或传播级别数据库不支持
这个比较少见,但也要提一下。
问题描述
比如你设置了某个隔离级别,但数据库不支持,事务行为可能不符合预期。或者用了NESTED传播级别,但数据库不支持Savepoint,也会有问题。
实际情况
大部分主流数据库都支持标准的隔离级别,这个问题现在不太常见。NESTED在MySQL InnoDB上是支持的,但某些老版本或其他数据库可能不支持。
总结
最常见的失效原因Top 5
- 同类内部调用 - 这是最最常见的,占了80%的坑
- 异常被catch了没抛出 - 很隐蔽,容易忽略
- 方法不是public - 初学者容易犯
- 抛的是CheckedException但没指定rollbackFor - 容易被忽略
- 没有被Spring管理 - 自己new的对象不生效
实践建议
- 养成好习惯,事务方法都写成public
- 不要在类内部直接调用事务方法,拆分到不同Service
- try-catch后一定要把异常重新抛出去
- @Transactional上加
rollbackFor = Exception.class,保险起见 - 确保类被@Service等注解标记,通过@Autowired注入使用
- 多线程的场景,重新设计事务边界
排查技巧
如果发现事务不生效,可以按以下步骤排查:
- 先看类有没有被Spring管理
- 再看方法是不是public
- 然后看是不是内部调用
- 最后看异常有没有被吞掉
基本上90%的问题都能通过这四步找到。
附录:常见问题对照表
| 失效场景 | 原因 | 解决方案 | 常见程度 |
|---|---|---|---|
| 同类内部调用 | 没走代理对象 | 拆分到不同Service | ⭐⭐⭐⭐⭐ |
| 方法不是public | 代理无法覆盖 | 改为public | ⭐⭐⭐⭐ |
| 异常被捕获 | Spring感知不到异常 | 重新抛出异常 | ⭐⭐⭐⭐ |
| CheckedException | 默认不回滚 | 加rollbackFor | ⭐⭐⭐ |
| 数据库引擎 | MyISAM不支持事务 | 改为InnoDB | ⭐⭐ |
| 未被Spring管理 | 不是Spring Bean | 加@Service注解 | ⭐⭐⭐ |
| 传播类型不当 | 配置错误 | 用默认REQUIRED | ⭐⭐ |
| 多线程调用 | ThreadLocal限制 | 重新设计事务边界 | ⭐⭐ |
| final方法 | 无法覆盖 | 去掉final | ⭐ |
| final类 | 无法生成子类 | 去掉final | ⭐ |
| 配置缺失 | 未启用事务支持 | 检查配置 | ⭐ |
| 数据库不支持 | 特性不兼容 | 更换配置或数据库 | ⭐ |