【RabbitMQ #9】 | 生产者可靠性

场景:支付服务网络异常,消息发出但未消费,目标:保证消息至少被消费一次。 消息丢失三大场景:

  1. 消息发送阶段丢失
  2. MQ 服务端存储丢失
  3. 消费者消费阶段丢失 可靠性保障分为三块:发送者可靠性、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 # 最大重试次数

参数解读:

  1. enabled=true:开启生产者重试
  2. initial-interval=1000ms:第一次重试等待 1s
  3. multiplier=1:每次重试间隔不变,永远 1s
  4. 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 三种模式
  1. none:关闭 confirm 机制
  2. simple:同步阻塞等待 MQ 回执,发送消息后阻塞等待 ACK/NACK
  3. correlated:异步回调(生产推荐),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 生产者确认返回值规则
  1. 消息投递到了 MQ,但是路由失败。会 return 路由异常原因,返回 ACK
  2. 临时消息投递到了 MQ,并且入队成功,返回 ACK
  3. 持久消息投递到了 MQ,并且入队完成持久化,返回 ACK
  4. 其它情况都会返回 NACK,告知投递失败
生产者确认处理策略
  1. 生产者确认会带来额外网络、系统资源开销,业务中尽量不要使用
  2. 如果一定要使用,一般无需开启 Publisher-Return,路由失败大多是业务代码 / 交换机配置问题,可以提前规避
  3. NACK 消息可以有限次数重试,多次重试仍然失败则记录异常消息,人工兜底

后续可继续补充:MQ

相关推荐
银河技术2 小时前
TB级文本去重实战:从单机 OOM 到 Spark / Ray 分布式架构的工程演进
分布式·微服务·重构·架构·spark·llm·rag
deepdata_cn3 小时前
从集中式到分布式:Data Mesh颠覆传统数据架构的底层逻辑
分布式·data mesh
500844 小时前
React Native for OpenHarmony 实战:三方库 react-native-device-uptime 的鸿蒙化适配指南
javascript·分布式·react native·react.js·harmonyos
灯澜忆梦11 小时前
【RabbitMQ #10】 | MQ可靠性
分布式·rabbitmq
醉颜凉14 小时前
Kafka 与 RabbitMQ/RocketMQ 选型对比:场景匹配、性能基准与迁移成本
kafka·消息队列·rabbitmq·rocketmq·中间件选型
逐流人15 小时前
Ceph分布式存储集群配置与池管理:从配置优先级到PG、复本池与纠删码池
运维·分布式·ceph·云原生·云计算·rados
天天喝旺仔18 小时前
gRPC 底层原理:HTTP/2 多路复用、Protobuf 序列化与四种流模式
分布式·http·微服务·rpc
灯澜忆梦21 小时前
【RabbitMQ #5】 | 三大交换机 Fanout ,Direct ,Topic
分布式·rabbitmq·ruby