你有没有遇到过这样的场景?

你正在开发一个用户注册功能:先保存用户信息,再记录一条操作日志。为了保证数据一致性,你在主方法上加了 @Transactional 注解。但突然有一天,用户注册失败了,事务回滚了——这没问题。可你回头一看日志表,发现那条“注册失败”的审计日志居然也跟着没了

你一脸困惑:

这就是我们今天要揭开的 Spring 事务中一个极其隐蔽却又高频踩坑的问题: @Transactional(propagation = Propagation.REQUIRES_NEW) 的“伪独立”陷阱

表面上看,REQUIRES_NEW 会挂起当前事务,开启一个全新的事务。理论上,即使外层事务回滚,这个新事务也应该已经提交、数据保留下来。但现实却常常打脸——尤其是在你没有真正理解 Spring 事务底层机制的情况下。

比如,当你在一个 @Transactional 方法中调用另一个标记为 REQUIRES_NEW 的方法时,你以为它运行在独立事务中,但实际上,如果这个方法调用发生在同一个类内(自调用),或者异常被捕获、传播配置不当,那么所谓的“新事务”可能根本没生效,日志自然也就随着主事务一起回滚了。

更让人头疼的是,这种问题在开发和测试阶段往往难以发现,只有在线上出现异常时才暴露出来,导致问题追溯困难、数据对账混乱。

这正是我们今天深入探讨的起点。

正常流程(理想情况):

@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,事务就不会回滚。

示例:

@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 事务管理器会认为方法执行没有问题,从而不会触发回滚。为了确保事务能够回滚,捕获的异常应该被重新抛出,或者标记为需要回滚的异常。

示例:

@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 的事务管理基于动态代理实现,但如果同一类中的方法相互调用(自调用),事务管理就不会生效。因为自调用不会触发代理方法,事务管理也无法插入到执行过程中。

示例:

@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 方法,因此这些方法上的事务也会失效。

示例:

@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

示例:


@Service
public class LogService {
    
    @Transactional(propagation = Propagation.REQUIRES)
    public void log(String message) {
        logMapper.insert(new Log(message));
    }
}
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]