← 返回题目列表

延迟队列有哪些实现方式?

高频 中等 第 13 / 26 题 更新于 2026/10/01
消息队列延迟队列时间轮定时任务
学习 AI 实战项目

简化版

延迟队列让消息在到期后才可消费,常见实现有 Broker 原生定时消息、固定延迟级别、多级时间轮、Redis ZSet 扫描和数据库定时扫描。选型要看最大延迟、精度、吞吐、取消修改、持久化和到期风暴,而不是只看能否“晚点投递”。

详细版

消息应保存 eventId、deliverAt、业务 key 和版本;服务端以自己的时间判断到期,生产者时间只作为输入。到期搬运与投递至少一次,消费者必须幂等;取消不能只删内存任务,要用状态或版本阻止已在途消息生效。

时间轮适合大量定时任务但精度受 tick 影响,ZSet 适合中小规模但轮询与热 key 明显,数据库便于事务一致但扫描成本高。无论哪种方式,都要监控到期延迟、待触发量、最老任务、失败重试和时钟异常。

完整版教学

一、延迟队列保证的是什么

它通常保证“不早于 deliverAt 可见”,不保证恰好在该毫秒执行。Broker 调度、消费排队和下游限流都会产生晚到;面试时要区分触发精度与业务完成时间。

记忆钩子:延迟队列管“最早什么时候开始”,不是预约一个绝对准点的完成时刻。

二、四类实现对比

方案精度扩展性主要边界
Broker 原生中高高产品能力差异
时间轮tick 级高重启恢复
Redis ZSet轮询级中热点与持久化
数据库扫描秒/分钟级中低索引与扫描压力

数据库方案适合任务与业务记录同事务创建;高吞吐长延迟更适合原生 Broker 或分层时间轮。

三、时间轮怎样工作

时间轴被分成槽,任务按剩余延迟放入槽与轮次;指针每个 tick 前进,到槽时递减轮次或投递。多级时间轮用秒、分、时多层降低大范围槽数量。

tick=1s, wheel=60 slots
delay=125s -> slot=(now+5)%60, rounds=2

在不考虑停顿和排队时,tick=1 秒带来的量化误差通常小于一个 tick;再叠加调度抖动与投递排队,仍不能宣称毫秒准点。

四、ZSet 与数据库扫描

ZSet 以 deliverAt 为 score,工作线程原子领取 score<=now 的成员。读取后再删除会被多实例重复领取,应使用 Lua、claim 状态或搬入处理队列。数据库用 (status, deliver_at) 索引分批 FOR UPDATE SKIP LOCKED,避免全表扫描。

两者都要限制单批数量;到期任务 100 万条同时触发时,立即全部推送只会把问题转成下游雪崩。

五、取消、修改与重复

取消可能与到期投递并发。可靠做法是任务带 version,消费者执行前查询或验证最新状态;取消把版本推进或状态改为 CANCELED,旧消息即使到达也不生效。修改时间可创建新版本并废弃旧版本。

eventId 唯一约束处理重复投递,但“同一业务的新版本”不能被旧幂等记录错误拦截,需把版本纳入处理条件。

六、容量与故障恢复

每秒到期 5000 条、单实例处理 1000/s,至少需 5 个实例,实际还要留失败与峰值余量。系统停机 10 分钟会集中释放过期任务,应按速率回放并允许业务判断是否已经失去意义。

持久化任务、调度水位和领取状态;进程恢复后重扫未确认任务。监控 schedule lag、触发 QPS、积压字节、重试和过期丢弃。

跨时区任务应先把业务时区规则解析成绝对时间,并记录原始时区与规则版本;夏令时切换不能只靠字符串时间临时换算。

七、常见误区与追问

  • 误区:延迟消息会准点执行。 它只保证不提前,执行仍受排队影响。
  • 误区:ZSet 取出后删除两步足够。 多实例并发会重复领取或丢任务。
  • 误区:取消就是删除消息。 已在途消息必须靠状态或版本失效。
  • 误区:全部到期后立即投递最快。 到期风暴会压垮消费者和下游。
  • 追问:时间轮精度由什么决定? tick、调度抖动和投递队列共同决定。
  • 追问:如何支持超长延迟? 多级时间轮、分层存储或 Broker 原生能力。
  • 追问:停机后过期任务怎么办? 持久化重扫,并按业务过期规则限速恢复。

八、加强记忆

延迟队列按“存时间—等到期—原子领取—至少一次投递—幂等执行”理解。选型比较精度、范围、吞吐与持久化;取消靠版本,到期风暴靠限速,恢复靠持久水位。能延迟不等于准点,更不等于恰好执行一次。