You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot事务异常时保持行锁并解决任务重调度事务回滚问题

问题根因

当前逻辑存在天然矛盾:processJob方法捕获异常后,既执行了rescheduleJobExecution更新任务重调度信息,又在末尾抛出RuntimeException触发外层事务回滚,导致重调度更新被一同回滚,同时事务被标记为rollback-only触发异常。

可行解决方案

方案一:移除catch块末尾异常抛出(推荐,实现成本最低)

如果业务无需回滚worker.execute产生的所有数据修改,直接删除processJob catch块最后一行throw new RuntimeException(throwable);即可。

  • 无需修改任何事务配置,外层runInTransaction方法会正常执行完成,事务正常提交:
    1. 任务重调度更新会被正常持久化,不会触发回滚
    2. 行锁会持有到事务提交才释放,全程不会出现多线程竞争问题
      如果上层需要感知任务执行结果,可以通过返回布尔值、错误枚举等状态替代异常抛出即可。

方案二:拆分事务边界,适用于需要回滚业务操作的场景

如果必须回滚worker.execute产生的其他业务数据修改,仅保留任务重调度更新,可手动拆分事务实现:

  1. 第一段事务仅执行任务查询加锁,全程持有行锁直到所有逻辑执行完成
  2. 第二段独立事务执行业务逻辑,异常时直接回滚业务事务,不影响外层持锁事务
  3. 第三段独立事务执行重调度逻辑,提交完成后再提交外层持锁事务释放行锁
    示例代码:
@Autowired
private PlatformTransactionManager transactionManager;

public void runInTransaction() {
    final Instant now = Now.getInstantUtc();
    // 持锁事务:仅用于查询任务加行锁,全程不释放
    DefaultTransactionDefinition selectTxDef = new DefaultTransactionDefinition();
    TransactionStatus selectStatus = transactionManager.getTransaction(selectTxDef);
    Optional<Job> jobForProcessing;
    try {
        jobForProcessing = dao.getJobForProcessing(now);
    } catch (Exception e) {
        transactionManager.rollback(selectStatus);
        throw e;
    }

    if (jobForProcessing.isEmpty()) {
        transactionManager.commit(selectStatus);
        Log.d(TAG, "No more jobs, stopping thread %s.".formatted(Thread.currentThread().getName()));
        return;
    }
    Job job = jobForProcessing.get();

    // 业务事务:独立执行任务逻辑
    DefaultTransactionDefinition businessTxDef = new DefaultTransactionDefinition();
    TransactionStatus businessStatus = transactionManager.getTransaction(businessTxDef);
    try {
        worker.execute(job);
        jobService.markJobAsProcessed(job);
        transactionManager.commit(businessStatus);
        transactionManager.commit(selectStatus);
    } catch (Throwable throwable) {
        transactionManager.rollback(businessStatus);
        Log.e(TAG, "Error to process job: " + job.getId(), throwable);
        // 重调度事务:持锁状态下执行重调度更新
        DefaultTransactionDefinition rescheduleTxDef = new DefaultTransactionDefinition();
        TransactionStatus rescheduleStatus = transactionManager.getTransaction(rescheduleTxDef);
        try {
            jobService.rescheduleJobExecution(job, throwable);
            transactionManager.commit(rescheduleStatus);
        } catch (Exception e) {
            transactionManager.rollback(rescheduleStatus);
            throw e;
        }
        // 重调度完成后再释放行锁
        transactionManager.commit(selectStatus);
    }
}

必做优化:补充任务查询过滤条件

现有getJobForProcessing查询未过滤nextRetryAt字段,会导致重调度后未到重试时间的任务仍被线程查询到,需修改查询逻辑:

@NotNull
@Query("""
        select ee from EventEntity ee
        where ee.processed = false and (ee.nextRetryAt is null or ee.nextRetryAt <= :now)
        order by ee.createdAt""")
@Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHints({@QueryHint(name = "javax.persistence.lock.timeout", value = LockOptions.SKIP_LOCKED + "")})
Optional<EventEntity> getJobForProcessing(@NotNull final Instant now, @NotNull final Pageable pageable);

原方案失效原因说明

  1. @Transactional(noRollbackFor = RuntimeException.class)未生效:因为processJob捕获异常后仍向外抛出RuntimeException,若未配置覆盖全部异常类型、或内层嵌套事务存在异常标记,都会导致配置失效。
  2. REQUIRES_NEW引发竞争:外层事务未提交时新开事务更新任务,外层事务回滚后直接释放行锁,此时重调度更新未完成,其他线程可直接查询到未修改的任务数据引发竞争。

内容的提问来源于stack exchange,提问作者Luiz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.24 02:54:04