用 Redis 实现延迟队列可靠吗?从 ZSet 轮询到消息不丢的边界
用 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。关键业务要以数据库状态为准,并准备幂等和补偿闭环。