跳过导航

用 Redis 实现延迟队列可靠吗?从 ZSet 轮询到消息不丢的边界

约 5 分钟...次浏览
专栏Redis 深度专题第 6 篇

用 ZSet 做延迟队列非常直观:score 保存执行时间,成员保存任务。它适合提醒、缓存刷新等可重试任务,但“到点能查到”不等于可靠队列。领取、确认、宕机恢复、重复消费和持久化策略必须一起设计。

1. 基本模型

ZADD delay:orders 1760000000000 task-123
ZRANGE delay:orders -inf <now> BYSCORE LIMIT 0 100

消费者查询到期任务后执行并 ZREM。若多个消费者同时查询,会拿到同一批任务;若先删除再执行,进程崩溃会丢任务;若先执行再删除,则可能重复执行。

这就是消息系统的核心事实:在一般故障模型下,很难同时免费获得“不丢、不重且高可用”。通常选择至少一次投递,再靠业务幂等消除重复影响。

2. 原子领取任务

使用 Lua 把“查询到期 + 从待处理移除 + 放入处理中”合并为原子操作:

local items = redis.call('ZRANGE', KEYS[1], '-inf', ARGV[1], 'BYSCORE', 'LIMIT', 0, ARGV[2])
for _, item in ipairs(items) do
  redis.call('ZREM', KEYS[1], item)
  redis.call('ZADD', KEYS[2], ARGV[3], item)
end
return items

KEYS[1] 是 delay,KEYS[2] 是 processing,ARGV[3] 是可见性超时时间。成功处理后从 processing 删除;消费者宕机后,回收器把超过可见性期限的任务重新投递。

Redis Cluster 中 Lua 涉及的 Key 必须位于同一 slot,可使用 hash tag:

queue:{orders}:delay
queue:{orders}:processing

脚本批次必须有上限,避免长时间阻塞主线程。

3. 任务载荷不要全塞进 ZSet

成员最好使用任务 ID,任务体单独存储:

ZSet: score -> taskId
Hash/String: taskId -> payload, attempts, status

这样便于更新重试次数、状态和大载荷。任务体也要设置清理策略,但不能早于队列生命周期。超大 payload 更适合对象存储或数据库,Redis 只保存索引。

4. 幂等是必需品

消费者完成业务但在 ACK 前崩溃,任务会再次出现。业务必须以任务 ID 或业务请求 ID 去重:

INSERT IGNORE INTO task_execution(task_id, status)
VALUES (?, 'DONE');

这里以 MySQL 且 task_id 具有唯一键为例;应用必须检查受影响行数,只有插入成功的一方才能继续执行业务。不要用 INSERT IGNORE 包住未经约束的复杂写入,因为它还可能把部分数据问题降为 warning。更严谨的做法是在同一数据库事务中完成“插入去重记录 + 业务状态转换”,并针对重复键错误做显式分支。单独在 Redis 设置去重 Key 仍存在 Redis 与数据库双写窗口。

5. 轮询与准时性

固定每秒轮询会带来最多约一秒延迟,缩短间隔又增加空查询。可以动态读取最近 score 并阻塞等待,新增更早任务时再唤醒;但实现复杂度迅速接近专业队列。

还要处理:

  • Redis 与应用时钟偏差;
  • 同一毫秒大量任务造成突刺;
  • 消费者并发和下游限流;
  • 重试使用指数退避与随机抖动;
  • 超过最大次数进入死信集合并告警。

6. Redis 持久化与高可用边界

若只启用 RDB,故障时可能丢失最近快照后的任务;AOF 不同 fsync 策略也存在不同窗口。主从异步复制和故障转移同样可能丢掉尚未复制的数据。必须把 Redis 的持久化承诺纳入队列 SLA,而不是看到 AOF 就假定零丢失。

强可靠订单超时关闭等任务,通常应让数据库保存权威状态,延迟消息只是触发器;消费时重新检查订单状态。即便消息丢失,也可由定时扫描补偿。

7. 什么时候换专业 MQ

出现以下需求时优先选择支持延迟/定时能力的消息系统:

  • 大规模分区消费和消费组管理;
  • 明确 ACK、重投、死信和堆积观测;
  • 跨机房复制和更高持久化等级;
  • 长时间延迟、海量任务;
  • 严格顺序或完善审计。

8. 检查清单

  • 领取和状态迁移是否原子?
  • 是否有 processing 可见性超时和宕机回收?
  • 消费是否幂等,失败是否退避并进入死信?
  • Cluster 多 Key 是否同 slot?
  • 是否理解 RDB/AOF 和主从切换的丢失窗口?
  • 是否有数据库扫描等最终补偿?

Redis 延迟队列可以可靠到“工程可接受”,但可靠程度取决于完整协议,而非一个 ZSet。关键业务要以数据库状态为准,并准备幂等和补偿闭环。

分享:
文章作者:狼码纪
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。文章可能参考了其他优秀文章,如有侵权请联系删除。