
本文系统剖析 Spring @Transactional 失效的三大高频场景——方法自调用绕过代理、@Async 异步执行脱离事务上下文、以及受检异常(checked exception)未触发回滚,并结合原理、代码示例与可落地的修复方案,助你一次根治“事务不生效”顽疾。
本文系统剖析 spring `@transactional` 失效的三大高频场景——方法自调用绕过代理、`@async` 异步执行脱离事务上下文、以及受检异常(checked exception)未触发回滚,并结合原理、代码示例与可落地的修复方案,助你一次根治“事务不生效”顽疾。
在 Spring 应用中,@Transactional 是保障数据一致性的基石。但正如你在升级 Spring 3→5、Hibernate 3→5 过程中所遭遇的:upgradeToSuperUser() 方法首次调用时事务活跃(isActualTransactionActive() == true),第二次却返回 false,最终抛出 TransactionRequiredException——这并非配置错误,而是 Spring 事务底层机制被无意绕过的典型信号。
? 根本原因:事务依赖代理,而非方法签名
Spring 的声明式事务本质是基于 AOP 代理实现的。容器启动时,为标注 @Transactional 的 Bean 创建代理对象(JDK 动态代理或 CGLIB 子类)。只有通过该代理对象发起的外部调用,才会触发 TransactionInterceptor 拦截,完成事务开启 → 执行 → 提交/回滚的全生命周期。
而你的 SuUser() 方法位于 Action 类中,直接调用 userManager.upgradeToSuperUser(...) —— 若 userManager 是 Spring 管理的 Bean,此调用本应生效;但若 upgradeToSuperUser() 在 userManager 内部又被其他非代理路径(如 this.xxx())二次调用,或 userManager 实际是手动 new 出的实例,则代理完全失效。更关键的是:TransactionSynchronizationManager 的事务上下文绑定在当前线程的 ThreadLocal 中。一旦方法执行完毕、事务提交/回滚,上下文即被清理,再次调用时自然 isActualTransactionActive() == false,后续 JPA 操作因无活跃事务而报错。
⚠️ 三大高频失效场景与精准修复
1. 方法自调用(This-Reference 调用)—— 占比超 60%
@Service
public class UserManagerImpl implements UserManager {
@Override
@Transactional
public void upgradeToSuperUser(UserBO newUser, UserBO oldUser) {
// ✅ 正确:通过代理调用(外部触发)
updateRole(newUser.getId(), "SUPERUSER");
updateRole(oldUser.getId(), "TEACHER"); // ❌ 错误:this.updateRole() 绕过代理!
}
// ❌ 非 public / package-private / this 调用均无效
void updateRole(Long id, String role) {
entityManager.createNativeQuery("UPDATE USER SET ROLE=? WHERE ID=?")
.setParameter(1, role).setParameter(2, id).executeUpdate();
}
}
✅ 修复方案(三选一):
-
注入自身 Bean(推荐):
@Autowired private UserManager self; // 循环依赖需加 @Lazy // 替换 this.updateRole(...) → self.updateRole(...)
-
启用暴露代理(需全局配置):
<annotation-driven expose-proxy="true"></annotation-driven>
// 代码中改为 ((UserManager) AopContext.currentProxy()).updateRole(...);
-
拆分至独立 Service:将
updateRole提取到RoleService,由 Spring 注入调用。
2. @Async 异步方法默认脱离事务
@Async 方法在新线程执行,TransactionSynchronizationManager 的 ThreadLocal 不会自动传递,导致子线程无事务上下文。
@Service
public class UserService {
@Transactional
public void transfer(String from, String to, BigDecimal amount) {
deduct(from, amount); // ✅ 主线程事务内
sendNotificationAsync(); // ❌ 新线程,无事务!
}
@Async
public void sendNotificationAsync() {
// 此处数据库操作将报 TransactionRequiredException
}
}
✅ 修复方案:
- 显式传播事务(需 Spring 5.3+ 且配置
task:executor支持事务继承):@Async(value = "transactionalTaskExecutor") // 自定义支持事务的线程池 public void sendNotificationAsync() { ... } - 或改用同步通知 + 异步补偿,避免在
@Async中执行需事务的 DB 操作。
3. 受检异常(Checked Exception)不触发回滚
Spring 默认仅对 RuntimeException 和 Error 回滚。若业务逻辑 throws Exception 且未捕获重抛,事务将静默提交。
@Transactional
public void riskyOperation() throws IOException {
fileService.read("config.txt"); // 抛出 IOException
userRepository.save(new User()); // 即使上面失败,此处仍会提交!
}
✅ 修复方案:
- 明确指定回滚异常类型:
@Transactional(rollbackFor = {IOException.class, Exception.class}) public void riskyOperation() throws IOException { ... } - 或统一转换为运行时异常:
} catch (IOException e) { throw new RuntimeException("I/O failed", e); }
?️ 升级 Spring 5 的特别注意事项
你当前使用 <annotation-driven mode="aspectj"></annotation-driven>,需确认:
-
spring-aspects与aspectjweaver版本兼容 Spring 5(建议aspectjweaver:1.9.7+); -
SharedEntityManagerBean配置正确,确保entityManagerFactory已初始化; -
禁用
proxy-target-class="true"时,务必保证目标类非 final,方法为 public —— 否则 CGLIB 代理失败,事务彻底失效。
✅ 最终验证清单
| 检查项 | 是否满足 | 说明 |
|---|---|---|
方法是否 public? |
☐ |
private/protected 方法注解无效 |
| 调用是否经由 Spring 代理? | ☐ | 避免 new UserManagerImpl() 或 this.xxx()
|
| 异常是否在回滚范围内? | ☐ | 检查 rollbackFor 配置或异常类型 |
@Async 是否需事务? |
☐ | 新线程默认无事务,需特殊配置 |
TransactionSynchronizationManager 是否跨线程? |
☐ |
@Async/线程池任务需显式传递上下文 |
总结:事务不是魔法,而是精密的代理链与线程上下文协同的结果。排查失效问题,永远从「调用路径是否经过代理」和「事务上下文是否存在于当前线程」两个根本点切入。修复后,
isActualTransactionActive()将稳定返回true,TransactionRequiredException自然消失。










