PHP队列任务处理核心在于选对存储介质、防丢机制和防卡死策略:Redis需设BRPOP超时并捕获异常,MySQL须用SELECT...FOR UPDATE SKIP LOCKED加事务,RabbitMQ必须手动ACK并配置死信队列。

PHP 实现队列任务处理,核心不在于“能不能做”,而在于“选哪种存储介质 + 怎么防丢 + 怎么不卡死”。用错方案,轻则任务堆积、重复执行,重则数据库锁表、Redis 内存爆满。
用 SplQueue 做内存队列只适合单次脚本内调度
它不是为长期运行设计的——进程结束,队列就清空。常见误用是把它当“全局队列”塞进 Web 请求里,结果每次 HTTP 请求都新建一个 SplQueue,根本没共享。
-
enqueue()和dequeue()是线程安全的,但仅限当前 PHP 进程内;Web 服务器(如 Apache/FPM)每个请求都是独立进程,无法跨请求传递 - 适合场景:CLI 脚本中批量处理一组已知任务,比如导出 1000 条记录前先
enqueue所有 ID,再循环dequeue执行 - 别在
$_SESSION或static属性里存SplQueue实例——序列化时会丢失内部指针,dequeue可能返回null
Redis LPUSH/BRPOP 队列必须设超时且捕获异常
很多人写消费者脚本只写 $redis->brPop(['task_queue'], 0),看似“一直等”,实则一断网或 Redis 重启,整个进程就卡死不动,supervisor 也拉不起来。
- 超时参数不能为
0,建议设为10(秒),避免永久阻塞;超时后应主动sleep(1)再重试,防止 CPU 空转 -
brPop返回false或抛出RedisException时,必须try/catch并记录日志,否则进程静默退出 - 消息体建议用
json_encode(['job' => 'send_email', 'data' => [...]]),别直接塞对象——反序列化时类不存在会报致命错误 - 生产者端用
lPush没问题,但高并发下若需优先级,得换zAdd+ 时间戳做有序队列,BRPOP就不适用了
MySQL 队列表必须用 SELECT ... FOR UPDATE SKIP LOCKED
这是 MySQL 8.0+ 才支持的语法,老版本只能靠乐观锁或额外状态字段兜底。不用它,多消费者同时 SELECT status = 0 会查到同一条记录,导致任务被重复执行。
立即学习“PHP免费学习笔记(深入)”;
- SQL 必须包在事务里:
BEGIN; SELECT id FROM queue WHERE status = 0 ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED; UPDATE queue SET status = 1 WHERE id = ?; COMMIT; - 如果
UPDATE影响行数为0,说明已被其他进程抢走,应立即continue下一轮查询,别 sleep -
status字段务必建索引,否则WHERE status = 0全表扫描,10 万条记录时消费者轮询延迟可达秒级 - 别把大字段(如 HTML 模板)直接存在
payload里——BLOB 类型拖慢整个表的 SELECT 效率
RabbitMQ 消费者必须手动 ack 否则消息会堆积
默认情况下,basic_consume 接收到消息后,RabbitMQ 就认为已投递成功。如果 PHP 脚本中途崩溃或 exit,那条消息就永远消失了——除非你显式关掉自动确认。
- 创建消费者时必须传
['no_ack' => false],并在业务逻辑执行成功后调用$channel->basic_ack($msg->getDeliveryTag()) - 没
ack的消息会堆积在 RabbitMQ 管理界面的 “Unacked” 列,超过内存阈值会触发流控,整个队列卡住 - 死信队列(DLX)要提前声明:消费者
basic_nack三次后,消息才进 DLX;别指望靠 PHP 层重试——网络抖动时 ACK 失败,消息可能被重复投递 - 连接中断时,
$channel和$connection对象失效,必须重建,不能复用旧实例
真正难的不是“怎么把任务塞进去”,而是“怎么确保它只被执行一次、失败可追溯、扩容时不冲突”。Redis 快但丢数据,MySQL 稳但慢,RabbitMQ 强大但运维成本高——选型时得看你的任务丢了会不会被老板打电话骂醒。



















