@Transactional失效的场景
@Transactional 注解失效是一个常见的痛点问题,通常由对 Spring 事务管理机制理解不深或配置疏忽引起。以下是常见的失效场景及其原因和解决方案:
📌 一、内部方法调用(最常见)
场景: 同一个类中,未加 @Transactional 的方法 A 调用了加了 @Transactional 的方法 B。
原因: Spring 事务管理基于代理(JDK 动态代理或 CGLIB)。代理对象在调用目标方法时,会通过拦截器链(包含事务拦截器)来管理事务。当你在同一个类内部调用 @Transactional 方法时,你调用的是 this.methodB(),而不是代理对象的 methodB(),因此事务拦截器没有被触发。
解决方案:
将调用方法移到另一个Bean: 将方法 A 和 方法 B 拆分到不同的 Service 类中,通过注入的 Service 来调用方法 B。
通过代理对象调用: 获取当前类的代理对象,然后通过代理对象调用方法 B(不推荐,侵入性强)。
使用 AopContext.currentProxy() (需要暴露代理): 在启动类或配置类加 @EnableAspectJAutoProxy(exposeProxy = true),然后在方法 A 中使用 ((YourServiceClass) AopContext.currentProxy()).methodB(); 调用。
使用 AspectJ 模式(编译时/加载时织入): 配置 Spring 使用 AspectJ 的 AOP(mode = AdviceMode.ASPECTJ),这种方式可以解决内部调用问题,但配置更复杂。
⚠ 二、异常类型不正确或被捕获
场景1: 方法抛出的是受检异常(Checked Exception),如 IOException、SQLException。
原因: Spring 事务默认只在抛出非受检异常(RuntimeException 及其子类) 和 Error 时才回滚。受检异常被认为是应用逻辑预期内可处理的,默认不会触发回滚。
解决方案: 在 @Transactional 注解中明确指定 rollbackFor 属性:
java
@Transactional(rollbackFor = {Exception.class, YourCheckedException.class}) // 回滚所有异常或指定异常
public void yourMethod() throws YourCheckedException {
// …
}
场景2: 方法内部 try-catch 捕获了异常,且没有在 catch 块中重新抛出异常或抛出新异常。
原因: 事务拦截器只有在看到方法抛出异常时,才会根据规则决定是否回滚。如果异常在方法内部被捕获并“吞掉”(没有重新抛出),事务拦截器就不知道发生了异常,事务自然会被提交。
解决方案:
在 catch 块中抛出合适的异常:
java
try {
// 可能出错的代码
} catch (Exception e) {
// 记录日志或其他处理
throw new YourBusinessException(e); // 抛出非受检异常或配置了rollbackFor的受检异常
}
在 catch 块中手动设置回滚(慎用):
java
try {
// 可能出错的代码
} catch (Exception e) {
// 记录日志或其他处理
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); // 标记当前事务为仅回滚
// 可以选择不再抛出异常,但事务会回滚
}
🔒 三、方法访问权限非 public
场景: @Transactional 注解应用在 protected、private 或 package-private(默认)方法上。
原因: Spring 的 AOP 代理(特别是 JDK 动态代理)要求被代理的方法必须是 public 的。对于非 public 方法,Spring 的事务拦截器可能无法正确应用。
解决方案: 确保所有需要事务管理的方法都是 public 的。这是 Spring 官方文档明确要求的。
🧵 四、多线程调用
场景: 在一个事务方法中,启动新线程去执行数据库操作。
原因: Spring 事务管理依赖于 ThreadLocal 来存储事务状态(如 Connection)。新线程拥有自己的 ThreadLocal 存储,与开启事务的原始线程隔离。新线程中的操作无法加入到原始线程的事务上下文中,会使用新的连接(通常开启了 autoCommit=true),从而脱离事务管理。
解决方案: 避免在事务方法中创建新线程进行数据库操作。如果必须并发操作,考虑:
使用异步任务框架(如 Spring @Async),但要清楚每个异步任务是在独立的事务中运行的。
使用分布式事务管理器(如 Seata) 管理跨线程/服务的事务(复杂且影响性能)。
重新设计,将需要并发执行的操作移到事务边界之外。
🚫 五、事务传播行为设置不当
场景: 方法的事务传播行为被设置为 Propagation.NOT_SUPPORTED、Propagation.NEVER、Propagation.SUPPORTS(且当前没有事务)等。
原因: 这些传播行为明确表示该方法不应该在事务中运行。
NOT_SUPPORTED:挂起当前事务(如果存在),非事务方式执行。
NEVER:要求不能在事务中执行,否则抛异常。
SUPPORTS:如果当前有事务,则加入;没有事务,则以非事务方式执行。
解决方案: 检查方法的 @Transactional(propagation = …) 设置,确保其传播行为符合你的预期(通常默认的 Propagation.REQUIRED 是需要的)。如果需要事务,避免使用上述三种传播行为。
🛠 六、未启用事务管理(配置问题)
场景: 整个应用或特定数据源/ORM 框架的事务管理未被启用。
原因:
忘记添加 @EnableTransactionManagement: 在基于 Java 配置的 Spring Boot 应用或 Spring 应用中,需要在配置类上添加此注解来启用事务管理注解驱动。Spring Boot 项目通常通过 spring-boot-starter-* 自动配置,但如果自定义配置不当可能失效。
数据源未配置事务管理器: Spring 需要一个 PlatformTransactionManager Bean(如 DataSourceTransactionManager、JpaTransactionManager、HibernateTransactionManager)来管理事务。如果未配置或配置错误(如使用了错误类型的事务管理器),事务无法生效。
ORM框架事务未集成: 如果使用 JPA/Hibernate 等,确保正确配置了对应的事务管理器(JpaTransactionManager/HibernateTransactionManager)。
解决方案:
确认配置类上有 @EnableTransactionManagement (Spring Boot 数据源 starter 通常自动配置,无需显式添加,但检查无妨)。
确认 Spring 容器中存在正确类型(匹配你的数据访问技术)的 PlatformTransactionManager Bean。Spring Boot 通常会为你自动配置好。
🗄 七、数据库引擎不支持事务
场景: 使用的数据库表引擎是 MyISAM。
原因: MySQL 的 MyISAM 存储引擎不支持事务。只有 InnoDB 等支持事务的引擎才能利用 Spring 的事务管理。
解决方案: 将相关表的存储引擎改为 InnoDB。
📝 八、手动提交或回滚干扰
场景: 在方法中手动获取了 Connection 并调用了 commit() 或 rollback()。
原因: Spring 事务管理期望完全控制事务的生命周期(开始、提交、回滚)。手动操作连接会干扰 Spring 的管理,导致状态不一致。
解决方案: 绝对避免在使用了 @Transactional 的方法中手动管理 Connection 的提交或回滚。让 Spring 统一管理。
🔍 排查建议
检查日志: 开启 Spring 的 Debug 日志(如 logging.level.org.springframework.transaction=DEBUG 或 logging.level.org.springframework.jdbc=DEBUG),查看事务的开启、提交、回滚日志。
检查代理: 在 Debug 时,查看调用方法的对象是否是代理对象(类名通常包含 EnhancerBySpringCGLIBEnhancerBySpringCGLIBEnhancerBySpringCGLIB 或 $Proxy)。
简化测试: 创建一个最简单的 @Transactional 方法(只做一次插入/更新然后抛异常),看是否能回滚,逐步添加复杂逻辑定位问题点。
检查配置: 确保 @EnableTransactionManagement 存在,确保正确的事务管理器 Bean 存在且被注入。
检查数据库引擎: 确认表使用的是 InnoDB。
审查异常处理: 仔细检查方法内部是否捕获了异常而没有重新抛出,或者抛出的异常类型是否符合 rollbackFor 规则。
通过系统性地检查以上场景,基本可以定位并解决绝大多数 @Transactional 失效的问题。理解 Spring 事务的代理机制、传播行为、异常处理规则是解决问题的关键。💡
1011

被折叠的 条评论
为什么被折叠?



