必须关闭 autoAck,因为开启后消息会立即被删除,导致未处理完成即丢失;应使用手动 ACK(autoAck=false),并确保每条消息成功处理后调用 msg.Ack(false) 或失败时正确调用 msg.Nack(true, false),配合 DLX 实现可靠重试与死信兜底。

为什么 RabbitMQ 消费者必须关掉 autoAck?
因为开 autoAck=true 时,RabbitMQ 一发消息就立刻从队列删掉,哪怕你的 Go 程序刚收到还没开始处理、或者处理中途 panic、网络断了、进程被 kill —— 消息就永久丢失了。这不是“可能丢”,是“一定丢”。
真实场景里,processMessage() 往往涉及数据库写入、HTTP 调用、文件 IO,这些都可能失败或超时。手动 ACK 才是唯一能保证“处理成功才删消息”的方式。
-
ch.Consume(..., false, ...)第三个参数必须是false(即autoAck=false) - 每条
msg收到后,**必须且只能调用一次**msg.Ack(false)或msg.Nack(...) - 忘记调用、重复调用、在 goroutine 外调用(比如 defer 里)都会导致连接卡死或消息堆积
- 若业务逻辑 panic,需 recover 并显式
Nack,否则该 delivery tag 会一直阻塞后续消息
msg.Nack 的两个布尔参数怎么选?
msg.Nack(requeue, multiple) 看似简单,但参数组合直接影响消息流向和重试行为,很多人配反就导致消息无限循环或直接进黑洞。
-
requeue=true:消息重新入队,可能被同一消费者或别的消费者再次拿到(适合临时性错误,如 DB 连接闪断) -
requeue=false:消息被丢弃 —— 除非你配置了死信交换器(DLX),否则它就消失了(慎用!) -
multiple=true:Nack 所有未确认的、delivery tag ≤ 当前 msg 的消息(极少用,容易误杀) - 日常重试只用
msg.Nack(true, false);死信兜底则确保队列声明时带x-dead-letter-exchange参数
Go 错误处理不是打补丁,而是控制流设计点
在消息消费场景里,error 不是日志里的一个字符串,它是决定消息命运的开关。把 if err != nil 写成 log.Printf("warn: %v", err) 就等于默认选择丢弃消息。
立即学习“go语言免费学习笔记(深入)”;
- 所有可能出错的环节都要返回
error:DB 查询、HTTP 请求、JSON 解析、甚至time.Parse() - 不要用
panic处理业务错误(比如 “用户不存在”、“余额不足”),它绕过Nack逻辑,消息直接卡住 - 需要区分错误类型?用自定义错误实现
Is()方法,或用errors.As()判断底层错误(如*url.Error) - 函数签名别偷懒:宁可多写一个
error返回值,也不要让上层靠nil检查猜逻辑分支
Kafka 和 RabbitMQ 在错误语义上根本不是一回事
很多人想“换 MQ 就改个库”,但 Kafka 的 consumer.CommitOffsets() 和 RabbitMQ 的 msg.Ack() 行为差异极大,强行套用会导致严重数据不一致。
- RabbitMQ 是 per-message 确认,ACK 后消息立即不可见;Kafka 是 offset 提交,提交后该 offset 前所有消息都算“已消费”,不管它们是否真被处理完
- Kafka 没有内置重试队列机制,
Nack需自己发回原 topic 或另建 retry topic,并管理重试次数 TTL - RabbitMQ 的 DLX 是声明式配置,Kafka 的死信要靠 consumer 主动判断 + 生产者发往专用 topic
- 如果你的业务强依赖“失败三次进人工干预”,RabbitMQ + DLX 是更稳的选择;若追求吞吐和顺序,Kafka 需自己补全幂等+重试+监控链路
消息没丢不等于业务没错。ACK 之后数据库写失败、回调第三方超时、状态机跳转遗漏 —— 这些都不会触发重试,得靠外部对账或状态补偿。真正的可靠性不在 MQ 配置里,而在你敢不敢把「处理完成」的定义写进业务代码里。











