← 全部文章

Redisson 延迟队列与分布式锁迁移记录

为提高系统调度精度、解耦并增强架构可靠性,本次进行彻底的 Redisson 改造。

本文目录 7 节

背景说明

原系统内存在两套冗余的比赛状态更新机制:

  1. 基于 Spring TaskScheduler 的内存精确调度(附带重启时的全量重载恢复)。
  2. 基于 ContestStatusScheduler 每 30 秒轮询数据库的全表扫描兜底。

同时原有 ContestSchedulerService 中的 3 处分布式锁(用于控制并发修改比赛状态)都使用了手写的 SETNX 模式,且解锁方式非原子操作,存在安全隐患。

为提高系统调度精度、解耦并增强架构可靠性,本次进行彻底的 Redisson 改造。

具体变动点

1. 依赖变更

  • 在 pom.xml 中引入 redisson-spring-boot-starter。目前直接复用已有的 Spring Redis(spring.data.redis)配置即可,开箱即用。

2. 核心调度机制重构 (延迟队列)

  • 取消轮询兜底:彻底删除了 ContestStatusScheduler 及其在 ContestServiceImpl 和拦截层中的 updateContestStatuses 轮询方法。
  • 引入 RDelayedQueue:在 ContestSchedulerService 的 scheduleContest 方法中,使用 Redisson 的 RDelayedQueue。比赛每次创建/修改时,会比对 startTime 和 endTime,计算延迟毫秒数后投递 START:{id} 和 END:{id} 消息。
  • 并发消费者:新建 ContestDelayedQueueConsumer 后台轮询组件。通过原生的 RBlockingQueue.take() 接管所有过期事件处理,并路由执行后续操作。
  • 安全拦截校验(取消任务特性):因原生消息不易摘除旧记录,采用“消费时乐观比对时间”策略:消费者被唤醒时去数据库查询,如果 startTime 被延后导致在当前还不应该发生,直接无视该条过期消息(新的有效消息已经在修改时被投递入队)。

3. 分布式锁升级 (RLock)

  • 原子性增强:在 ContestSchedulerService#startContest 等核心操作中,将原手写的 setIfAbsent / UUID token 逻辑全部移除,改为 redissonClient.getLock()。
  • 防误解锁机制:直接使用 lock.isHeldByCurrentThread()。
  • 看门狗续期应用:由于 endContest 需要处理复杂的统分排行榜固化更新,采用了 lock.tryLock(0, -1, TimeUnit.SECONDS) 的机制开启 Redisson Watchdog,确保长时间运行的排行榜计算不会导致分布式锁自动过期剥夺。

4. 冗余重启流程优化

  • 原 ContestRecoveryInitializer 每次服务重启都要轮询全表的 NOT_STARTED / RUNNING 注入调度器。因为现在基于 Redis 持久化存储调度队列,这一块调度器重现逻辑被精简。当前只需关注由于未处理完即宕机造成的 ENDED 落库失败异常恢复。

适用场景

本次更新让 OJ 系统在高压环境或多实例部署下可以完全依赖 Redis 进行强精准状态控制,免除了 CPU 密集型的全表扫行为,同时化解了 SETNX 在极端场景下的隐患。

评论