hyperf的reserved队列并非独立队列,而是consumerprocess从{channel}:waiting弹出任务后、未ack前临时lpush到{channel}:reserved的暂存快照;进程崩溃会导致任务滞留于此,需通过async-queue:reload命令迁移至waiting或failed队列,并手动del清理。

要准确理解Hyperf异步队列中reserved队列的用途、生成时机和清理逻辑,必须直击Redis底层存储结构与ConsumerProcess消费循环源码,不能仅依赖配置文档或表层行为描述。
reserved队列不是独立队列,而是Redis List的临时标记
Hyperf的reserved队列【根本不存在独立的reserved key】。它只是ConsumerProcess在pop出一个job后、尚未ack前,将该job数据原样写入一个以{channel}:reserved为名的Redis List中——本质是未确认任务的暂存快照。
执行pop操作时,ConsumerProcess调用RedisDriver::pop(),内部先从{channel}:waiting弹出元素,再立即LPUSH {channel}:reserved $jobData,两步原子性不保证,但框架默认不开启事务。
若此时进程崩溃或网络中断,该job就滞留在:reserved里,既不在waiting也不在failed,成为“幽灵任务”。
reserved任务何时被ack或fail?看handle()执行结果
ConsumerProcess拿到job实例后,反射调用$job->handle():
→ 成功执行且无异常:触发RedisDriver::ack($data),从{channel}:reserved中LREM 1 $jobData,并清空job对象引用;
→ 抛出可捕获异常(非致命错误):进入重试逻辑,先RedisDriver::fail($data)把job推入{channel}:failed,再从:reserved中删除;
→ 发生PHP致命错误(如Segmentation Fault、内存耗尽)或进程被kill -9::reserved中的job永远不会被ack或fail,除非人工干预。
reload命令如何恢复卡死的reserved任务?
Hyperf提供php bin/hyperf.php async-queue:reload命令,其核心是调用RedisDriver::reload()方法:
第一步:对每个配置的queue channel,执行LRANGE {channel}:reserved 0 -1拉取全部待恢复job;
第二步:遍历每个job数据,反序列化为JobInterface实例,检查$job->maxAttempts是否已达上限;
第三步:未超限的job重新PUSH {channel}:waiting,已超限的直接PUSH {channel}:failed;
【注意】reload不会清空:reserved,只做迁移。迁移完成后需手动DEL {channel}:reserved,否则下次reload仍会重复加载。
如何定位某个job卡在reserved里?
方法一:直接查Redis
redis-cli -c -n 0 LRANGE "async-queue:reserved" 0 5
方法二:启用debug日志
在config/autoload/logging.php中为hyperf.async-queue通道设置level为debug,ConsumerProcess会在每次pop/ack/fail时输出key与job ID;
方法三:监听Redis键事件(需提前开启notify-keyspace-events Ex)
redis-cli --csv psubscribe '__keyevent@0__:expired' '__keyevent@0__:del'
当reserved中job因TTL过期被删(Hyperf未设TTL,此法无效),或你手动DEL时可捕获——但Hyperf默认不设TTL,所以该方法实际不可用。











