Redis List 做 PHP 异步队列因简单、够用、不依赖额外服务;LPUSH/BRPOP 天然适配生产者-消费者模型,延迟低、原子性强,适合中小流量场景如发邮件、生成缩略图等。

为什么用 Redis List 做 PHP 异步任务队列?
因为简单、够用、不依赖额外服务。PHP 本身不支持真正的多线程异步,fork 或 pcntl 在 Web SAPI(如 FPM)下受限甚至不可用;而 Redis 的 LPUSH/BRPOP 天然适配“生产者-消费者”模型,延迟低、原子性强,比文件轮询或数据库轮询更可靠。
注意:这不是消息中间件替代方案,适合中小流量、无严格事务/重试要求的场景(比如发邮件、生成缩略图、清理日志)。
怎么用 LPUSH + BRPOP 实现基础队列?
核心是两个命令配合:LPUSH 入队(生产者),BRPOP 阻塞出队(消费者)。BRPOP 会等待指定 key 有数据才返回,避免空轮询消耗 CPU。
- 生产者端(Web 请求中):
redis->lPush('queue:mail', json_encode(['to' => 'user@example.com', 'subject' => 'Hi'])) - 消费者端(常驻 CLI 脚本):
$job = $redis->brPop(['queue:mail'], 0)—— 第二个参数0表示无限等待 - 拿到的是数组:
['queue:mail', '{"to":"..."}'],第二项才是原始 job 数据,需json_decode
别漏掉 json_encode 和 json_decode:Redis 只存字符串,结构化数据必须序列化;否则取出来是乱码或 null。
立即学习“PHP免费学习笔记(深入)”;
消费者进程怎么避免挂掉后丢任务?
BRPOP 是原子操作,但消费失败(如网络异常、脚本崩溃)会导致 job 永久丢失 —— 因为它已从 list 中弹出。解决方案是「先取后删」+「手动 ACK」:
- 改用
LPOP非阻塞取一个 job,立刻SET job:{id} {data} EX 300(带过期时间缓存) - 处理成功后
DEL job:{id};失败则让它自动过期,后续巡检脚本可重新入队 - 或者用 Redis Stream(
XADD/XREADGROUP),但需要 Redis ≥ 5.0,且 PHP 客户端需确认支持xreadgroup
用 LPOP + 过期键模拟 ACK,比引入 RPOPLPUSH + 单独 fail queue 更轻量,也更容易调试。
实际跑起来要注意哪些坑?
CLI 消费者脚本不是写完就能一直跑,环境和权限经常卡住:
- 确保 PHP CLI 版本与 Web 一致(
php -vvsphpinfo()),尤其 Redis 扩展版本差异可能导致brPop返回格式不同 - 不要在 Web 环境里直接
exec('php worker.php &')启动 —— FPM 进程结束时子进程大概率被杀,要用systemd或supervisord管理 -
BRPOP的 timeout 设为0时,若 Redis 连接中断,脚本会卡死;建议加set_time_limit(0)和连接重试逻辑 - 大量 job 积压时,单个消费者吞吐瓶颈明显,横向扩展靠起多个
php worker.php实例即可,Redis List 天然支持并发读
真正难的不是代码怎么写,而是让那个后台脚本稳稳当当地活过一次部署、一次 Redis 重启、一次磁盘满——这些细节没兜住,队列就变成单点故障源。



















