场景:支付服务网络异常,消息发出但未消费,目标:保证消息至少被消费一次。 消息丢失三大场景:
- 消息发送阶段丢失
- MQ 服务端存储丢失
- 消费者消费阶段丢失 可靠性保障分为三块:发送者可靠性、MQ 可靠性、消费者可靠性;兜底方案:延迟消息。
一、发送者的可靠性
1. 生产者发送重试(Spring template.retry)
作用:客户端与 MQ 通信异常(网络断开、连接超时)时重试。
⚠️ 注意:仅作用在消息投递阶段;不是消费者消费失败重试。 如果消息成功投递 Broker,只是路由失败,不会触发这个重试,需要配合发布确认机制。
spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
max-attempts: 3 # 最大重试次数
参数解读:
- enabled=true:开启生产者重试
- initial-interval=1000ms:第一次重试等待 1s
- multiplier=1:每次重试间隔不变,永远 1s
- max-attempts=3:最多重试 3 次(原始发送 1 次 + 重试 2 次,一共尝试 3 次)
缺陷:重试会阻塞线程,影响性能,需要合理配置等待时长与最大重试次数。
Go 等价代码(带重试发送)
Go
package main
import (
"context"
"fmt"
"github.com/rabbitmq/amqp091-go"
"time"
)
// 配置参数,和yml一一对应
const (
connectionTimeout = 1 * time.Second
initialInterval = 1000 * time.Millisecond
multiplier = 1.0
maxAttempts = 3
)
// 带重试的发送函数
func publishWithRetry(ch *amqp091.Channel, exchange, routingKey string, msg []byte) error {
var err error
attempt := 0
currentInterval := initialInterval
for ; attempt < maxAttempts; attempt++ {
ctx, cancel := context.WithTimeout(context.Background(), connectionTimeout)
defer cancel()
err = ch.PublishWithContext(ctx,
exchange,
routingKey,
false,
false,
amqp091.Publishing{
ContentType: "application/json",
Body: msg,
})
if err == nil {
fmt.Printf("第%d次发送成功\n", attempt+1)
return nil
}
fmt.Printf("第%d次发送失败,err:%v,等待%v后重试\n", attempt+1, err, currentInterval)
time.Sleep(currentInterval)
// 间隔乘以倍数
currentInterval = time.Duration(float64(currentInterval) * multiplier)
}
return fmt.Errorf("所有%d次重试全部失败,last err:%w", maxAttempts, err)
}
func main() {
conn, err := amqp091.DialConfig("amqp://guest:guest@127.0.0.1:5672/", amqp091.Config{
ConnectionTimeout: connectionTimeout,
})
if err != nil {
panic(err)
}
defer conn.Close()
ch, err := conn.Channel()
if err != nil {
panic(err)
}
defer ch.Close()
// 声明topic交换机
err = ch.ExchangeDeclare(
"pay.topic",
"topic",
true,
false,
false,
false,
nil,
)
if err != nil {
panic(err)
}
msgBody := []byte(`{"orderId":"20260929001","payStatus":"success"}`)
err = publishWithRetry(ch, "pay.topic", "pay.success", msgBody)
if err != nil {
fmt.Println("消息发送最终失败:", err)
} else {
fmt.Println("消息投递完成")
}
}
2. 生产者确认机制(Publisher Confirm + Publisher Return)
作用:Broker 收到消息后,向生产者返回 ACK/NACK,用来确认消息是否被 Broker 正确处理。
- Publisher Confirm(发布确认) :Broker 收到消息后返回 ACK/NACK
- ACK:Broker 成功处理消息
- NACK:Broker 收到消息,但处理失败,消息丢失
- Publisher Return(消息退回) :消息成功到达交换机,路由失败,找不到匹配队列,消息退回生产者。
SpringAMQP publisher-confirm-type 三种模式
none:关闭 confirm 机制simple:同步阻塞等待 MQ 回执,发送消息后阻塞等待 ACK/NACKcorrelated:异步回调(生产推荐),MQ 异步返回回执,通过回调处理。
Spring 特性:单个 RabbitTemplate 实例只能绑定 1 个 ReturnCallback,多次 set 会覆盖。 方案:①同一个回调内部通过 if 分支处理多业务;②新建多个 RabbitTemplate Bean。
Go amqp091-go:没有单回调限制。NotifyReturn 返回通道,可以多 goroutine 消费,可复制消息分发实现多个回调处理器。
Go
// returnChan 是这个channel上所有路由退回消息的统一入口
returnChan := ch.NotifyReturn(make(chan amqp091.Return, 100))
// 协程1:日志记录
go func() {
for ret := range returnChan {
fmt.Printf("[日志]路由失败:%s\n", ret.RoutingKey)
}
}()
// 协程2:告警
go func() {
for ret := range returnChan {
fmt.Printf("[告警]路由失败消息:%s\n", ret.Body)
}
}()
核心原理
AMQP 协议中,channel 每发送一条消息,会递增序列号deliveryTag。 发送消息拿到 tag,维护映射:deliveryTag → 消息内容+回调函数; 监听 NotifyPublish 通道收到 ACK/NACK 时,根据 tag 找到对应回调执行。
Spring 依靠
CorrelationData自动维护消息与回调映射;Go 需要手动用sync.Map维护。
Go 完整异步 confirm 示例(对标 Spring CorrelationData)
Go
package main
import (
"context"
"fmt"
"github.com/rabbitmq/amqp091-go"
"sync"
)
// MsgCallback 自定义结构体,等价Spring CorrelationData
// 保存消息、成功/失败回调函数
type MsgCallback struct {
msgBody []byte
// 成功回调
onAck func()
// 失败回调(NACK)
onNack func(reason string)
}
// 全局map:key=deliveryTag,value=消息+回调
var tagMap sync.Map
func main() {
conn, err := amqp091.Dial("amqp://guest:guest@127.0.0.1:5672/")
if err != nil {
panic(err)
}
defer conn.Close()
ch, err := conn.Channel()
if err != nil {
panic(err)
}
defer ch.Close()
// 开启confirm,非批量模式,对应publisher-confirm-type=correlated
err = ch.Confirm(false)
if err != nil {
panic(err)
}
// 监听ACK/NACK通道
confirmChan := ch.NotifyPublish(make(chan amqp091.Confirmation, 100))
// 异步协程:处理confirm回调(对应Spring addCallback)
go func() {
for conf := range confirmChan {
// 根据deliveryTag取出我们保存的回调信息
val, ok := tagMap.Load(conf.DeliveryTag)
if !ok {
continue
}
tagMap.Delete(conf.DeliveryTag)
mc := val.(*MsgCallback)
if conf.Ack {
// ACK成功,执行onSuccess,等价 result.isAck()==true
mc.onAck()
} else {
// NACK失败,执行onFailure
mc.onNack("Broker返回NACK,消息存储失败")
}
}
}()
// ========== 发送一条消息 ==========
msg := []byte("hello")
// 构造CorrelationData等价对象
cd := &MsgCallback{
msgBody: msg,
onAck: func() {
fmt.Println("发送消息成功,收到 ack!")
},
onNack: func(reason string) {
fmt.Printf("发送消息失败,收到 nack,reason:%s\n", reason)
},
}
// 发送消息
err = ch.PublishWithContext(context.Background(),
"hmall.direct", // exchange
"red1", // routingKey
true, // mandatory=true,开启Return
false,
amqp091.Publishing{
DeliveryMode: amqp091.Persistent,
Body: msg,
})
if err != nil {
fmt.Println("publish直接失败,网络异常")
return
}
// 获取当前消息的deliveryTag,存入map绑定回调
tag := ch.NextPublishSeqNo
tagMap.Store(tag, cd)
fmt.Printf("消息发送完成,tag=%d,等待异步回调\n", tag)
select {} // 阻塞,等待回调
}
SpringAMQP 生产者确认返回值规则
- 消息投递到了 MQ,但是路由失败。会 return 路由异常原因,返回 ACK
- 临时消息投递到了 MQ,并且入队成功,返回 ACK
- 持久消息投递到了 MQ,并且入队完成持久化,返回 ACK
- 其它情况都会返回 NACK,告知投递失败
生产者确认处理策略
- 生产者确认会带来额外网络、系统资源开销,业务中尽量不要使用
- 如果一定要使用,一般无需开启 Publisher-Return,路由失败大多是业务代码 / 交换机配置问题,可以提前规避
- NACK 消息可以有限次数重试,多次重试仍然失败则记录异常消息,人工兜底
后续可继续补充:MQ