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方法运行在完全独立的新事务中,与外层长事务隔离,具体步骤如下:
- 移除
RetryService类上的类级别@Transactional,避免所有方法默认加入外层长事务。 - 拆分D方法逻辑:将重试逻辑和实际持久化逻辑拆分为两个方法,重试方法不加事务注解,保证异常能被重试切面捕获;持久化方法指定事务传播级别为
REQUIRES_NEW,执行时会自动挂起外层长事务,新开独立事务执行,该事务的提交/回滚完全不影响外层事务状态。 - 修正乐观锁更新逻辑:独立事务中不能直接使用外层事务查询出的实体(跨持久化上下文会报错),必须在新事务中重新查询最新版本的实体再做修改。
- 启动类添加
@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
相关产品推荐
相关产品推荐

