Transactional失效的情况总结

@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

  1. 同类内部调用 - 这是最最常见的,占了80%的坑
  2. 异常被catch了没抛出 - 很隐蔽,容易忽略
  3. 方法不是public - 初学者容易犯
  4. 抛的是CheckedException但没指定rollbackFor - 容易被忽略
  5. 没有被Spring管理 - 自己new的对象不生效

实践建议

  • 养成好习惯,事务方法都写成public
  • 不要在类内部直接调用事务方法,拆分到不同Service
  • try-catch后一定要把异常重新抛出去
  • @Transactional上加rollbackFor = Exception.class,保险起见
  • 确保类被@Service等注解标记,通过@Autowired注入使用
  • 多线程的场景,重新设计事务边界

排查技巧

如果发现事务不生效,可以按以下步骤排查:

  1. 先看类有没有被Spring管理
  2. 再看方法是不是public
  3. 然后看是不是内部调用
  4. 最后看异常有没有被吞掉

基本上90%的问题都能通过这四步找到。


附录:常见问题对照表

失效场景 原因 解决方案 常见程度
同类内部调用 没走代理对象 拆分到不同Service ⭐⭐⭐⭐⭐
方法不是public 代理无法覆盖 改为public ⭐⭐⭐⭐
异常被捕获 Spring感知不到异常 重新抛出异常 ⭐⭐⭐⭐
CheckedException 默认不回滚 加rollbackFor ⭐⭐⭐
数据库引擎 MyISAM不支持事务 改为InnoDB ⭐⭐
未被Spring管理 不是Spring Bean 加@Service注解 ⭐⭐⭐
传播类型不当 配置错误 用默认REQUIRED ⭐⭐
多线程调用 ThreadLocal限制 重新设计事务边界 ⭐⭐
final方法 无法覆盖 去掉final
final类 无法生成子类 去掉final
配置缺失 未启用事务支持 检查配置
数据库不支持 特性不兼容 更换配置或数据库

posted @ 2026-01-25 21:25  菜鸟~风  阅读(7)  评论(0)    收藏  举报