Workerman 默认消费会丢消息,因其集成的php-amqplib默认启用no_ack=true,消息一推送即被RabbitMQ删除,而Worker崩溃或PHP致命错误时无法补ACK;必须手动在basic_consume回调中严格控制ack()/nack()时机,并配合QoS、心跳、死信等机制构建可靠性。

Workerman 本身不提供 ACK 确认重发机制,必须靠你手动在 basic_consume 回调里控制 ack() / nack() 的时机,并配合 RabbitMQ 的预取、重入队、死信等能力来构建可靠性。光靠 Workerman 启动几个 Worker 进程,消息照样会丢。
为什么 Workerman 默认消费会丢消息?
RabbitMQ 的 PHP 客户端(php-amqplib)在 basic_consume 中默认启用 no_ack=true —— 消息一推过来,RabbitMQ 就立刻从内存/磁盘队列里删掉。哪怕你的业务逻辑刚执行到一半,Worker 进程被 kill 或 PHP Fatal Error 崩溃,消息就永远消失了。
-
no_ack=true是“fire-and-forget”模式,和可靠性完全对立 - Workerman 的常驻进程模型放大了这个问题:没有请求生命周期兜底,出错就是静默丢失
- 别指望
onClose或onError补 ACK —— 那些回调根本收不到未进入onMessage的消息
怎么写一个真正可靠的 onMessage 回调?
核心是:只在业务逻辑 100% 成功后才调用 ack();失败时必须 nack($requeue=true) 并确保连接不中断。示例结构如下:
$channel->basic_consume('order_events', '', false, false, false, false, function ($msg) use ($channel) {
$delivery_tag = $msg->getDeliveryTag();
try {
// ✅ 所有耗时操作加超时:DB 查询设 PDO::ATTR_TIMEOUT,HTTP 调用用 Guzzle 异步
// ✅ 关键状态先落库(如 order_status = 'processing'),再发通知
// ✅ 处理完成后再 delete() 或 update() 标记为 done
processOrderEvent(json_decode($msg->body, true));
$channel->ack($delivery_tag);
} catch (Throwable $e) {
// ❌ 不要 log 后 silence 掉 —— 必须 nack 让消息重回队列头或进死信
$channel->nack($delivery_tag, false, true); // requeue=true
// 可选:记录失败日志 + 报警,但不要阻塞当前回调
}
});
- 第 3 个参数
false表示不批量 ACK,避免一条失败拖垮整批 -
nack(..., true)是关键:让消息重新入队,而不是丢弃;若需限重试次数,得靠死信交换机(DLX)+ TTL 配合 - 别在 try 块里做
sleep()或同步 curl —— 会卡住整个 Worker,导致后续消息无法及时 ACK
预取数量(QoS)和心跳配置不到位,ACK 机制直接失效
即使代码写了 ack(),如果没配 basic_qos 和心跳,RabbitMQ 会把大量消息提前推给 Worker,结果 Worker 内存爆掉、连接断开、消息全卡在 unacked 状态,队列就堵死了。
- 务必在
onWorkerStart初始化后调用:$channel->basic_qos(0, 1, false)—— 限制每个 Channel 最多 1 条未 ACK 消息 - 连接层必须开启心跳:
$connection->setHeartbeat(30),并在主循环中定期调用$connection->writeHeartbeat() - 捕获
AMQPConnectionClosedException,在onError里重建连接和 channel,否则断连后不再收新消息 - Supervisor 或 systemd 管理 Worker 时,要配
RestartSec=5,但不能依赖重启来补 ACK —— 未处理完的消息不会自动恢复
最容易被忽略的点是:业务幂等性比 ACK 更重要。哪怕你 ACK 写得滴水不漏,如果订单创建接口被重复触发两次,系统照样出错。所有消费逻辑必须基于唯一消息 ID 做去重或状态校验,否则重发机制反而成了事故放大器。

















