ThinkPHP队列消费必须闭环实践:任务类需按TP6(fire)或TP8(handle)规范独立实现,禁用控制器直连DB;仅php think queue:work为生产命令,须配--sleep=3与--max-jobs=500,并校准retry_after防重复消费。

ThinkPHP队列消费不是“写个类+跑个命令”就能上线的,它是一套必须闭环的工程实践:任务定义、驱动配置、进程启动、失败兜底、监控对齐,缺一不可。直接在控制器里调用 Db::insert() 或 Cache::delete() 并推入队列,等于埋雷——重试失效、状态失联、并发错乱。
任务类必须独立且接口匹配 TP6/TP8
TP6 和 TP8 的任务类签名不兼容,混用会导致静默跳过执行,消费者进程无报错但 handle() 或 fire() 根本不进。你不能把 TP6 的类复制到 TP8 项目里就完事。
- TP6 要求实现
fire(Job $job, $data),类无需继承基类,但必须接受两个参数 - TP8 必须用
handle(Job $job, $data),且类需显式use think\queue\Job;或继承think\queue\Job - 构造函数传参可以,但不要在
fire()/handle()里依赖$this->uid等未序列化属性——对象反序列化时它们会丢失,务必把关键数据塞进$data或用__serialize()显式控制 - 别在方法里
throw new Exception()来“触发重试”:框架只在未调用$job->delete()且没抛未捕获异常时才重试;抛异常却没 catch,worker 进程直接崩溃退出
php think queue:work 是唯一生产可用命令
php think queue:listen 在 TP6+ 已被移除或未注册,强行运行报 Command "queue:listen" is not defined。即使旧版能跑,它也是轮询模式,每次拉任务都 reload 整个框架,1 小时后内存可能 OOM。
- 生产环境只认
php think queue:work,它单进程常驻,复用实例,支持信号控制 - 必须加
--sleep=3(空闲时休眠秒数),避免 Redis 频繁连接;设太小(如--sleep=0.1)会打爆 Redis 连接数 - 必须加
--max-jobs=500,防止单进程长期运行导致内存泄漏,到数自动退出,交由 Supervisor 拉起新进程 - 禁止加
--daemon:TP6.3+ 已废弃,加了会报错或降级为普通模式,失去信号响应能力 - 如果执行后立即退出,先检查
config/queue.php中'driver'是否拼错(比如写成'redis'实际应为小写,但配置键名是'driver',不是'type')
Redis 驱动下 retry_after 和 block_for 必须对齐
Redis 驱动不靠异常次数判失败,而是靠 retry_after 时间戳。任务执行超时未删 job,就会被其他 worker 重复捞取,造成重复消费。
立即学习“PHP免费学习笔记(深入)”;
-
retry_after默认 60 秒,若你的任务要导出大 Excel(耗时 120 秒),必须在config/queue.php的connections.redis里显式设为'retry_after' => 150 -
block_for控制 BRPOP 阻塞等待时长,建议和--sleep一致(如都设为 3),否则空轮询或响应延迟 - failed_jobs 表没记录?不是配置错,而是 Redis 驱动默认不写 failed_jobs——它只标记“过期”,真正失败(抛异常)才落库;若需完整失败日志,得自己监听
Queue::failing()事件手动记录 - 任务里有
curl_exec()或sleep()?必须设超时,否则整个 worker 卡住,后续所有任务阻塞
数据库操作必须带前置校验 + 显式事务边界
队列任务里用 Db::transaction() 不等于安全。进程被 kill(如 Supervisor 内存超限重启)时,事务不会回滚,DB 状态和队列状态立刻脱节。
- 所有写操作前,先查业务状态并加行锁:
SELECT balance FROM user WHERE id = ? FOR UPDATE,确认是否已处理过 - 更新成功后,再调用
$job->delete();失败则不删,靠框架重试(前提是没抛未捕获异常) - 避免在任务里直接用
Db::table('user')->where(...)->update(...),必须封装进事务块,并捕获所有异常,Db::rollback()后返回,不删 job - 投递任务时指定的
--queue=default,必须和Queue::push(..., $job, 'default')中的队列名完全一致,大小写敏感,否则消费端收不到
最易被忽略的是:任务类代码改了,queue:work 进程不会自动重载——它复用的是启动时加载的类实例。生产环境必须靠 Supervisor 的 autorestart=true + --max-jobs 主动退出来触发新进程加载新代码,而不是指望“热更新”。



















