Spring 事务传播机制与 REQUIRES_NEW

Spring 事务传播机制与 REQUIRES_NEW

一、核心概念

什么是事务传播行为

当一个事务方法调用另一个事务方法时,Spring 需要决定如何处理事务边界------是加入已有事务、新建独立事务,还是以无事务方式执行。这就是事务传播行为(Propagation Behavior)。

复制代码
方法 A(有事务)  →  调用方法 B(也声明了事务)
                         ↓
                  B 是加入 A 的事务?
                  还是新建自己的事务?
                  还是挂起 A 的事务?

为什么需要 REQUIRES_NEW

默认传播行为 REQUIRED 表示"有事务就加入,没有就新建"。但在某些场景下,方法 B 的成功/失败不应影响方法 A ,或者方法 B 需要独立提交 (即使 A 后续回滚),此时需要 REQUIRES_NEW

复制代码
REQUIRED(默认):            REQUIRES_NEW:
┌── 事务 A ──────────┐      ┌── 事务 A ──────────┐
│  操作1              │      │  操作1              │
│  ┌── 方法 B ────┐  │      │  ┌── 事务 B ────┐  │  ← 独立事务
│  │  操作2       │  │      │  │  操作2       │  │
│  │  操作3       │  │      │  │  操作3       │  │
│  └──────────────┘  │      │  └── 提交/回滚 ─┘  │
│  操作4              │      │  操作4              │
└── 全部提交或回滚 ───┘      └── 提交/回滚 ────────┘
                              ↑ 两个事务互相独立

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、Spring 全部传播行为

传播行为 含义 适用场景
REQUIRED 有事务加入,没有新建 默认选择,绝大多数业务方法
REQUIRES_NEW 挂起当前事务,新建独立事务 独立提交、错误日志记录、MQ 消费
NESTED 在当前事务中创建保存点(嵌套事务) 部分失败可回滚到保存点
SUPPORTS 有事务加入,没有就非事务执行 只读查询
NOT_SUPPORTED 挂起当前事务,非事务执行 长时间查询不占事务连接
MANDATORY 必须在已有事务中调用,否则抛异常 强制要求调用方提供事务
NEVER 必须在无事务中调用,否则抛异常 明确不允许事务的操作

三、REQUIRES_NEW 的行为详解

执行流程

复制代码
1. 调用方存在事务 T1
2. Spring 检测到 REQUIRES_NEW
3. 挂起事务 T1(释放 T1 的数据库连接回连接池?不,保持持有但暂停)
4. 新建事务 T2,获取新的数据库连接
5. 执行方法体
6. T2 提交或回滚
7. 恢复事务 T1,继续执行

隔离性

java 复制代码
@Transactional
public void methodA() {
    // 事务 T1
    orderRepo.save(order);  // T1 中写入数据

    methodB();  // T2 独立提交

    // 此时 T1 仍未提交
    // T2 已提交的数据对 T1 可见(取决于隔离级别)
    // 如果 T1 后续回滚,T2 的数据不受影响
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
    // 事务 T2:独立于 T1
    logRepo.save(errorLog);  // T2 提交后立即持久化
    // 即使 methodA 后续抛异常回滚 T1,这条日志也不会丢
}

关键特性

特性 说明
独立提交 T2 提交不依赖 T1 的最终结果
独立回滚 T2 回滚不影响 T1(除非异常被传播)
新连接 T2 使用独立的数据库连接
对 T1 不可见(默认) T1 未提交的数据在 T2 中不可见(READ_COMMITTED 隔离级别下)
死锁风险 T1 锁的行如果 T2 也要锁,会死锁(T1 等 T2 完成,T2 等 T1 释放锁)

四、典型应用场景

场景 1:MQ 消费方法独立事务

java 复制代码
/**
 * MQ 消费者调用 Service 层的处理方法.
 * 为什么需要 REQUIRES_NEW:
 * 1. MQ 框架调用消费方法时可能没有事务上下文
 * 2. 需要明确的事务边界控制提交/回滚
 * 3. 异常时只回滚本次业务操作,不影响消费框架
 */
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void processMessage(Long logId) {
    // 独立事务:成功则提交,失败则回滚
    TaskLog taskLog = taskLogRepo.findById(logId).orElseThrow();
    businessService.doWork(taskLog);
    taskLog.setStatus("Y");
    taskLogRepo.saveAndFlush(taskLog);
}

场景 2:错误日志独立保存

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void processOrder(Long orderId) {
    try {
        doBusinessLogic(orderId);
    } catch (Exception e) {
        // 业务失败,当前事务将回滚
        // 但错误日志必须持久化(不能跟着回滚)
        errorLogService.saveErrorLog(orderId, e.getMessage());
        throw e;
    }
}

// 错误日志服务
@Service
public class ErrorLogService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveErrorLog(Long bizId, String errorMsg) {
        // 独立事务:即使外层事务回滚,错误日志也能保存
        ErrorLog log = new ErrorLog();
        log.setBizId(bizId);
        log.setErrorMsg(errorMsg);
        log.setCreateTime(new Date());
        errorLogRepo.save(log);
    }
}

场景 3:部分操作需要立即可见

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void createOrderAndNotify(OrderDto dto) {
    // 主事务:创建订单(尚未提交)
    Order order = createOrder(dto);

    // 需要立即生成一个编号并让其他服务可查到
    String seqNo = sequenceService.generateAndPersist(order.getId());

    // 继续主流程...
    order.setSeqNo(seqNo);
    orderRepo.save(order);
}

@Service
public class SequenceService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public String generateAndPersist(Long orderId) {
        // 独立事务:立即提交,其他事务可以查到这个编号
        String seq = generateNextSeq();
        SeqRecord record = new SeqRecord(orderId, seq);
        seqRepo.saveAndFlush(record);
        return seq;
    }
}

场景 4:批量处理中的单条隔离

java 复制代码
@Service
public class BatchProcessor {

    @Resource
    private SingleItemProcessor singleItemProcessor;

    /**
     * 批量处理:每条记录独立事务.
     * 一条失败不影响其他记录。
     */
    public BatchResult processBatch(List<Long> itemIds) {
        BatchResult result = new BatchResult();
        for (Long itemId : itemIds) {
            try {
                singleItemProcessor.processOne(itemId);
                result.addSuccess(itemId);
            } catch (Exception e) {
                // 单条失败不中断批量
                result.addFailed(itemId, e.getMessage());
            }
        }
        return result;
    }
}

@Service
public class SingleItemProcessor {

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void processOne(Long itemId) {
        // 每条记录一个独立事务
        // 失败只回滚当前这条
        Item item = itemRepo.findById(itemId).orElseThrow();
        doProcess(item);
        item.setStatus("DONE");
        itemRepo.save(item);
    }
}

五、注意事项与陷阱

5.1 自调用失效问题

java 复制代码
@Service
public class OrderService {

    // ❌ 错误:同类内部调用,事务注解不生效
    @Transactional
    public void methodA() {
        this.methodB(); // 直接调用,不走代理,REQUIRES_NEW 失效!
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // 实际仍在 methodA 的事务中执行
    }
}

解决方案:

java 复制代码
// 方案1:注入自身代理
@Service
public class OrderService {

    @Resource
    private OrderService self; // 注入代理对象

    @Transactional
    public void methodA() {
        self.methodB(); // 通过代理调用,事务注解生效
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() { ... }
}

// 方案2:拆分到不同 Service(推荐)
@Service
public class OrderService {

    @Resource
    private OrderLogService orderLogService;

    @Transactional
    public void methodA() {
        orderLogService.methodB(); // 不同类,走代理
    }
}

@Service
public class OrderLogService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() { ... }
}

5.2 死锁风险

java 复制代码
@Transactional
public void methodA() {
    // T1 锁定了 order 表 id=1 的行
    Order order = orderRepo.findByIdForUpdate(1L);
    order.setStatus("PROCESSING");
    orderRepo.save(order); // 行锁未释放(T1 未提交)

    // 调用 REQUIRES_NEW 方法
    auditService.audit(1L); // T2 开始
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void audit(Long orderId) {
    // T2 也要锁 order 表 id=1 的行
    Order order = orderRepo.findByIdForUpdate(orderId); // 💀 死锁!
    // T2 等待 T1 释放行锁
    // T1 等待 T2 完成才能继续
}

规避方式:

java 复制代码
// 方案1:REQUIRES_NEW 方法不锁相同行
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void audit(Long orderId) {
    // 只读查询,不加锁
    Order order = orderRepo.findById(orderId).orElseThrow();
    // 写入审计表(不同的表)
    AuditLog audit = new AuditLog(orderId, "APPROVED");
    auditLogRepo.save(audit);
}

// 方案2:在 REQUIRES_NEW 调用前释放行锁(flush + 不再修改)
@Transactional
public void methodA() {
    Order order = orderRepo.findById(1L).orElseThrow();
    order.setStatus("PROCESSING");
    orderRepo.saveAndFlush(order); // flush 但不释放锁

    // 改为事务提交后再调用审计(避免死锁)
    // 或者审计方法操作不同的行/表
}

5.3 连接池耗尽

java 复制代码
// ❌ 危险:嵌套过深的 REQUIRES_NEW
@Transactional
public void level1() {
    level2(); // 新连接
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void level2() {
    level3(); // 又一个新连接
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void level3() {
    level4(); // 又一个新连接...
}

// 每层挂起的事务都持有一个连接
// 4 层嵌套 = 4 个数据库连接被同一个线程占用
// 并发量大时连接池迅速耗尽

规避方式:

  • 控制 REQUIRES_NEW 嵌套深度(建议不超过 2 层)
  • 尽量在最内层使用,不要级联嵌套

5.4 异常传播

java 复制代码
@Transactional
public void methodA() {
    try {
        newTxService.methodB(); // REQUIRES_NEW,内部抛异常
    } catch (Exception e) {
        // T2 已回滚
        // 这里 catch 住了,T1 不会回滚
        log.warn("methodB 失败,继续执行 methodA", e);
    }
    // T1 继续正常提交
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
    // T2 内部抛异常 → T2 回滚
    throw new RuntimeException("业务异常");
}

注意:如果 methodA 不 catch 异常,异常传播到 methodA 的事务边界时,T1 也会被标记为 rollback-only。


六、REQUIRES_NEW vs NESTED

维度 REQUIRES_NEW NESTED
事务关系 完全独立的新事务 外层事务的子事务(保存点)
数据库连接 使用新连接 共用外层连接
外层回滚影响 不影响内层(已提交) 内层也回滚(保存点失效)
内层回滚影响 不影响外层(catch 住) 回滚到保存点,外层可继续
内层可见外层数据 不可见(不同连接) 可见(同一连接)
JPA 支持 完全支持 取决于实现(不是所有 JPA 实现都支持)
java 复制代码
// NESTED:内层失败可回滚到保存点,外层继续
@Transactional
public void methodA() {
    saveOrder();       // 保存订单

    try {
        nestedMethod(); // NESTED 事务
    } catch (Exception e) {
        // 内层回滚到保存点
        // 外层 saveOrder() 的数据不受影响
    }

    saveLog();         // 继续执行
}

@Transactional(propagation = Propagation.NESTED)
public void nestedMethod() {
    // 在保存点内执行
    // 失败只回滚这部分
}

七、MQ 消费场景深入分析

为什么 MQ 消费方法适合 REQUIRES_NEW

java 复制代码
// MQ 消费者框架代码(简化)
@Component
public class MqConsumer {

    @RabbitListener(queues = "my-queue")
    public void onMessage(Long logId) {
        // 1. MQ 框架调用此方法时,通常没有外层事务
        // 2. 即使有框架层事务,业务也应该隔离

        try {
            // REQUIRES_NEW 保证明确的事务边界
            businessService.processInNewTx(logId);
            // 成功 → T2 已提交 → ACK 消息
        } catch (Exception e) {
            // 失败 → T2 已回滚 → NACK/重试
            log.warn("消费失败", e);
        }
    }
}

@Service
public class BusinessService {

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void processInNewTx(Long logId) {
        // 明确的独立事务:
        // - 成功:数据提交,状态更新为 Y
        // - 失败:数据回滚,状态保持 O/P
    }
}

错误日志为什么也要 REQUIRES_NEW

java 复制代码
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void processMessage(Long logId) {
    try {
        doBusinessLogic(logId);
        markSuccess(logId);
    } catch (Exception e) {
        // 问题:如果在当前事务中写错误日志,事务回滚后日志也丢了
        // 解决:错误日志写入方法也是 REQUIRES_NEW
        errorLogService.saveError(logId, e); // 独立事务,不随本事务回滚
        throw e; // 重新抛出,让本事务回滚
    }
}

与事务后置动作的配合

java 复制代码
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void processMessage(Long logId) {
    // 业务逻辑...
    doWork();

    // 事务提交后才发送 MQ(保证数据可见性)
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                // 此时 REQUIRES_NEW 的事务已提交
                // 新的消费者能读到本次写入的数据
                anotherMqSender.send(nextLogId);
            }
        });
}

八、完整示例:MQ 消费 + 独立事务 + 错误处理

java 复制代码
/**
 * MQ 消费者.
 * 职责:消息接收、锁控制、异常处理
 * 不含业务逻辑。
 */
@Component
@Slf4j
public class TaskMqConsumer {

    @Resource
    private TaskProcessService taskProcessService;
    @Resource
    private DistributedLockProvider lockProvider;

    @RabbitListener(queues = "${mq.queue.task-process}")
    public void consume(Long logId) {
        log.info("收到消息, logId={}", logId);

        String lockKey = "task:process:" + logId;
        DistributedLock lock = lockProvider.getLock(lockKey, 60, TimeUnit.SECONDS);
        if (!lock.tryLock(30, TimeUnit.SECONDS)) {
            log.warn("获取锁失败, logId={}", logId);
            return;
        }

        try {
            // 调用独立事务的处理方法
            taskProcessService.processInNewTransaction(logId);
        } catch (Exception e) {
            // 事务已回滚,消息可能重试
            log.warn("任务处理失败, logId={}", logId, e);
        } finally {
            lock.unlock();
        }
    }
}

/**
 * 任务处理 Service.
 * REQUIRES_NEW 保证每次消费都是独立事务。
 */
@Service
@Slf4j
public class TaskProcessService {

    @Resource
    private TaskLogRepository taskLogRepository;
    @Resource
    private ErrorLogService errorLogService;
    @Resource
    private TaskProcessor taskProcessor;
    @Resource
    private FollowUpMqSender followUpMqSender;

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void processInNewTransaction(Long logId) {
        // 1. 查询任务
        TaskLog taskLog = taskLogRepository.findById(logId).orElse(null);
        if (taskLog == null) {
            log.warn("任务不存在, logId={}", logId);
            return;
        }

        // 2. 幂等判断
        if ("Y".equals(taskLog.getStatus())) {
            return;
        }

        try {
            // 3. 执行业务
            taskProcessor.execute(taskLog);

            // 4. 标记成功
            taskLog.setStatus("Y");
            taskLog.setErrorMsg(null);
            taskLogRepository.saveAndFlush(taskLog);

            // 5. 事务提交后触发后续动作
            TransactionSynchronizationManager.registerSynchronization(
                new TransactionSynchronizationAdapter() {
                    @Override
                    public void afterCommit() {
                        followUpMqSender.send(taskLog.getFollowUpId());
                    }
                });

        } catch (Exception e) {
            log.warn("任务执行失败, logId={}", logId, e);
            // 6. 错误日志独立保存(不受本事务回滚影响)
            errorLogService.saveError(logId, e.getMessage());
            throw e; // 本事务回滚
        }
    }
}

/**
 * 错误日志服务.
 * 独立事务保证错误信息不丢失。
 */
@Service
public class ErrorLogService {

    @Resource
    private TaskLogRepository taskLogRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void saveError(Long logId, String errorMsg) {
        TaskLog taskLog = taskLogRepository.findById(logId).orElse(null);
        if (taskLog == null) return;
        taskLog.setStatus("P");
        taskLog.setRetryCount(taskLog.getRetryCount() + 1);
        taskLog.setErrorMsg(errorMsg != null ? errorMsg.substring(0, Math.min(errorMsg.length(), 500)) : null);
        taskLogRepository.saveAndFlush(taskLog);
    }
}

九、决策指南

什么时候用 REQUIRES_NEW

  • ✅ MQ 消费者的核心处理方法
  • ✅ 错误/审计日志写入(不能随业务事务回滚)
  • ✅ 序列号/编号生成(需要立即提交避免重复)
  • ✅ 批量操作中每条记录的独立处理
  • ✅ 与外部系统交互前的状态锁定(提交后才调外部)

什么时候不该用 REQUIRES_NEW

  • ❌ 普通的 Service 层方法调用(用默认 REQUIRED)
  • ❌ 需要与调用方共享事务上下文的操作
  • ❌ 深层嵌套调用(超过 2 层)
  • ❌ 可能与外层事务锁相同数据行的操作(死锁)
  • ❌ 纯查询方法(用 SUPPORTS 或 readOnly=true)
相关推荐
BerryS3N1 小时前
Java 后端转型大模型:Demo 能跑不等于能上线
java·人工智能·python·java后端·spring ai·langchain4j·大模型转型
心机之蛙qee1 小时前
Docker Compose 多容器编排
java·docker·容器
YOU OU1 小时前
Redis分布式锁
数据库·redis·分布式
小草儿7992 小时前
GBase 8s CLOB → VARCHAR(32739) 数据迁移方案文档
数据库
秋田君2 小时前
QT_QColorDialog颜色对话框
java·数据库·qt
涛声依旧393162 小时前
运维项目:InnoAI SQL 助手
运维·数据库·sql
万悉科技2 小时前
时尚品牌GEO技术选型指南:LLM原生优化与AI记忆构建全解析
数据库·人工智能·搜索引擎
lb29172 小时前
在window上安装orcale数据库流程(快速,简洁)
数据库
IT小盘2 小时前
09-使用Redis保存大模型多轮对话上下文
数据库·redis·缓存