redis 的 lpush + brpop 是最稳的 php 队列组合,因其阻塞式消费避免轮询空耗、不丢任务,配合 json 编码、合理超时、失败重试与进程守护可支撑高可靠生产服务。

Redis 的 LPUSH + BRPOP 是最稳的 PHP 队列组合
PHP 本身没有原生队列机制,得靠外部存储。Redis 的 list 结构配合阻塞弹出,是生产环境最常用、最不容易丢任务的方案。它不像 PUSH/POP 那样轮询空耗 CPU,也不像 BLPOP 在多消费者时可能漏消息(注意:BRPOP 和 BLPOP 行为一致,但方向相反;实际用哪个取决于你 push 的方向,保持一致就行)。
常见错误现象:BRPOP 返回 null 或超时,其实是 key 不存在或 timeout 设太小;消费者进程意外退出,没重试逻辑,导致任务卡住。
实操建议:
-
LPUSH写入任务时,务必用 JSON 编码:json_encode(['job' => 'send_email', 'data' => ['to' => 'a@b.c']]),避免特殊字符破坏结构 -
BRPOP的 timeout 建议设为0(永久阻塞)或至少30,别用1——频繁唤醒会把 Redis 打满 - 消费者必须用
try/catch包住整个处理逻辑,失败后用RPUSH把原始消息塞回队尾,或写进失败重试队列 - 不要在
BRPOP后再做DEL或EXPIRE:消息已出队,key 自动变空,删不删没区别;加过期反而干扰监控
PHP Worker 进程必须手动管理生命周期,不能靠 php -f worker.php 一跑了事
没人帮你 reload、捕获信号、清理僵尸进程。一个没守护的 worker 跑着跑着就挂了,任务就停了。
使用场景:你不是在写 demo,是要跑一周不掉线的发短信服务或日志归档任务。
实操建议:
- 启动前加
pcntl_signal(SIGTERM, function() { exit(0); });,让kill能优雅退出 - 用
pcntl_fork()做简单守护即可,别一上来就上 Supervisor——先验证逻辑正确性 - 每次处理完任务后加
usleep(10000)(10ms),防止单个 worker 把 CPU 占死,尤其在无任务时 - 记录 PID 到文件(如
/tmp/queue_worker.pid),方便 kill 旧进程;但别用file_put_contents直接覆盖——并发启动时可能写乱
Redis::lLen() 不是监控队列长度的好方法
它返回的是当前 list 长度,但高并发下这个数字刚读出来就可能失效。更糟的是,有些 Redis 版本在 key 不存在时返回 false 而不是 0,直接用 if ($len > 100) 会报 warning。
性能影响:频繁调用 lLen() 对 Redis 是无效压力,尤其当队列空的时候,纯属白忙活。
实操建议:
- 监控用
INFO queue或 Redis 自带的redis-cli info keyspace查db0:keys=100,expires=0,avg_ttl=0类信息,更准 - 真要代码里判断积压,改用
BRPOP $queue 0.1尝试弹一个,成功说明有货,失败说明基本空了——顺便还省一次 round-trip - 如果非要用长度,先
EXISTS $queue,再lLen,且必须判断返回值类型:is_int($len) && $len > 0
任务失败重试必须带指数退避,别用固定间隔
网络抖动、下游接口限流、DB 锁表……这些临时故障,立刻重试大概率再失败,还可能雪崩。
容易踩的坑:sleep(1) 硬等,或者把失败任务 LPUSH 回原队列——等于给所有失败任务“一键复活”,瞬间压垮下游。
实操建议:
- 重试队列单独建一个,比如
retry:send_email:3(3 表示第 3 次重试),用EXPIRE设 TTL,比如60 * pow(2, $retry_count) - 消费时用
KEYS retry:*扫描过期 key 不现实——改用 Sorted Set 存时间戳:ZADD retry_queue (time()+delay) json_task,再用ZRANGEBYSCORE拉待执行的 - 最多重试 5 次,第 5 次失败后写进 MySQL 的
failed_jobs表,人工介入——别指望自动恢复所有异常
真正难的不是怎么推到 Redis,是怎么让任务在崩溃、重启、网络分区后依然可追溯、可补偿。很多团队卡在这一步,不是不会写 LPUSH,是没想清楚“失败”到底算什么状态。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











