RabbitMQ 的 DLX+TTL 实现消息重试应避免本地 for 循环重试,消费函数只做一次调用并按错误类型分流:临时失败走 DLX 重试,确定失败进死信或落库;超时用 context 控制,幂等性由下游保障,重试间隔由 TTL 而非 time.Sleep 控制。

用 RabbitMQ 的 DLX + TTL 实现消息重试,不是在代码里写 for 循环
消息消费失败后本地重试(比如 for i := 0; i )是典型误区:它不通知 RabbitMQ,消息不会重回队列,也不会进死信队列,更无法人工干预或监控。真正可靠的重试必须由 AMQP 协议层驱动。
核心思路是利用 RabbitMQ 的两个原生机制:dead-letter-exchange(DLX)和 x-message-ttl(TTL)。失败消息先发往一个带 TTL 的“重试队列”,过期后自动路由到 DLX,再由 DLX 绑定回原始消费队列——形成可控的重试闭环。
- 消费者收到消息后,先检查
msg.Headers["retry_count"],若为空或 ch.Publish 到死信交换器 - 重试队列声明时必须设
Arguments: map[string]interface{}{"x-dead-letter-exchange": "original-exchange"} - TTL 不要设在 message 上(
deliveryMode=2不保证),而要设在重试队列本身:x-message-ttl: 10000(单位毫秒) - 每次重试前更新 header:
msg.Headers["retry_count"] = count + 1,再重新 publish 到重试队列
手动 ACK + Nack 控制消息生命周期,禁用 autoAck
amqp.AutoAck = true 是消息丢失的头号原因——消息一抵达就从队列删除,根本没机会重试。所有生产环境消费者必须关闭 autoAck,改用手动确认。
关键动作只有两个:ch.Ack() 表示成功消费;ch.Nack(false, true) 表示失败且需重回队列头部(注意第二个参数 requeue=true)。
立即学习“go语言免费学习笔记(深入)”;
- 启动 consumer 时调用
ch.Qos(1, 0, false),限制未确认消息数为 1,防止单条卡住阻塞后续 - 每条消息处理完再
ch.Ack();panic 或 error 必须用recover()捕获,然后ch.Nack(false, true) - 别在 handler 里直接
return或忽略错误——未Ack也未Nack的消息会滞留在 unacked 状态,直到 channel 关闭才可能被重发(行为不可控) - 模拟崩溃测试时,用
http.HandleFunc("/crash", func() { os.Exit(1) }),而非kill -9,才能触发 RabbitMQ 的 requeue 逻辑
重试次数控制必须存在 header 或 message 层,不能靠内存变量
用局部变量 retryCount int 记录重试次数毫无意义:goroutine 重启、进程崩溃、消费者扩缩容都会丢掉该值。RabbitMQ 不保存你的 Go 变量,只认 message headers 或 body 字段。
推荐优先用 header:msg.Headers["retry_count"]。它轻量、AMQP 原生支持、不参与业务反序列化逻辑,且在消息跨队列流转时自动携带。
- 首次发送时,在
amqp.Publishing.Headers中初始化:"retry_count": 0 - 重试队列消费时,读取并递增:
count := msg.Headers["retry_count"].(int); count++ - 若
count >= 3,不再发回重试队列,而是发往 DLQ 或直接落库记录:ch.Publish("dl-exchange", "", false, false, amqp.Publishing{...}) - 避免把重试计数塞进 JSON body——解析失败会导致整个消息无法处理,header 失败则只是字段缺失,更健壮
gRPC/HTTP 调用失败时,别在消费函数里嵌套重试逻辑
消费者函数 func(msg amqp.Delivery) error 的职责是“可靠地把消息交出去”,不是“确保下游服务一定成功”。如果在内部对 HTTP/gRPC 请求做重试,会拖长单条消息处理时间,导致 QoS 触发、unacked 积压、甚至 RabbitMQ 主动断连。
正确分层是:消费函数只做一次调用 + 错误分类;临时性失败(如 net.OpError、HTTP 503)走 DLX 重试;确定性失败(如 json.SyntaxError、HTTP 400)直接进死信或落库。
- HTTP 调用务必传入带 timeout 的
context.Context,超时后立即Nack,交给 DLX 下一轮重试 - 不要用
backoff.Retry包裹整个消费逻辑——它会让一条消息卡住 channel 几十秒,违背消息队列“快速流转”设计初衷 - 幂等性必须由下游服务保障:同一
msg.MessageId或业务 ID 多次投递,结果一致。否则 DLX 重试只会放大数据错乱 - 重试间隔由 TTL 控制(如 1s / 5s / 30s),不是代码里的
time.Sleep——后者无法跨进程生效,且增加消费者资源占用


















