一、消费者确认机制 Consumer Acknowledgement
为了确认消费者是否成功处理消息,RabbitMQ 提供了消费者确认机制(Consumer Acknowledgement)。 当消费者处理消息结束后,向 RabbitMQ 发送回执,告知消息处理状态。回执分为 3 种:
ack:成功处理消息,RabbitMQ 从队列删除该消息nack:消息处理失败,RabbitMQ 可再次投递消息reject:消息处理失败并拒绝该消息,RabbitMQ 直接删除消息
Spring AMQP 封装了确认功能,配置文件指定 ack 模式,共 3 种:
- none :自动 ack。消息投递到消费者立刻 ack,MQ 直接删除消息。不推荐,业务处理失败消息已经丢失,无法补救。
- manual:手动 ack。业务代码手动调用 ack/nack/reject,灵活,需要开发者自己处理异常。
- auto :自动模式(推荐)。Spring 利用 AOP 环绕拦截消息处理逻辑:
- 业务正常执行 → 自动返回
ack - 业务异常 → 自动返回
nack - 消息校验 / 解析异常 → 自动返回
reject
- 业务正常执行 → 自动返回
重点:Go amqp091-go SDK 没有 Spring 那种开箱即用的 auto 自动 AOP 模式 Spring 的
auto是框架封装能力,原生 AMQP 协议底层只有两种:
autoAck=true≈ Springnone:消息投递直接 ack,MQ 删消息,不安全autoAck=false≈ Springmanual:手动 ack,所有 ack/nack/reject 全部手写代码,自己捕获 panic、区分异常类型,模拟 Spring auto 逻辑
Go 代码:模拟 Spring auto 自动确认
Go
package main
import (
"log"
"github.com/rabbitmq/amqp091-go"
)
func main() {
conn, err := amqp091.Dial("amqp://guest:guest@127.0.0.1:5672/")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
ch, err := conn.Channel()
if err != nil {
log.Fatal(err)
}
defer ch.Close()
// autoAck=false 开启手动ack,模拟Spring auto模式
msgs, err := ch.Consume(
"test_queue", // queue
"", // consumer
false, // autoAck=false【核心】
false, // exclusive
false, // noLocal
false, // noWait
nil, // args
)
if err != nil {
log.Fatal(err)
}
forever := make(chan struct{})
go func() {
for d := range msgs {
// 模拟Spring AOP环绕,捕获panic
func() {
defer func() {
// 捕获panic
if r := recover(); r != nil {
log.Printf("panic异常: %v", r)
// 业务panic → Nack,消息重新入队
_ = d.Nack(false, true)
}
}()
// ========== 业务处理逻辑 ==========
err := handleMsg(d.Body)
if err != nil {
// 判断异常类型
if isMsgCheckErr(err) {
// 消息校验异常 → reject,丢弃消息(不重投)
log.Println("消息校验失败,reject丢弃")
_ = d.Reject(false)
} else {
// 业务异常 → nack,消息重新投递
log.Println("业务异常,nack重入队列")
_ = d.Nack(false, true)
}
} else {
// 正常处理成功 → ack,MQ删除消息
log.Println("处理成功,ack")
_ = d.Ack(false)
}
}()
}
}()
log.Println("消费者启动,等待消息")
<-forever
}
// handleMsg 业务处理函数
func handleMsg(body []byte) error {
// 业务逻辑
return nil
}
// isMsgCheckErr 判断是否为消息校验类异常(自行实现)
// 例如:json解析失败、参数非法这类不可恢复的消息异常
func isMsgCheckErr(err error) bool {
return false
}
二、消费失败处理 & 本地重试
问题:消费失败后如果直接 nack (requeue=true),消息会不断重新入队,无限重试,造成 MQ 消息堆积、压力飙升。 解决方案:本地重试 :在当前 goroutine 内循环重试业务,重试全部失败之后,再决定消息去向,不要把消息丢回 MQ 无限循环。
Go 本地重试完整代码(对齐 Spring 重试配置)
Go
package main
import (
"fmt"
"github.com/streadway/amqp"
"log"
"time"
)
// 本地重试配置,对应spring配置
const (
maxAttempts = 3 // 最大重试次数,对应max-attempts
initialInterval = 1 * time.Second // 初始等待时间,initial-interval
multiplier = 1.0 // 时间倍数 multiplier
)
// 模拟业务消费函数
func handleMsg(msg *amqp.Delivery) error {
// 模拟业务异常
return fmt.Errorf("业务处理失败")
}
// 判断是否是不可恢复异常(这类不重试,直接丢弃)
func isMsgCheckErr(err error) bool {
// json解析失败、参数非法这类,不可恢复,不重试
return false
}
func consume(queueName string, ch *amqp.Channel) {
msgs, err := ch.Consume(
queueName,
"",
false, // autoAck=false,手动ack
false,
false,
false,
nil,
)
if err != nil {
log.Fatal(err)
}
forever := make(chan bool)
go func() {
for d := range msgs {
var err error
waitTime := initialInterval
success := false
// ========== 本地重试逻辑 ==========
for attempt := 0; attempt < maxAttempts; attempt++ {
err = handleMsg(&d)
if err == nil {
success = true
break
}
// 不可恢复异常,直接跳出,不再重试
if isMsgCheckErr(err) {
break
}
log.Printf("消费失败,第%d次重试,等待%v, err:%v", attempt+1, waitTime, err)
time.Sleep(waitTime)
waitTime = time.Duration(float64(waitTime) * multiplier)
}
if success {
// 消费成功,ACK
err = d.Ack(false)
if err != nil {
log.Println("ack失败", err)
}
} else {
// 本地重试全部耗尽:Nack(false),不重新入队!!
// 注意:第二个参数 requeue=false,千万不要写true!
err = d.Nack(false, false)
if err != nil {
log.Println("nack失败", err)
}
log.Println("本地重试全部失败,消息丢弃/转入死信")
}
}
}()
log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
<-forever
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
ch, err := conn.Channel()
if err != nil {
log.Fatal(err)
}
defer ch.Close()
queueName := "test_queue"
consume(queueName, ch)
}
三、MessageRecoverer 失败消息处理策略
Spring AMQP 在重试耗尽后,使用MessageRecoverer接口处理失败消息,共 3 种实现:
表格
| Spring AMQP 类 | 含义 | Go 代码等价操作 |
|---|---|---|
| RejectAndDontRequeueRecoverer(默认) | 重试耗尽,拒绝消息,不重新入队;队列配置死信就进 DLQ,没配置直接丢弃 | d.Nack(false, false) 第二个参数 requeue=false |
| ImmediateRequeueMessageRecoverer | 重试耗尽,消息放回原队列,重新消费(会无限循环,生产严禁使用) | d.Nack(false, true) 第二个参数 requeue=true |
| RepublishMessageRecoverer | 重试耗尽,重新发布一条新消息到指定交换机(异常交换机 error.direct),消息头附加异常信息 | 手动调用 ch.Publish(),body + 异常信息放入 header 投递到目标交换机 |
区分: 原生死信 DLQ:MQ Broker 自动转发原消息; RepublishMessageRecoverer:代码手动发布一条全新消息,原消息 ack 移除,不属于原生 DLQ,常用来做异常告警,投递到 error.queue 人工排查。
Go 实现 RepublishMessageRecoverer(复刻 Spring)
Go
package main
import (
"github.com/streadway/amqp"
"log"
)
// 模仿Spring RepublishMessageRecoverer
type RepublishMessageRecoverer struct {
ch *amqp.Channel
targetExchange string
routingKey string
}
func NewRepublishMessageRecoverer(ch *amqp.Channel, exchange, rk string) *RepublishMessageRecoverer {
return &RepublishMessageRecoverer{
ch: ch,
targetExchange: exchange,
routingKey: rk,
}
}
// Recover 重试耗尽后调用
func (r *RepublishMessageRecoverer) Recover(delivery *amqp.Delivery, err error) error {
// 复制header,追加异常信息
headers := delivery.Headers
if headers == nil {
headers = make(amqp.Table)
}
headers["x-exception"] = err.Error()
headers["x-original-exchange"] = delivery.Exchange
headers["x-original-rk"] = delivery.RoutingKey
// 手动发布一条新消息到 error.direct,和Spring逻辑一致
errPub := r.ch.Publish(
r.targetExchange,
r.routingKey,
false,
false,
amqp.Publishing{
Body: delivery.Body,
Headers: headers,
ContentType: delivery.ContentType,
},
)
if errPub != nil {
log.Printf("republish失败:%v", errPub)
return errPub
}
// 原消息ack,从原队列移除
errAck := delivery.Ack(false)
if errAck != nil {
log.Printf("ack原消息失败:%v", errAck)
}
return nil
}
四、核心总结(面试背诵版)
消费者如何保证消息一定被消费?
- 开启消费者确认机制为 auto,由 Spring 确认消息处理成功后返回 ack,异常时返回 nack。
- 开启消费者失败重试机制 ,并设置
MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理。