Spring Boot事务异常时保持行锁并解决任务重调度事务回滚问题
问题根因
当前逻辑存在天然矛盾:processJob方法捕获异常后,既执行了rescheduleJobExecution更新任务重调度信息,又在末尾抛出RuntimeException触发外层事务回滚,导致重调度更新被一同回滚,同时事务被标记为rollback-only触发异常。
可行解决方案
方案一:移除catch块末尾异常抛出(推荐,实现成本最低)
如果业务无需回滚worker.execute产生的所有数据修改,直接删除processJob catch块最后一行throw new RuntimeException(throwable);即可。
- 无需修改任何事务配置,外层
runInTransaction方法会正常执行完成,事务正常提交:- 任务重调度更新会被正常持久化,不会触发回滚
- 行锁会持有到事务提交才释放,全程不会出现多线程竞争问题
如果上层需要感知任务执行结果,可以通过返回布尔值、错误枚举等状态替代异常抛出即可。
方案二:拆分事务边界,适用于需要回滚业务操作的场景
如果必须回滚worker.execute产生的其他业务数据修改,仅保留任务重调度更新,可手动拆分事务实现:
- 第一段事务仅执行任务查询加锁,全程持有行锁直到所有逻辑执行完成
- 第二段独立事务执行业务逻辑,异常时直接回滚业务事务,不影响外层持锁事务
- 第三段独立事务执行重调度逻辑,提交完成后再提交外层持锁事务释放行锁
示例代码:
@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);
原方案失效原因说明
@Transactional(noRollbackFor = RuntimeException.class)未生效:因为processJob捕获异常后仍向外抛出RuntimeException,若未配置覆盖全部异常类型、或内层嵌套事务存在异常标记,都会导致配置失效。REQUIRES_NEW引发竞争:外层事务未提交时新开事务更新任务,外层事务回滚后直接释放行锁,此时重调度更新未完成,其他线程可直接查询到未修改的任务数据引发竞争。
内容的提问来源于stack exchange,提问作者Luiz
相关产品推荐
相关产品推荐

