用Redis ZSet实现PHP延迟队列的核心是将任务执行时间戳作为score、任务内容作为member,利用ZSet有序性与范围查询能力实现“到点取任务”,适用于订单超时关闭等中小规模场景。

用 Redis 的 ZSet 实现 PHP 延迟队列,核心思路是:把任务的执行时间戳作为 score,任务内容作为 member,利用 ZSet 有序性和范围查询能力实现“到点取任务”。它轻量、高效,适合中小规模延迟场景(如订单超时关闭、消息重试、定时通知)。
一、数据结构设计:用 score 存时间,member 存任务
ZSet 天然按 score 升序排列,正好符合“越早该执行的任务 score 越小”的逻辑。推荐使用 Unix 时间戳(秒级或毫秒级)作为 score,避免浮点精度或时区问题。
- key 名建议带业务前缀,比如 delay_queue:order_timeout
- member 推荐用唯一标识 + 序列化数据,例如:order_123456:{"order_id":123456,"action":"close"},方便去重和调试
- score 必须是整数,PHP 中统一用 time() 或 microtime(true)*1000 向上取整(毫秒级更精准)
二、入队:zAdd 写入带时间戳的任务
调用 zAdd 把任务塞进 ZSet,score 是计划执行时间(不是当前时间)。
// 示例:30秒后关闭订单
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$job = json_encode(['order_id' => 123456, 'action' => 'close']);
$executeAt = time() + 30; // 30秒后执行
$member = 'order_123456:' . $job;
$redis->zAdd('delay_queue:order_timeout', $executeAt, $member);
注意:如果同一 member 多次 zAdd,score 会更新,自动覆盖旧时间——这可用于“刷新延迟”,比如用户付款后取消超时关闭任务(只需 zRem 删除即可)。
立即学习“PHP免费学习笔记(深入)”;
三、出队:zRangeByScore + zRem 拿出并移除已到期任务
消费者进程(如 CLI 脚本或守护进程)定期执行以下操作:
- 用 zRangeByScore key -inf now LIMIT 0 10 拉取最多 10 条 score ≤ 当前时间的任务
- 对每条任务,先 zRem 删除,再执行业务逻辑(防止重复消费)
- 若执行失败,可重新 zAdd(设置新 score),实现失败重试
$now = time();
$jobs = $redis->zRangeByScore('delay_queue:order_timeout', '-inf', $now, ['limit' => [0, 10]]);
foreach ($jobs as $member) {
// 先删除再处理,保证原子性(可用 Lua 脚本进一步强化)
if ($redis->zRem('delay_queue:order_timeout', $member)) {
$data = explode(':', $member, 2);
$payload = json_decode($data[1], true);
handleOrderTimeout($payload); // 你的业务处理函数
}
}
四、注意事项与优化建议
实际用起来有几个关键点容易踩坑:
- 避免轮询过密:sleep(1) 比 while(true) usleep(10000) 更稳;高并发下可改用 Redis 的 BLPOP 配合 ZSet,或加个哨兵 key 触发检查
- 防止任务堆积:定期用 zCard 监控队列长度,结合 zCount key -inf +inf 查看总量
- 确保幂等:ZSet 本身不防重入,业务逻辑必须支持重复执行(如用 Redis 锁或 DB 唯一约束校验状态)
- 清理过期任务:长期运行需考虑已取消/无效任务,可定时 zRemRangeByScore key -inf (now-3600) 清理1小时前的陈旧任务(括号表示开区间)
不复杂但容易忽略细节。用好 ZSet 的有序+去重+范围能力,就能搭出稳定可靠的延迟队列。



















