不只是注解失效,而是代理机制被绕过了
很多团队第一次遇到事务失效时,第一反应是 @Transactional 这个注解是不是写错了,或者Spring版本有兼容性问题。但真实情况往往是,代码的调用路径绕过了Spring精心构建的那套AOP代理机制。理解这一点,是排查所有事务问题的起点。
Spring的声明式事务并不是一个魔法开关。当你给一个方法加上 @Transactional,Spring在容器启动时,并不会直接修改这个方法,而是为这个Bean创建了一个代理对象。后续所有通过Spring容器(比如通过 @Autowired)获取到的,其实都是这个代理对象。事务的开启、提交、回滚逻辑,都封装在代理对象的方法增强里。
// 这是你写的Service
@Service
public class OrderService {
@Transactional
public void createOrder() {
// 业务逻辑
}
}
// Spring在背后为你做的事情(概念上)
public class OrderService$$EnhancerBySpringCGLIB extends OrderService {
private OrderService target; // 原始对象
private TransactionInterceptor interceptor; // 事务拦截器
public void createOrder() {
// 1. 拦截器介入:开启事务
TransactionStatus status = transactionManager.beginTransaction();
try {
// 2. 调用原始对象的createOrder()
target.createOrder();
// 3. 成功则提交事务
transactionManager.commit(status);
} catch (RuntimeException e) {
// 4. 失败则回滚事务
transactionManager.rollback(status);
throw e;
}
}
}
所以,事务“失效”的本质,是你的调用没有走到上面这段增强逻辑里。下面我们来看几个工程中最容易踩坑的场景。
高频失效场景一:同类中的“自调用”
这是新手和老手都可能掉进去的经典陷阱。假设你有一个订单服务,在创建订单的主流程里,需要调用一个更新库存的方法,并且希望这个更新是事务性的。
@Service
public class OrderServiceV1 {
public void processOrder() {
// ... 一些非事务性操作
this.deductStock(); // 事务在这里失效了!
}
@Transactional
public void deductStock() {
// 扣减库存的数据库操作
stockMapper.decrease(productId, quantity);
}
}
为什么失效?因为这里的 this.deductStock() 调用的是原始 OrderServiceV1 对象上的方法,而不是Spring注入的那个代理对象。在Java中,this 关键字永远指向当前实例,它不会因为Spring的存在而变成代理对象。于是,事务拦截器完全没有机会介入。
解决方案对比:
| 方案 | 做法 | 优点 | 缺点/注意点 |
|---|---|---|---|
| 抽取到独立Service | 将 deductStock 方法移到另一个Service(如 StockService),然后注入调用。 |
符合单一职责,结构清晰,是最推荐的方式。 | 需要多创建一个类,对于简单逻辑可能显得繁琐。 |
| 注入自身代理 | 在类内 @Autowired private OrderService self;,然后通过 self.deductStock() 调用。 |
能快速解决当前类的问题。 | 代码有点“怪”,容易引起困惑,且存在循环依赖风险。 |
| 使用AopContext | 调用 ((OrderService) AopContext.currentProxy()).deductStock()。 |
无需注入,直接获取代理。 | 需要配置 @EnableAspectJAutoProxy(exposeProxy = true),且强依赖于AOP上下文。 |
对于大多数项目,我会建议采用第一种方案。它不仅解决了事务问题,还促使你的代码结构向更松散耦合的方向演进。
高频失效场景二:异常被“吞掉”或类型不对
事务回滚的触发条件是“方法执行过程中抛出了未被捕获的、符合回滚规则的异常”。这里有两个关键点容易被忽略。
第一,异常被捕获却没重新抛出。 这是为了“友好处理错误”而常犯的错。
@Transactional
public void saveData(Data data) {
try {
dataDao.insert(data);
// 模拟一个业务异常
if (data.isInvalid()) {
throw new RuntimeException("数据无效");
}
} catch (Exception e) {
// 糟糕!异常在这里被“吃”掉了
log.error("保存数据失败", e);
// 没有 throw e;
}
// Spring事务拦截器认为方法正常结束,提交事务!
}
在这个例子里,虽然抛出了 RuntimeException,但在方法边界内就被捕获并处理了。外层的 TransactionInterceptor 根本感知不到异常的发生,自然会提交事务。如果你必须在事务方法内进行异常捕获,又希望触发回滚,可以手动标记事务状态:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();。
第二,抛出了“不回滚”的异常类型。 @Transactional 默认只对 RuntimeException 和 Error 进行回滚。如果你抛出了一个受检异常(Checked Exception),比如 IOException 或自定义的业务异常(如果它继承自 Exception 而非 RuntimeException),事务同样不会回滚。
@Transactional
public void transferMoney() throws BusinessException { // BusinessException 继承自 Exception
debitAccount();
creditAccount();
if (balanceCheckFailed) {
throw new BusinessException("余额不足"); // 默认不回滚!
}
}
解决办法是在注解中明确指定:@Transactional(rollbackFor = Exception.class)。在实际项目中,为了避免遗漏,很多团队会直接采用这个配置,或者统一让自定义业务异常继承自 RuntimeException。
其他需要留意的“边界”情况
除了上述两大高频场景,还有一些边界条件也值得注意:
- 方法可见性非public: Spring的AOP代理(无论是JDK动态代理还是CGLIB)默认只对public方法生效。将
@Transactional用在protected、private或包权限方法上,注解会被直接忽略。 - 数据库引擎不支持: Spring事务是对底层数据库事务的封装。如果你用的是MySQL的MyISAM表,或者某些NoSQL数据库,它们本身不支持事务,那么Spring的注解自然也无能为力。
- 多线程调用: 事务信息(如数据库连接)通常绑定在
ThreadLocal上。如果你在一个事务方法里启动新线程去执行数据库操作,这个新线程将无法继承原事务的上下文,操作将在非事务环境下进行。 - 传播行为配置的误解: 比如在方法上配置了
Propagation.NOT_SUPPORTED(以非事务方式执行,挂起当前事务),那么无论调用者是否有事务,该方法都不会在事务中运行。这有时是预期的,但若误用就会导致“失效”错觉。
排查事务问题的实战思路
当遇到事务不生效时,可以遵循以下步骤进行排查:
- 确认代理是否生效: 在调试时,查看调用方法时
this引用的对象类型。如果打印出的类名包含$$EnhancerBySpringCGLIB$$或$Proxy(JDK代理),说明代理对象是存在的。如果是原始类名,则说明可能是通过new创建或自调用。 - 检查方法可见性: 确保
@Transactional注解的方法是public的。 - 审查异常处理: 仔细检查事务方法内部及调用链上是否有
try-catch吞掉异常,或者抛出的异常类型是否不在默认回滚列表里。 - 开启Spring事务调试日志: 在配置文件中设置
logging.level.org.springframework.transaction.interceptor=TRACE。Spring会输出详细的事务开启、提交、回滚日志,这对于定位问题非常有用。 - 简化验证: 创建一个最简单的
@Test,只注入Service并调用那个事务方法,看事务是否生效。这可以排除业务代码中其他复杂调用链的干扰。
理解本质,而非记忆场景
Spring事务失效的场景列表可以列得很长,但核心原理始终围绕着“AOP代理”和“异常触发回滚”这两个支柱。下次当你配置 @Transactional 时,不妨在脑子里过一遍:
- 我这次调用,走的是Spring给的代理对象吗?
- 如果方法执行出错,一个能让Spring感知到的异常,会被顺利抛到方法之外吗?
想清楚这两个问题,大部分失效场景你都能提前规避。事务管理是确保数据一致性的基石,花点时间理解其背后的机制,远比盲目复制粘贴解决方案要可靠得多。
原创文章,作者:,如若转载,请注明出处:https://fczx.net/wiki/133