hyperf 3.0 redis 驱动异步队列无自动 ack,任务崩溃丢失或重复消费需手动 release/retry 控制重试,并依赖业务幂等、失败队列兜底及监控告警。

Hyperf 3.0 的异步消息队列(async-queue)默认不提供自动 ACK 机制,消费进程崩溃时未手动确认,任务会重新入队,导致重复消费。这不是 Bug,而是 Redis 驱动的「无状态重投」设计决定的——它依赖你主动控制任务生命周期。
Redis 驱动没有内置 ACK,靠 release/retry 手动模拟
Hyperf 的 RedisDriver 没有类似 RabbitMQ 的 ack/nack 协议,也没有 Kafka 的 offset 提交。它只做两件事:从 List 弹出任务、执行 handle()。一旦进程异常退出(如 PHP Fatal Error、OOM、被 kill),当前任务就“消失”了——既没成功完成,也没失败记录,Redis 中该任务已出队,但没留下任何痕迹。
结果就是:下一轮消费者拉取时,下一个任务顶上,而这个“半途失踪”的任务再也找不回来,**看似丢失;但如果任务在执行中途崩溃前已修改了外部状态(如扣了库存但没发短信),再由另一个进程重试,就变成重复消费。**
所以关键不是“等 ACK”,而是用 $this->release() 或 $this->retry() 显式把任务送回队列,并附带延迟时间,形成可控重试:
-
$this->release(60):延迟 60 秒后重新入队(进同一 channel 的最前端) -
$this->retry(120):延迟 120 秒后重试(进同一 channel 的最前端,语义更明确) - 两者都要求你在
handle()中 必须 try/catch,不能让异常穿透出去
进程崩溃 ≠ 任务失败,要区分三类退出场景
不是所有崩溃都需要重试。需结合退出原因判断是否应重放任务:
- PHP 致命错误(Fatal Error):无法 catch,进程直接终止 → 任务已丢失,无法重试。预防方式是加 PHP 启动检查(如依赖类是否存在)、避免动态调用未定义方法
-
未捕获异常(Exception/Throwable 穿透 handle):async-queue 默认记录日志并移入
hyperf:queue:failed,不会重试 → 必须包 try/catch,否则等于放弃任务 - 进程被系统杀死(OOM、kill -9、Supervisor 强制 stop):此时 handle() 中的逻辑中断,无任何回调 → 任务已出队、无痕丢失 → 唯一补救是业务层幂等(如订单号唯一索引、状态机校验)
真正防重复,靠的是业务幂等 + 可控重试 + 失败兜底
光靠队列机制无法 100% 规避重复,必须分层防御:
-
幂等写操作:比如更新订单状态,用
WHERE status = 'created';发短信用「手机号+事件类型+业务单号」组合唯一索引,插入前先查重 -
失败队列人工干预:定期扫描
redis-cli lrange hyperf:queue:failed 0 -1,对可恢复任务调用RedisDriver::push($job, 'default')重放 -
监控消费延迟与失败率:在
handle()开头打点,在结尾记录耗时;用 Prometheus 抓取指标,当失败率突增或平均耗时翻倍,说明下游不稳定,需降级或限流
补充:AMQP/Kafka 驱动行为不同,但仍有风险
如果你切换到 AMQP(RabbitMQ)或 Kafka 驱动,它们原生支持 manual ack,Hyperf 会自动在 handle() 成功后发送 ack。但要注意:
- RabbitMQ 若未开启
channel->confirmSelect()或消费者异常退出未 nack,Broker 仍可能重发 - Kafka 若 consumer 进程崩溃且未提交 offset,rebalance 后新实例会从上次 offset 继续读 —— 若 offset 提交策略是 auto 且间隔长,也会重复
- 所以即便换驱动,幂等仍是底线,不可省略











