ZSet 延时队列是什么
延时队列要解决的是「某件事过一段时间再执行」:订单超时关闭、优惠券到期提醒、失败任务延后重试。用 ZSet 实现的方式很直接,把任务的执行时间戳当作分数,任务标识当作成员,写入即投递;消费端用 ZRANGEBYSCORE 取出分数小于当前时间的成员,也就是所有已到期的任务。取出与删除必须在同一个 Lua 脚本内完成,否则多消费者会重复领到同一条任务。
基本信息
| 成员 | 任务标识,如订单号或任务 ID,必须唯一 |
|---|---|
| 分数 | 到期时间的 Unix 时间戳,秒或毫秒要全系统统一 |
| 投递 | ZADD delay <执行时间戳> <任务ID> |
| 领取 | ZRANGEBYSCORE delay -inf <当前时间> LIMIT 0 10 后立即 ZREM |
| 原子性 | 取与删必须放在一段 Lua 脚本里,单条命令无法保证 |
典型使用场景
- 订单超时未支付自动关闭:下单时写入到期时间,到点后被消费端取出执行关单
- 失败任务退避重试:按指数退避算出下次执行时间,重新 ZADD 回队列
- 定时提醒与通知:消息本身不带调度能力,靠延时队列把发送时间前置
- 本地消息表的兜底触发:业务落库后投递延时任务,到点校验是否已完成
- 限流后的延迟放行:被限流的请求延后若干毫秒重新排队
投递与领取
投递与领取
redis-cli ZADD delay 1730020000 task:1001 # 到点时间戳作为分数
redis-cli ZADD delay 1730023600 task:1002
redis-cli ZRANGEBYSCORE delay -inf 1730021000 LIMIT 0 10 # 取出已到期任务
redis-cli ZREM delay task:1001 # 领取后移除(必须与上一步原子)
redis-cli ZCARD delay # 队列积压量,用于监控
redis-cli ZRANGEBYSCORE delay -inf +inf LIMIT 0 1 WITHSCORES # 看最早的任务到点时间Lua 原子领取
Lua 原子领取
local jobs = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], 'LIMIT', 0, 10)
for i, id in ipairs(jobs) do
redis.call('ZREM', KEYS[1], id)
end
return jobs可靠性的取舍
| 顺序 | 风险 | 缓解手段 |
|---|---|---|
| 先删除后执行 | 进程崩溃导致任务永久丢失 | 用待确认集合或补偿扫描兜底 |
| 先执行后删除 | 崩溃或超时导致任务被重复执行 | 业务逻辑必须幂等 |
| Lua 内取删 | 已取出但尚未执行时进程崩溃 | 配合失败重投表与告警 |
| 单 key 集中 | 任务量大时成为热点 | 按业务维度拆成多个 delay key |
轮询间隔决定实际精度:到期任务只会在下一次扫描时才被领取,扫描间隔 1 秒就意味着最坏 1 秒的额外延迟;间隔过密则空转请求增多。多数业务按秒级精度设计,用固定间隔扫描比给每个任务建定时器更简单可靠。
时间戳单位必须全系统统一:秒与毫秒混用会让任务在错误的时间点被领取,可能提前数十年执行。推荐统一使用毫秒并把单位写进接口约定与代码注释,避免跨团队协作时出错。