死信队列是消息可靠性的兜底机制,必须显式配置dlx参数、手动声明死信交换机,kafka需应用层双生产者实现;nack与reject语义不同,应避免重入队和defer调用;dlq自身需监控、结构化日志、重试限制及独立配置。

死信不是“额外功能”,而是消息可靠性的兜底开关;不配 DLQ,等于裸奔处理关键业务。
为什么你的消息没进死信队列?
常见现象是:消息处理失败、msg.Nack(true, false) 也调了,但死信队列里空空如也。根本原因往往在队列声明阶段就漏了关键参数。
- 用
amqp.Table声明 normal queue 时,必须显式设置"x-dead-letter-exchange"(指向死信交换机)和"x-dead-letter-routing-key"(指定路由键),缺一不可 - RabbitMQ 不会自动创建死信交换机,你得先手动声明
dead_exchange,且类型要匹配(通常为direct或topic) - 如果同时设置了
"x-message-ttl"和"x-dead-letter-exchange",TTL 过期后消息才会被投递到 DLX;但若只设了 TTL 没配 DLX,过期消息直接被丢弃,不会进任何队列 - Kafka 没有原生死信队列机制,所谓“DLQ”必须靠应用层双生产者实现:主消费者失败后,用另一个
kafka.Producer显式发往"my-topic-dlq"主题
Go 中 Nack 和 Reject 怎么选?
两者都会触发死信,但语义和行为不同,选错会导致重试逻辑混乱或消息堆积。
-
msg.Nack(true, false):批量拒绝当前及之前未确认的所有消息,且不重新入队(requeue=false)——这是最常用、最安全的失败处理方式 -
msg.Reject(false):只拒绝当前消息,不重入队;适合明确不可恢复的错误(如 JSON 解析失败、字段缺失) -
msg.Nack(false, true)或msg.Reject(true):让消息重入队头,容易造成无限循环消费;除非你有幂等+指数退避,否则禁用 - 别在
defer里调Nack:goroutine 退出后连接可能已断,调用会 panic 或静默失败
如何避免死信队列自己也“死掉”?
死信队列不是保险箱,它同样需要监控、限流和可追溯性,否则故障会从主链路蔓延到兜底链路。
- DLQ 主题/队列也要配置独立的 consumer group,不能复用主 consumer 的
group.id,否则 offset 提交互相干扰 - DLQ 消费者必须记录完整原始消息体 + 错误堆栈 + 时间戳,建议写入结构化日志或专用表(如
dlq_events),而不是只打log.Printf("dlq: %v", err) - 对 DLQ 消息做最大重试次数限制(比如 3 次),超过后转入归档存储(如 S3 / OSS),避免人工介入前持续占用资源
- Kafka 场景下,DLQ 生产者要用独立
ConfigMap,尤其注意"acks"设为"all",防止 DLQ 消息自己丢失
跨 MQ 迁移时死信逻辑最容易崩在哪?
很多人以为把 msg.Ack() 换成 consumer.CommitOffsets() 就完事,结果线上大量消息重复或丢失。
- RabbitMQ 的
Ack/Nack是 per-message 的,Kafka 的CommitOffsets是 per-partition+offset range 的,语义完全不同 - Kafka 没有“拒绝单条消息”的 API,只能靠业务逻辑跳过处理 + 手动发 DLQ;而 RabbitMQ 可以精准控制哪条进死信
- RabbitMQ 死信由 Broker 自动路由,Kafka DLQ 必须由消费者主动发送,这意味着:Kafka 的 DLQ 路径多一次网络 IO、多一个失败点、多一层错误处理
- 别复用同一套 error 判断逻辑:RabbitMQ 的
amqp.Error和 Kafka 的kafka.Error类型不兼容,panic 捕获方式也不同
真正难的不是写通 DLQ,而是让每条失败消息都留下可定位的上下文、可控的流转路径和明确的生命周期终点。否则,死信队列只是把问题从眼前挪到了后台日志里。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











