go和rabbitMq 消息队列封装使用

最近在开发一个购车相关的app,使用到go和rabbitMQ记录分享。

项目技术栈:

  1. 搜索部分用到ES
  2. go框架使用 go-zero
  3. 消息队列:rabbitMq
  4. 数据库:mysql、redis

go库 github.com/rabbitmq/amqp091-go

rabbitMQ go客户端初始化代码

package rabbitMq

import (
	"errors"
	"fmt"
	"log"
	"math"
	"reflect"
	"sync"
	"time"

	amqp "github.com/rabbitmq/amqp091-go"
)

var (
	AmqpClient  *amqpConnection
	clientMutex sync.Mutex
)

type amqpConnection struct {
	*amqp.Connection
	initChannel []func() error
	amqpError   chan *amqp.Error
}

func New() *amqpConnection {
	return AmqpClient
}

func NewChannel() (ch *amqp.Channel, err error) {
	if AmqpClient == nil {
		return nil, errors.New("rabbitMq 未初始化连接")
	}
	if AmqpClient.IsClosed() {
		return nil, errors.New("rabbitMq 连接已关闭")
	}
	return AmqpClient.Channel()
}

func Init(url string, initChannel func() error) (err error) {
	clientMutex.Lock()
	defer clientMutex.Unlock()
	if AmqpClient == nil {
		AmqpClient = &amqpConnection{}
	}
	if AmqpClient.Connection == nil {
		dialConfig := amqp.Config{
			Heartbeat: 30 * time.Second, // 设置心跳间隔为30秒
		}
		AmqpClient.Connection, err = amqp.DialConfig(url, dialConfig)
		if err != nil {
			return err
		}
		// 监听连接关闭并重连
		AmqpClient.monitorConnection(url)
	}
	if AmqpClient != nil && !AmqpClient.IsClosed() && initChannel != nil {
		AmqpClient.initChannel = append(AmqpClient.initChannel, initChannel)
		return initChannel()
	} else if AmqpClient != nil && !AmqpClient.IsClosed() && len(AmqpClient.initChannel) > 0 {
		for _, f := range AmqpClient.initChannel {
			if err := f(); err != nil {
				return err
			}
		}
	}
	return
}

// 初始化交换机队列
func InitExchangeQueue(e *ExchangeDeclare, queues ...QueueDeclare) (ch *amqp.Channel, err error) {
	ch, err = NewChannel()
	if err != nil {
		return nil, err
	}
	defer func() {
		if r := recover(); r != nil {
			ch.Close()
			return
		}
		if err != nil {
			ch.Close()
			return
		}
	}()
	if e != nil {
		err = ch.ExchangeDeclare(
			e.Exchange,
			e.Type,
			e.Durable,    // durable
			e.AutoDelete, // auto-delete
			e.Internal,   // internal
			e.NoWait,     // noWait
			e.Arguments,
		)
		if err != nil {
			return nil, err
		}
	}
	if len(queues) > 0 {
		err = exchangeBindQueue(ch, e, queues...)
		if err != nil {
			return nil, err
		}
	}
	// 监听通道关闭(便于日志与排查)
	// closeCh := make(chan *amqp.Error)
	// ch.NotifyClose(closeCh)
	// go func() {
	// 	if err := <-closeCh; err != nil {
	// 		log.Printf("RabbitMQ 通道关闭: %v", err)
	// 	}
	// }()
	return
}

func exchangeBindQueue(ch *amqp.Channel, e *ExchangeDeclare, queues ...QueueDeclare) (err error) {
	for _, v := range queues {
		q, err := ch.QueueDeclare(
			v.Queue,
			v.Durable,    // durable
			v.AutoDelete, // delete when unused
			v.Exclusive,  // exclusive
			v.NoWait,     // no-wait
			v.Arguments,  // arguments
		)
		if err != nil {
			return err
		}
		err = ch.QueueBind(
			q.Name,
			v.QueueBind.RoutingKey,
			e.Exchange,
			v.QueueBind.NoWait,
			v.QueueBind.Arguments,
		)
		if err != nil {
			return err
		}
		if v.BasicQos != nil {
			err = ch.Qos(
				v.BasicQos.PrefetchCount, // prefetch count
				v.BasicQos.PrefetchSize,  // prefetch size
				v.BasicQos.Global,        // global
			)
			if err != nil {
				return err
			}
		}
		if v.Func != nil {
			msgs, err := ch.Consume(
				q.Name,
				v.BasicConsume.ConsumerTag,
				v.BasicConsume.AutoAck, // auto-ack set to false for manual acknowledgment
				v.BasicConsume.Exclusive,
				v.BasicConsume.NoLocal,
				v.BasicConsume.NoWait,
				v.BasicConsume.Arguments,
			)
			if err != nil {
				return err
			}
			go func() {
				for d := range msgs {
					v.Func(ch, d)
				}
			}()
		}
	}
	return
}

// monitorBlocked 会监听 RabbitMQ 的阻塞和解除阻塞事件。
// 当服务器启用流控时(Active 为 true),连接会被阻塞;当流控解除时(Active 为 false),连接恢复。
func (that *amqpConnection) monitorBlocked() {
	// 创建一个 channel 用于接收阻塞通知
	blockedChan := make(chan amqp.Blocking)
	// 注册监听阻塞通知,NotifyBlocked 会返回传入的 channel
	that.Connection.NotifyBlocked(blockedChan)
	// 启动一个 goroutine 处理阻塞事件
	go func() {
		for b := range blockedChan {
			if b.Active {
				// 当 Active 为 true 时表示连接被阻塞
				log.Printf("RabbitMQ 连接被阻塞: %v", b.Reason)
			} else {
				// 当 Active 为 false 时表示阻塞解除
				log.Printf("RabbitMQ 连接阻塞解除")
			}
		}
	}()
}

// 监控 RabbitMQ 连接关闭的情况,并尝试重新连接
func (that *amqpConnection) monitorConnection(url string) {
	go func(url string) {
		that.amqpError = make(chan *amqp.Error)
		closeErr := <-that.Connection.NotifyClose(that.amqpError)
		if closeErr != nil {
			log.Printf("RabbitMQ连接关闭: %v. Attempting to reconnect...", closeErr)
		}
		that.reconnect(url)
	}(url)
}

func (that *amqpConnection) reconnect(url string) {
	maxRetries := 92
	var err error
	var wait time.Duration
	var y float64
	for i := range maxRetries {
		if i <= 60 {
			wait = time.Second
		} else {
			wait = time.Duration(math.Pow(2, float64(y))) * time.Second
			y++
		}
		time.Sleep(wait)
		DeferClose()
		err = Init(url, nil)
		if err != nil {
			log.Printf("重连 RabbitMQ 失败,尝试第 %d 次 err: %v", i+1, err)
			continue
		}
		log.Printf("成功重连到 RabbitMQ")
		return
	}
	log.Printf("无法重连到 RabbitMQ,错误: %v", err)
}

// 关闭 connection
func DeferClose() {
	if AmqpClient != nil && AmqpClient.Connection != nil {
		AmqpClient.Connection.Close()
		AmqpClient.Connection = nil
	}
}

// SetChannel 将 *amqp.Channel 塞到 chStruct 指向的结构体的指定字段中。
// 要求:
//  1. chStruct 必须是“指向结构体的指针”(*T 且 T 为 struct)
//  2. fieldName 必须存在且为可导出字段(否则不可设置)
//  3. 字段类型必须能接收 *amqp.Channel(类型可赋值)
func SetChannel(chStruct any, fieldName string, ch *amqp.Channel) error {
	if chStruct == nil {
		return errors.New("chStruct is nil")
	}
	if ch == nil {
		return errors.New("channel is nil")
	}

	// 必须是指向结构体的指针
	t := reflect.TypeOf(chStruct)
	v := reflect.ValueOf(chStruct)
	if t.Kind() != reflect.Ptr {
		return errors.New("chStruct 必须是指向结构体的指针")
	}
	if v.IsNil() {
		return errors.New("chStruct 不能是 nil 指针")
	}
	elemType := t.Elem()
	if elemType.Kind() != reflect.Struct {
		return errors.New("chStruct 必须指向结构体")
	}

	// 查找字段
	field, ok := elemType.FieldByName(fieldName)
	if !ok {
		return fmt.Errorf("字段 %q 不存在于 %s", fieldName, elemType.Name())
	}
	// 未导出字段无法设置(CanSet=false)
	fieldVal := v.Elem().FieldByIndex(field.Index)
	if !fieldVal.CanSet() {
		return fmt.Errorf("字段 %q 不可设置(可能未导出)", fieldName)
	}

	// 类型可赋值检查:值类型必须“可赋给”字段类型
	chVal := reflect.ValueOf(ch)
	if !chVal.Type().AssignableTo(fieldVal.Type()) {
		return fmt.Errorf("字段 %q 的类型是 %s,无法接收 %s",
			fieldName, fieldVal.Type(), chVal.Type())
	}

	fieldVal.Set(chVal)
	return nil
}

// 声明一个临时、独占、自动删除的队列用于接收响应
func ReplyTo(ch *amqp.Channel) (msgs <-chan amqp.Delivery, q amqp.Queue, err error) {
	q, err = ch.QueueDeclare("", false, false, true, false, nil)
	if err != nil {
		return
	}
	msgs, err = ch.Consume(
		q.Name, // queue
		"",     // consumer
		true,   // auto-ack
		false,  // exclusive
		false,  // no-local
		false,  // no-wait
		nil,    // args
	)
	if err != nil {
		return
	}
	return
}

type ExchangeDeclare struct {
	Exchange      string
	Type          string
	Passive       bool
	Durable       bool
	AutoDelete    bool
	Internal      bool
	NoWait        bool
	Arguments     amqp.Table
	QueueDeclares []QueueDeclare
	FieldName     string
}

type QueueDeclare struct {
	Queue        string
	Passive      bool
	Durable      bool
	Exclusive    bool
	AutoDelete   bool
	NoWait       bool
	Arguments    amqp.Table
	QueueBind    QueueBind
	BasicConsume BasicConsume
	BasicQos     *BasicQos
	Func         func(*amqp.Channel, amqp.Delivery)
}

type QueueBind struct {
	Queue      string
	Exchange   string
	RoutingKey string
	NoWait     bool
	Arguments  amqp.Table
}

type BasicConsume struct {
	Queue       string
	ConsumerTag string
	NoLocal     bool
	AutoAck     bool
	Exclusive   bool
	NoWait      bool
	Arguments   amqp.Table
}

type BasicQos struct {
	PrefetchCount int
	PrefetchSize  int
	Global        bool
}

配置部分大家可以按照自己的框架配置

package config

import (
	"base/pkg/rabbitMq"

	amqp "github.com/rabbitmq/amqp091-go"
)

type exchange struct {
	ExchangeMap map[string]*rabbitMq.ExchangeDeclare
}

func NewExchange() *exchange {
	return &exchange{}
}

func (that *exchange) GetExchange(exchangeName string) (e *rabbitMq.ExchangeDeclare) {
	if v, ok := that.ExchangeMap[exchangeName]; ok {
		return v
	}
	return nil
}

func (that *exchange) Get() map[string]*rabbitMq.ExchangeDeclare {
	return that.ExchangeMap
}

func (that *exchange) Set(exchangeName string, e *rabbitMq.ExchangeDeclare) *exchange {
	that.ExchangeMap[exchangeName] = e
	return that
}

func (that *exchange) SetFunc(exchangeName string, f func(*amqp.Channel, amqp.Delivery)) *exchange {
	if _, ok := that.ExchangeMap[exchangeName]; ok {
		for i := range that.ExchangeMap[exchangeName].QueueDeclares {
			that.ExchangeMap[exchangeName].QueueDeclares[i].Func = f
		}
	}
	return that
}

// 车源MQ
const (
	SyncCar                  = "SyncCar"
	SyncCarMaxConcurrentJobs = 10 // 最大并发处理数量
)

func (that *exchange) CarExchange() *exchange {
	that.ExchangeMap = map[string]*rabbitMq.ExchangeDeclare{
		//同步车源
		SyncCar: {
			Exchange:  "car_sync_exchange",
			Type:      "topic",
			Durable:   true,
			Arguments: amqp.Table{},
			QueueDeclares: []rabbitMq.QueueDeclare{
				{
					Queue:    "car_sync_queue",
					Durable:  true,
					BasicQos: &rabbitMq.BasicQos{PrefetchCount: SyncCarMaxConcurrentJobs, PrefetchSize: 0, Global: false},
				},
			},
			FieldName: SyncCar,
		},
	}
	return that
}


// 网关MQ
const (
	OrderClose    = "OrderClose"
	OrderComplete = "OrderComplete"
	PaySettlement = "PaySettlement"
	OrderCreate   = "OrderCreate"
)

func (that *exchange) GatewayExchange() *exchange {
	that.ExchangeMap = map[string]*rabbitMq.ExchangeDeclare{
		// 下单使用
		OrderCreate: {
			Exchange:  "order_create_exchange",
			Type:      "direct",
			Durable:   false,
			Arguments: amqp.Table{},
			QueueDeclares: []rabbitMq.QueueDeclare{
				{
					Queue:   "order_create_queue",
					Durable: false,
					BasicQos: &rabbitMq.BasicQos{
						PrefetchCount: 1,     // prefetch count
						PrefetchSize:  0,     // prefetch size
						Global:        false, // global
					},
				},
			},
			FieldName: OrderCreate,
		},
	}
	return that
}

示例同步车源数据到ES上消费者

// 初始化 Exchange Queue
func InitChannel() (err error) {
	// 获取Exchange配置并设置消费者方法 syncCarConsumer,我这里是获取上面示例配置,大家可以根据自己使用框架来
	exchangeConfig := config.NewExchange().CarExchange().SetFunc(config.SyncCar, syncCarConsumer)
	for _, v := range exchangeConfig.Get() {
		//开始初始化Exchange Queue
		ch, err := rabbitMq.InitExchangeQueue(v, v.QueueDeclares...)
		if err != nil {
			return err
		}
	}
	return
}

var startOnce sync.Once
func syncCarConsumer(ch *amqp.Channel, d amqp.Delivery) {
	workChan <- d
}
// 使用工作池限制并发处理var
var workChan = make(chan amqp.Delivery, 2*config.SyncCarMaxConcurrentJobs)

func SyncCarWork(svcCtx *svc.ServiceContext) {
	defer func() {
		if r := recover(); r != nil {
			logx.Errorf("SyncCarConsumer panic: %v", r)
		}
	}()
	// 启动工作协程
	for i := range config.SyncCarMaxConcurrentJobs {
		go func(i int) {
			for d := range workChan {
				// 同步ES代码。。。
			}
		}(i)
	}
}

var url = "amqp://" +  "Username" + ":" + "Password" + "@" +  "RabbitMq Hosts" + "/"
err := rabbitMq.Init(url, InitChannel)
if err != nil {
	//链接失败返回错误
	return err
}
//链接成功

例子购车下单消息队列

现在新能源车这么火,来做一个购车下单消息队列

订单消费者consumer.go


func GetExchange() (exchange map[string]*rabbitMq.ExchangeDeclare) {
	exchangeConfig := config.NewExchange().GatewayExchange().
		SetFunc(config.OrderCreate, func(ch *amqp.Channel, d amqp.Delivery) {
		 	// 购车业务处理 。。。
			
			// 发回给消费者回调
			body := []byte("下单成功")
			err = ch.PublishWithContext(ctx, "", d.ReplyTo, false, false, amqp.Publishing{
				ContentType:   "application/json",
				CorrelationId: d.CorrelationId,
				Body:          body,
			})
			if err != nil {
				logx.Error("Publish error:", err)
				err = d.Nack(false, false)
				if err != nil {
					logx.Error(" Nack error:", err)
				}
				return err
			}
			
			// Acknowledge message
			err = d.Ack(false)
			if err != nil {
				logx.Error("Ack error:", err)
				return err
			}
		})
		return exchangeConfig.Exchange
}

// Init initializes the RabbitMQ connection and channels
func Init(ctx *svc.ServiceContext) error {
	var url = "amqp://" + "Username" + ":" + "Password" + "@" + "Hosts" + "/"

	err := rabbitMq.Init(url, InitChannel)
	if err != nil {
		return err
	}
	return nil
}

// InitChannel initializes RabbitMQ channels and consumers
func InitChannel() error {
	exchangeArr := GetExchange()
	for _, v := range exchangeArr {
		_, err := rabbitMq.InitExchangeQueue(v, v.QueueDeclares...)
		if err != nil {
			return err
		}
	}
	return nil
}

订单生产者 producer.go

var (
	ch   *Channel
	once sync.Once
)

type Channel struct {
	OrderCreate *amqp.Channel //订单关闭
}

func NewCh() *Channel {
	once.Do(func() {
		ch = &Channel{}
	})
	return ch
}

func Init() (err error) {
	var url = "amqp://" + "Username"+ ":" + "Password" + "@" + "Hosts" + "/"
	err = rabbitMq.Init(url, InitChannel)
	if err != nil {
		logx.Error(err)
		return err
	}
	return
}

func GetExchange() (exchange []*rabbitMq.ExchangeDeclare) {
	exchangeConfig := config.NewExchange().GatewayExchange()
	exchange = append(exchange, exchangeConfig.GetExchange(config.OrderCreate))
	return
}

func InitChannel() (err error) {
	exchangeArr := GetExchange()
	for _, v := range exchangeArr {
		ch, err := rabbitMq.InitExchangeQueue(v, v.QueueDeclares...)
		if err != nil {
			return err
		}
		err = rabbitMq.SetChannel(NewCh(), v.FieldName, ch)
		if err != nil {
			return err
		}
	}
	return
}


type BuyCarReq struct {
	CarNo               string              `json:"CarNo"`                        // 车源编号
}

type BuyCarRsp struct {
	OrderNo    string  `json:"OrderNo,omitempty"`    //订单编号
	PayNo      string  `json:"PayNo,omitempty"`      //支付单号
	Amount     float64 `json:"Amount,omitempty"`     //金额
}

// 下单方法 客户端请求调用
func OrderCreateSend(buyCarReq *types.BuyCarReq) (buyCarRsp *types.BuyCarRsp, err error) {
	buyCarRsp = &types.BuyCarRsp{}
	reqBody, err := json.Marshal(buyCarReq)
	if err != nil {
		logx.Error("Failed to marshal buyCarReq:", err)
		return buyCarRsp, err
	}

	ch := NewCh().OrderCreate
	// 回调临时队列,也可以用固定队列
	msgs, q, err := rabbitMq.ReplyTo(ch)
	corrId := uuid.New()
	//设置下单超时
	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
	defer cancel()
	// 往rabbitMQ的 order_create_exchange 交换机发送消息
	err = ch.PublishWithContext(ctx,
		"order_create_exchange", // exchange
		"",                     	// routing key
		false,                   // mandatory
		false,                   // immediate
		amqp.Publishing{
			ContentType:   "application/json",
			CorrelationId: corrId,
			ReplyTo:       q.Name, //回调队列
			Body:          reqBody,
		})
	if err != nil {
		//发送失败
		return buyCarRsp, err
	}
	//等待回调
	for {
		select {
		case <-ctx.Done():
			logx.Error("Timeout waiting for response")
			return buyCarRsp, fmt.Errorf("购买超时,请稍后重试")
		case d, ok := <-msgs:
			if !ok {
				return buyCarRsp, fmt.Errorf("购买失败,请稍后重试")
			}
			if d.CorrelationId == corrId {
				if err = json.Unmarshal(d.Body, &buyCarRsp); err != nil {
					return buyCarRsp, err
				}
				if buyCarRsp.Error != "" {
					err = errors.New(buyCarRsp.Error)
				}
				return buyCarRsp, err
			}
		}
	}
}
posted @ 2025-09-01 15:46  耳东01  阅读(15)  评论(0)    收藏  举报