必须禁用 autoack 并手动控制 ack/nack:设 autoack=false,用 defer+recover 捕获 panic 后调用 nack(requeue=true/false),业务成功后再 ack(),配合幂等、dlx 和 qos 防消息丢失。
panic 发生时 autoack=true 会直接丢消息
go 客户端(如 streadway/amqp)默认开启 autoack=true,一旦消费者 goroutine panic,连接断开前 rabbitmq 已把消息标记为“已投递”,后续不会重发——消息实质丢失。这不是网络问题,是语义错误:rabbitmq 认为你“已处理完”,而你根本没来得及执行业务逻辑。
必须禁用 autoAck 并手动控制 ACK/NACK 流程
核心原则:只有业务逻辑完整执行完毕、无 panic、且显式调用 msg.Ack(false) 后,才允许 RabbitMQ 删除消息。中间任何环节出错(包括 panic),都要确保消息能重回队列或进入死信路径。
- 声明 consumer 时传入
autoAck=false:ch.Consume(..., false, ...) - 每个
amqp.Delivery必须包裹在defer+recover中,捕获 panic 并触发msg.Nack(false, true)(不重新排队)或msg.Nack(false, false)(重新排队) - 不要在 defer 里直接调用
msg.Ack()——这会导致成功逻辑也触发 ACK,而 panic 时又没机会走 recover 分支 - 推荐模式:先
recover→ 判断是否 panic → 是则Nack(requeue=true);否则业务完成后再Ack()
panic 恢复后必须区分“已处理”和“未处理”状态
单纯 recover 并 requeue 不够:如果业务逻辑已部分写库、发 HTTP、改缓存,再重试就会重复执行。所以不能只靠 RabbitMQ 机制兜底,还得配合幂等设计。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 消息体里带唯一
message_id或业务单号,入库/落日志前先查是否存在处理记录 -
Nack(requeue=true)前建议加短延迟(如time.Sleep(100 * time.Millisecond)),避免瞬时重试风暴 - 高频失败场景下,
Nack(requeue=false)+ 死信交换机(DLX)更安全,避免无限循环压垮队列 - 注意
msg.Nack()的第二个参数是requeue,不是multiple(Go 客户端不支持 multiple Nack)
连接意外中断时消息仍可能卡住
即使你写了完美的 recover,如果 consumer 进程被 kill -9、OOM killer 杀掉、或容器被强制删除,goroutine 来不及执行 defer,消息就永远停留在 “unack” 状态,既不重发也不释放。
- 设置合理的
channel.Qos(prefetch=1),避免预取过多消息导致“卡住”范围扩大 - 启用 RabbitMQ 的
consumer_timeout或客户端心跳(amqp.Config.Heartbeat),让 broker 主动检测并释放 dead consumer 占用的 unack 消息 - 关键业务务必配置队列的
x-dead-letter-exchange,把长期 unack 的消息自动转到 DLQ,人工介入排查
真正难的不是写 recover,而是判断什么情况下该 requeue、什么情况下该进 DLQ、以及业务层如何做到幂等。这三个点漏掉任何一个,panic 后的消息都可能变成幽灵数据。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










