PHP无原生延迟队列,需借助Redis(推荐zset,以时间戳为score)或MySQL实现;Redis方案更精准轻量,MySQL适合小流量场景;避免用BRPOPLPUSH模拟延迟。

PHP 里没有原生 delay queue,得自己搭
PHP 本身不提供延迟任务调度能力,sleep() 或 usleep() 只能阻塞当前请求,没法做到“10 分钟后发邮件”这种异步延迟。真要实现,必须借助外部存储(如 Redis、MySQL)+ 定时轮询或常驻进程。
用 Redis 的 zset 做延迟队列最靠谱
Redis 的有序集合(zset)天然适合:把执行时间戳设为 score,任务内容作为 member,用 zrangebyscore 拉取已到期任务。比用 list + expire 更精确,也比轮询数据库轻量。
实操建议:
- 插入任务时,用
ZADD delay_queue <timestamp><json_task></json_task></timestamp>,注意<timestamp></timestamp>是绝对时间戳(秒级,不是毫秒) - 消费者进程每秒执行一次
ZRANGEBYSCORE delay_queue -inf <now_timestamp></now_timestamp>,然后用ZREM删除已取出的任务(避免重复消费) - 取出后立刻处理;若失败,可重新
ZADD回去(加个重试次数字段控制死循环) - 别用
BRPOPLPUSH类命令模拟延迟——它不支持时间维度,只能靠客户端 sleep,不可靠
用 MySQL 实现延迟队列要注意锁和性能
适合小流量、已有 MySQL 且不想引入 Redis 的场景,但容易卡在查询和更新上。
立即学习“PHP免费学习笔记(深入)”;
常见错误现象:
- 用
SELECT ... WHERE exec_at 取任务,但没加 <code>FOR UPDATE,导致多个 worker 同时取到同一条 - 更新状态时用
UPDATE ... SET status='running' WHERE id = ? AND status = 'pending',但没检查影响行数,失败了也不报 - 没给
exec_at字段建索引,轮询变全表扫描,10 万条记录就明显变慢
关键参数差异:
- 推荐字段设计:
id,payload(JSON),exec_at(DATETIME 或 INT),status(enum: pending/running/done/failed),retry_count - 轮询间隔别设太短(比如 100ms),MySQL 频繁唤醒反而加重负载;2–5 秒较稳妥
不要用 PHP-FPM 或 Web 请求直接触发延迟逻辑
用户点击“1 小时后提醒”,你不能在 HTTP 请求里写 sleep(3600) 再发通知——请求超时、PHP 超时、Nginx 超时会层层中断,任务直接消失。
真正可用的路径只有两条:
- Web 请求只负责把任务写入 Redis/MySQL,然后立刻返回成功;后台有独立的 CLI 进程(如
php worker.php)持续拉取执行 - 用系统级定时器(
cron)每分钟跑一次脚本,但它精度只有 1 分钟,不适合秒级延迟需求
容易被忽略的一点:CLI 进程挂了没人重启。至少得配 supervisord 或 systemd 看护,不然队列就停摆了。











