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

Spring JPA长事务链@Retryable部分重试失效解决方法

Spring长事务下部分逻辑重试实现方案

问题根因

你的@Retryable不生效、无法保留A/B/C执行结果的核心原因有两个:

  • 事务传播配置错误:RetryService类上添加了类级别@Transactional,默认传播级别为REQUIRED,会直接加入外层TblDimensionService开启的长事务。当D方法抛出乐观锁异常时,整个长事务会被直接标记为rollback-only,不仅A/B/C的执行状态会被回滚,后续重试也会因为事务已失效无法正常执行。
  • 代理顺序与异常触发节点问题:如果@Retryable和@Transactional加在同一方法上,默认事务切面优先级高于重试切面,乐观锁异常通常在事务提交flush阶段才会抛出,此时异常先被事务切面捕获处理,重试切面无法捕获到异常触发重试;就算触发重试,也运行在同一个已标记回滚的事务内,没有任何意义。

实现方案

要实现「A/B/C状态保留、仅重试D方法」的需求,不需要手写循环try-catch,基于Spring原生注解即可实现,核心逻辑是让D方法运行在完全独立的新事务中,与外层长事务隔离,具体步骤如下:

  1. 移除RetryService类上的类级别@Transactional,避免所有方法默认加入外层长事务。
  2. 拆分D方法逻辑:将重试逻辑和实际持久化逻辑拆分为两个方法,重试方法不加事务注解,保证异常能被重试切面捕获;持久化方法指定事务传播级别为REQUIRES_NEW,执行时会自动挂起外层长事务,新开独立事务执行,该事务的提交/回滚完全不影响外层事务状态。
  3. 修正乐观锁更新逻辑:独立事务中不能直接使用外层事务查询出的实体(跨持久化上下文会报错),必须在新事务中重新查询最新版本的实体再做修改。
  4. 启动类添加@EnableRetry注解,开启Spring Retry自动代理。

修正后的代码示例

RetryService 修正版

@Service
public class RetryService {
    @Autowired
    private TblDimensionRepository dimensionRepository;
    // 注入自身代理,避免自调用导致注解失效
    @Autowired
    private RetryService self;

    /**
     * 重试外层逻辑,不加事务,保证并发异常能被重试切面捕获
     */
    @Retryable(
            value = {ObjectOptimisticLockingFailureException.class, ConcurrencyFailureException.class},
            maxAttempts = 3, // 最大重试次数可按需调整
            backoff = @Backoff(delay = 100, multiplier = 2) // 重试退避策略可按需调整
    )
    public TblDimension updateEntity(TblDimension dimension, TblDimensionDTO dimensionDTO) throws InterruptedException {
        Thread.sleep(3000);
        return self.doUpdateEntity(dimension, dimensionDTO);
    }

    /**
     * 独立事务逻辑:挂起外层长事务,新开事务执行更新,提交/回滚不影响外层
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public TblDimension doUpdateEntity(TblDimension dimension, TblDimensionDTO dimensionDTO) {
        Thread.sleep(3000);
        // 必须在新事务中重新查询最新版本的实体,禁止直接使用外层传入的持久化实体
        TblDimension latestDimension = dimensionRepository.findById(dimension.getId())
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "维度数据不存在"));
        latestDimension.setHeight(latestDimension.getHeight() + 1);
        // 方法执行完直接提交独立事务,乐观锁异常会在此处抛出,被外层重试切面捕获
        return dimensionRepository.save(latestDimension);
    }

    /**
     * 重试耗尽后的降级逻辑,可按需实现
     */
    @Recover
    public TblDimension recoverUpdate(ConcurrencyFailureException e, TblDimension dimension, TblDimensionDTO dimensionDTO) {
        throw new ResponseStatusException(HttpStatus.CONFLICT, "数据并发更新冲突,请稍后重试");
    }
}

TblDimensionService 无需额外修改

类上的@Transactional保留即可,所有在调用retryService.updateEntity()之前执行的逻辑(参数校验、初始实体查询等)都会在外层长事务中执行,状态会被完整保留,不会因为D方法的重试被回滚。

执行流程说明

修正后的执行链路如下:

  • 外层长事务开启,执行参数校验、初始实体查询等A/B/C逻辑
  • 调用updateEntity方法时,重试切面生效
  • 每次重试调用doUpdateEntity时,外层长事务被挂起,开启全新独立事务
  • 独立事务中查询最新实体、修改、保存、提交:如果提交抛出乐观锁异常,独立事务回滚,异常抛给重试切面触发下一次重试;如果提交成功,独立事务结束,外层长事务恢复
  • D方法重试成功后,继续执行后续E方法逻辑,最终外层长事务提交,所有A/B/C/D/E的执行结果全部持久化

注意事项

  • 禁止将@Retryable和@Transactional(propagation = REQUIRES_NEW)加在同一个方法上,代理顺序不可控会导致重试失效,拆分方法通过代理调用是最稳妥的实现方式。
  • 独立事务中必须重新查询最新版本的数据,不能直接操作外层事务查询出的托管实体,否则会抛出跨持久化上下文操作异常。
  • 重试次数、重试间隔不要设置过大,避免大量请求阻塞导致服务雪崩。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 07:27:18