你有没有遇到过这样的场景?
你正在开发一个用户注册功能:先保存用户信息,再记录一条操作日志。为了保证数据一致性,你在主方法上加了 @Transactional
注解。但突然有一天,用户注册失败了,事务回滚了------这没问题。可你回头一看日志表,发现那条"注册失败"的审计日志居然也跟着没了!
你一脸困惑:
"我明明是在事务里写入的日志,为什么回滚后日志也没了?"
"我不是用了
REQUIRES_NEW
吗?它难道不是独立事务?"
这就是我们今天要揭开的 Spring 事务中一个极其隐蔽却又高频踩坑的问题: @Transactional(propagation = Propagation.REQUIRES_NEW)
的"伪独立"陷阱。
表面上看,REQUIRES_NEW
会挂起当前事务,开启一个全新的事务。理论上,即使外层事务回滚,这个新事务也应该已经提交、数据保留下来。但现实却常常打脸------尤其是在你没有真正理解 Spring 事务底层机制的情况下。
比如,当你在一个 @Transactional
方法中调用另一个标记为 REQUIRES_NEW
的方法时,你以为它运行在独立事务中,但实际上,如果这个方法调用发生在同一个类内(自调用),或者异常被捕获、传播配置不当,那么所谓的"新事务"可能根本没生效,日志自然也就随着主事务一起回滚了。
更让人头疼的是,这种问题在开发和测试阶段往往难以发现,只有在线上出现异常时才暴露出来,导致问题追溯困难、数据对账混乱。
这正是我们今天深入探讨的起点。
✅ 正常流程(理想情况):
java
@Transactional
public void registerUser(User user) {
saveUser(user); // 主事务中的操作
logService.log("User registration attempt");
// ↑ 调用外部 Bean,触发 REQUIRES_NEW,独立事务提交
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String message) {
logMapper.insert(new Log(message));
}
}
此时:即使 registerUser
后续抛出异常导致主事务回滚,log()
方法由于运行在独立事务中,且已提交,日志依然存在。
为什么日志还是消失了?有以下几种可能
🛑 1. rollbackFor
未正确设置
默认情况下,Spring 事务只会回滚 RuntimeException
和 Error
。如果在方法内抛出了其他类型的异常(例如 IOException
),而没有设置 rollbackFor
,事务就不会回滚。
示例:
java
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void log(String message){
try{
logMapper.insert(new Log(message));
}catch(Exception e) {
throw new AuditLogException("审计日志记录失败", e);
}
}
}
在这个例子中,AuditLogException
被显式列为需要回滚的异常类型。
⚠️ 2. 异常被捕获后没有重新抛出
当异常被 catch
捕获并且没有重新抛出时,Spring 事务管理器会认为方法执行没有问题,从而不会触发回滚。为了确保事务能够回滚,捕获的异常应该被重新抛出,或者标记为需要回滚的异常。
示例:
java
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void log(String message){
try{
logMapper.insert(new Log(message));
}catch(Exception e) {
log.error("Error occurred: ", e); // 捕获异常但不抛出,事务不会回滚
}
}
}
在此代码中,异常被捕获但没有重新抛出,导致事务无法回滚。
🚫 3. 同一类中的方法调用
Spring 的事务管理基于动态代理实现,但如果同一类中的方法相互调用(自调用),事务管理就不会生效。因为自调用不会触发代理方法,事务管理也无法插入到执行过程中。
示例:
java
@Transactional
public void registerUser(User user) {
saveUser(user); // 主事务中的操作
log("User registration attempt");
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String message) {
logMapper.insert(new Log(message));
}
解决办法:
可以通过将方法提取到不同的类中,或通过外部调用来触发代理。
🔒 4. @Transactional
应用在final
或非 public
方法上
Spring 默认使用动态代理(JDK 动态代理或 CGLIB)来实现声明式事务管理(@Transactional
注解)。JDK 动态代理只能代理接口的 public
方法,CGLIB 可以代理非 public
方法,无法代理 final
方法,因此这些方法上的事务也会失效。
示例:
java
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
private void log(String message) {
logMapper.insert(new Log(message));
}
或者
@Transactional(propagation = Propagation.REQUIRES_NEW)
public final void log(String message) {
logMapper.insert(new Log(message));
}
}
🔄 5. 事务传播机制配置错误
如果配置了错误的事务传播行为,误将Propagation.REQUIRES_NEW
配置为Propagation.REQUIRES
示例:
java
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES)
public void log(String message) {
logMapper.insert(new Log(message));
}
}