php队列任务处理核心是选对存储介质(redis/mysql/rabbitmq)并保障防丢、防卡死:redis需brpop设超时并捕获异常,mysql须select for update skip locked加事务,rabbitmq必须手动ack并配死信队列。

array_push 和 array_shift 不是队列的终点,而是起点——它们只适合单进程、无并发、不持久、低吞吐的本地调试场景。真正在业务中跑起来的队列,必须解决「任务不丢」「消费不断」「状态可查」「失败可溯」这四个硬需求。
用 SplQueue 写本地队列逻辑,但别上线
它比纯数组快,支持迭代器和 count(),API 更规范:enqueue() / dequeue() / bottom()。但本质仍是内存结构,PHP 请求结束就清空,无法跨请求共享。你写个 CLI 脚本跑 SplQueue 模拟消费流程可以,但直接塞进 Web 请求里做“队列”,等于没队列。
常见错误现象:new SplQueue() 放在控制器里,每次 HTTP 请求都新建一个空队列,前端点十次,后台只处理最后一次的数据。
- 仅用于单元测试或算法练习,验证 FIFO 逻辑是否正确
- 不要依赖它做任何需要状态延续的操作(比如任务重试、进度追踪)
- 若需多进程共享,必须配合外部存储(Redis/DB),
SplQueue只能当临时容器
redis 的 lPush + brPop 是最常用落地组合
它不是“最可靠”的方案,但它是 PHP 工程师上线第一个异步任务时最可能选对的方案:快、轻、够用。关键不是命令本身,而是怎么避免掉坑。
容易踩的坑:
-
brPop超时设太短(如 1 秒),会导致消费者频繁唤醒又休眠,CPU 白耗;设太长(如 300 秒),故障恢复慢,新任务积压感知迟钝 —— 建议从 10 秒起步,按实际平均处理时长 × 2 动态调 - 没做
json_decode()容错,一串格式错误的 JSON 进来,brPop拿到后直接fatal error,整个消费者进程崩掉 —— 必须包try/catch,失败消息记日志并continue - 生产者用
lPush,消费者用rPop,结果顺序反了 —— 记住口诀:“左进右出”才是 FIFO,lPush+rPop或rPush+lPop都行,但别混用
MySQL 队列表必须带 FOR UPDATE SKIP LOCKED
不用这个,高并发下两条消费者同时 SELECT ... WHERE status = 0 LIMIT 1,极大概率取到同一条记录,然后都去 UPDATE,最终只有一个成功,另一个默默跳过 —— 任务就丢了。
MySQL 8.0+ 才支持 SKIP LOCKED,5.7 及更早版本只能靠乐观锁(加 version 字段 + WHERE version = ?)或应用层加分布式锁(如 Redis setnx),复杂度陡增。
- 建表时
status字段务必加索引,否则SELECT ... WHERE status = 0全表扫描,性能随数据量线性恶化 - 消费者脚本不能只查一次就退出,得做成循环 +
sleep(1),否则轮询间隔拉长,任务延迟不可控 -
UPDATE ... SET status = 1后必须立刻COMMIT,否则事务卡住,后续 SELECT 被阻塞
RabbitMQ 的 ack 不手动发,等于没开
很多人装完 RabbitMQ、连上 php-amqplib、跑通 hello world 就以为搞定了。其实真正决定可靠性的,是消费者拿到消息后那一句 $channel->basic_ack($delivery_info['delivery_tag'])。
默认是 auto-ack 模式,消息一发过去就删,消费者崩溃或处理中途失败,消息永远丢失。切到 manual-ack 后,必须在业务逻辑彻底执行完毕(包括 DB 写入、API 调用成功)后再发 ack,否则任务会重回队列或进死信。
- 别在 try 块开头就发 ack,要在 finally 之前、且确认所有副作用完成之后
- 遇到异常别静默吞掉,要明确调用
basic_nack并设置requeue = false,推入死信队列供人工干预 - 消费者进程启动时,务必声明队列为
durable = true,否则服务重启后队列消失,积压消息全丢
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











