🔍 为什么我的日志在事务回滚后也没了?——揭秘 REQUIRES_NEW 的陷阱

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

你正在开发一个用户注册功能:先保存用户信息,再记录一条操作日志。为了保证数据一致性,你在主方法上加了 @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 事务只会回滚 RuntimeExceptionError。如果在方法内抛出了其他类型的异常(例如 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));
    }
}
相关推荐
间彧4 小时前
Spring Boot 2.6+版本为什么默认禁止循环引用?
后端
=>>漫反射=>>4 小时前
单元测试 vs Main方法调试:何时使用哪种方式?
java·spring boot·单元测试
初圣魔门首席弟子4 小时前
c++ bug 记录(merge函数调用时错误地传入了vector对象而非迭代器。)
java·c++·bug
间彧4 小时前
在实际项目中,如何通过代码规范和工具来系统性预防NPE?
后端
ZhengEnCi4 小时前
@Parameter 注解技术解析-从 API 文档生成到接口描述清晰的 SpringBoot 利器
java·spring boot
数据库那些事儿4 小时前
Qoder + ADB Supabase :5分钟GET超火AI手办生图APP
数据库·后端
用户68545375977694 小时前
从"打电话"到"装修智能家居":让你的AI从话痨变成行动派!
后端
AresXue5 小时前
2025最新Java性能优化建议 应用 数据库 机器 网络
java
跟着珅聪学java5 小时前
spring boot 整合 activiti 教程
android·java·spring