必须关闭 autoAck = false,否则消息必然丢失;需显式调用 msg.Ack() 或 msg.Nack(true) 且仅一次,配合 Qos(1)、Confirm 和持久化配置,并用业务字段而非 deliveryTag 实现幂等。

必须关掉 autoAck = true,否则消息一定丢
设 autoAck = true 时,RabbitMQ 一发出消息就立刻从队列删除,不管 Go 程序是否 panic、是否处理到一半、甚至还没开始执行业务逻辑。这不是“可能丢”,是“必然丢”。管理界面看到的 Unacked 数永远为 0,反而会误导你认为一切正常。
真实业务里,process(msg.Body) 往往涉及数据库写入、HTTP 调用或文件 IO,任一环节失败都会导致消息消失。唯一能守住底线的做法是:在 channel.Consume() 中显式传入 false 作为第三个参数。
-
msgs, err := channel.Consume(queueName, "", false, false, false, false, nil)—— 第三个false就是autoAck - 不要依赖注释或变量名推断;直接写
false,避免误传autoAck: true - 日志采集、埋点等允许丢失的场景除外;订单、支付、库存类业务绝不能开
msg.Ack() 和 msg.Nack() 必须各调一次,且只在当前 goroutine 内调用
msg.Ack(false) 表示确认当前这条消息;msg.Nack(false, true) 表示拒绝当前消息并重新入队(requeue = true 是防丢底线)。这两者必须且只能调用一次,否则 RabbitMQ 会卡住后续消息,甚至报 PRECONDITION_FAILED - unknown delivery tag。
- 别在
defer里调用:defer msg.Ack(false)会导致成功和失败路径都触发,重复确认 - panic 必须 recover 并显式
Nack:没 recover 的 panic 会让该deliveryTag永久滞留为 unacked 状态 - 别跨 goroutine 传递
amqp.Delivery后再调Ack:deliveryTag只在创建它的channel内有效,换 channel 或连接后无效 -
multiple = true风险高:如Ack(5, true)要求 tag 连续且 ≤5 的所有消息都已处理,实际很难保证顺序与完整性
Qos、Confirm、持久化三者缺一不可
只手动 Ack 不够。若不设 Qos,RabbitMQ 会预推大量消息进消费者内存,一条卡住,其余全堵住;若不开启 channel.Confirm(),生产端发出去就失联;若消息或队列没持久化,Broker 重启后数据全空。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 消费前必须
channel.Qos(1, 0, false):限制未确认消息数为 1,防止单条慢处理拖垮整个消费者 - 生产端发消息前必须先
channel.Confirm(false):该调用不可逆,且只对之后的Publish()生效;漏掉则所有 publish 都无确认保障 - 消息发布时加
amqp.Publishing{DeliveryMode: amqp.Persistent};队列声明时durable: true;否则 ACK 成功也白搭 - 死信配置别漏:
msg.Nack(false, false)会直接丢弃消息,除非队列已声明x-dead-letter-exchange,否则等于黑洞
deliveryTag 不是 ID,幂等性得靠业务字段
deliveryTag 是每个 channel 独立维护的单调递增整数,不是全局唯一标识。它不能存数据库、不能跨进程复用、也不能用于日志追踪对齐——连接断开后,所有未确认的 tag 全部失效,RabbitMQ 会自动重发对应消息。
真正能支撑去重和幂等的是业务自身字段,比如订单号、事件 ID 或 UUID。这些值要随消息体一起发送,并在消费逻辑开头做去重检查(例如查 DB 是否已存在该订单号的状态记录)。
常见错误是把 deliveryTag 当成消息 ID 打日志、入库、甚至用于 HTTP 请求参数。结果服务重启后,tag 重置,日志无法关联,状态对不上,排查成本陡增。它只该出现在 Ack() / Nack() 调用里,别让它逃出那几行代码。

















