ThinkPHP队列需显式配置retry_after(须大于任务最大耗时)和failed处理机制(Redis驱动默认不记录失败,须监听JobFailed事件或用--tries参数),禁用--daemon而改用supervisor守护进程,并注意Queue::later()的时间戳冲突问题。

ThinkPHP 的队列不是“配好就能跑”的黑盒,直接 Queue::push() 后用 php think queue:work 启动,线上大概率出现任务卡住、重复执行、失败不记录、延迟不准等问题。真正能落地的异步处理,得盯住几个关键配置点和运行方式。
retry_after 必须显式设,且要大于实际任务耗时
很多人以为只要 Redis 连上了,队列就自动可靠了。其实不然:retry_after 是 Redis 驱动判断任务是否“超时失败”的唯一依据。它不是重试间隔,而是任务最长允许执行时间。
- 如果发邮件实际可能耗时 70 秒(含 SMTP 连接、附件读取),
retry_after却设为 60,那任务还没执行完就被标记失败,立刻重入队列——造成无限循环 - 该值必须在
config/queue.php的connections.redis.retry_after中明确写死,不能依赖默认值(ThinkPHP 默认是 60) - 建议设为预估最大耗时 × 1.5,比如 90 或 120,并配合监控观察真实执行时长再微调
failed_jobs 表不能靠“默认有”,Redis 驱动默认不存失败记录
Database 驱动会自动建 failed_jobs 表并写入失败任务;但 Redis 驱动完全不保存失败状态——这意味着你根本不知道哪个任务崩了、为什么崩。
- 生产环境必须手动监听
JobFailed事件,在事件回调里把$event->job和$event->exception写进日志或数据库 - 或者改用
--tries=3 --delay=10启动消费者:php think queue:work redis --tries=3 --delay=10,让框架自动重试并延后,但依然不记录原始错误堆栈 - 别指望
think-queue自带的failed命令能查到 Redis 失败任务——它只对 database 驱动有效
不要用 --daemon,改用 supervisor 或 systemd 管理 worker 进程
php think queue:work --daemon 看似省资源,实则埋雷:PHP CLI 模式下长期运行会累积内存泄漏,尤其加载了大量类或用了 ORM 关联查询后,几小时就 OOM。
立即学习“PHP免费学习笔记(深入)”;
-
--daemon本质是让单个 PHP 进程循环消费,不重启,不释放内存 - 正确做法是去掉
--daemon,用supervisor配置autostart=true+autorestart=true,每次任务执行完进程退出,由 supervisor 拉起新实例 - 同时设置
startsecs=0和stopwaitsecs=10,避免频繁重启误判,也给任务留出正常退出时间
延迟任务用 Queue::later() 要防时间回拨和 score 冲突
Queue::later(300, new SendEmail(), $data) 底层靠 Redis ZSET 实现,score 是时间戳。这在 NTP 时间校正或高并发场景下极易出错。
- 若服务器时间被 NTP 回拨(比如从 15:02:03 调回 15:02:01),ZSET 中已到时间的任务可能永远不被取出
- 同一秒内投递多个任务,score 相同,Redis 不保证 ZRANGE 返回顺序,任务执行顺序不可控
- 解决方法:不用裸时间戳,改用
time() . '.' . uniqid()或自增seq拼成唯一 score,例如1718637481.12345
队列真正的难点不在“怎么推”,而在“怎么稳收、怎么知错、怎么续命”。配置写错一行,监控缺一个环节,线上就可能静默丢任务。尤其是 retry_after 和 failed 处理,最容易被跳过,但恰恰是故障定位的第一道防线。



















