go 进阶 三方库之 streadway/amqp

一. 基础示例

  1. github.com/streadway/amqp: 操作rabbitmq相关库
  2. 连接mq, 初始化交换机,队列,并绑定
import (
	"fmt"
	"github.com/streadway/amqp"
	"log"
	"os"
	"testing"
	"time"
)

//连接端口通知channel
var ConnErrorChan = make(chan *amqp.Error, 1)

//1.读取配置信息,连接mq,获取到amqp通道
func newRabbitMQConn() *amqp.Channel {
	conn, err := amqp.Dial("amqp://用户名:密码@localhost:5672/") //考虑是否拼接命名空间
	if nil != err {
		fmt.Println("连接mq失败")
		os.Exit(1)
	}
	//defer conn.Close()

	ch, err := conn.Channel()
	if nil != err {
		fmt.Println("连接mq失败")
		os.Exit(1)
	}
	//defer ch.Close()

	//那到mq连接Connection后,Connection上有两个方法,其中通过NotifyClose可以做重连
	//conn.NotifyBlocked()
	conn.NotifyClose(ConnErrorChan)
	go reopen()

	return ch
}

//2.监听断开消息,进行重连
func reopen() {
	select {
	case err := <-ConnErrorChan:
		fmt.Println("连接关闭开始重连 err:" + err.Error())
		time.Sleep(time.Second * 10)
		_ = newRabbitMQConn()
		//考虑有消费业务情况下,再次启动消费监听
		//go Consume(newMMQ)
		break
	}
}

//3.初始化交换机,队列,并绑定
func initExchangeAndQueue(amqpCh *amqp.Channel) {
	//声明交换机
	err := amqpCh.ExchangeDeclare(
		"logs",   // name  交换机名字
		"fanout", // type  交换机模式 :direct,fanout, topic,headers
		true,     // durable  持久化
		false,    // auto-deleted 是否自动删除
		false,    // internal
		false,    // no-wait
		nil,      // arguments 附属参数
	)
	if nil != err {
		fmt.Println("声明交换机异常")
		os.Exit(1)
	}

	//声明队列
	q, err := amqpCh.QueueDeclare(
		"hello world", // 队列名
		true,          // 持久化
		false,         // 是否自动删除
		false,         // 排他性
		false,         // no-wait
		nil,           // 附属参数
	)
	if nil != err {
		fmt.Println("声明交队列异常")
		os.Exit(1)
	}

	//队列交换机绑定
	err = amqpCh.QueueBind(
		q.Name, //队列名称
		"",     // routing key
		"logs", //交换机名称
		false,
		nil)
	if nil != err {
		fmt.Println("交换机队列绑定异常")
		os.Exit(1)
	}
}
  1. 生产者发送消息
//4.发送消息数据
func Publish(amqpCh *amqp.Channel, data interface{}) {
	//发送消息
	body := "消息数据"
	err := amqpCh.Publish(
		"logs", // exchange  生产者将消息发往logs交换机
		"",     // routing key
		false,  // mandatory
		false,  // immediate
		amqp.Publishing{
			DeliveryMode: amqp.Persistent, //消息持久化
			ContentType:  "text/plain",
			Body:         []byte(body),
		})

	if nil != err {
		fmt.Println("消息发送失败")
	}
}

func TestProduction(t *testing.T) {
	//1.获取mq连接
	amqpCh := newRabbitMQConn()
	//2.初始化交换机,队列,并绑定
	initExchangeAndQueue(amqpCh)
	//3.发送消息
	Publish(amqpCh, "消息数据")
}
  1. 消费者接收消息
func TestConsume(t *testing.T) {
	//1.获取mq连接
	amqpCh := newRabbitMQConn()
	//2.初始化交换机,队列,并绑定
	initExchangeAndQueue(amqpCh)
	//3.消费消息
	Consume(amqpCh)
}

func Consume(amqpCh *amqp.Channel) {
	//1.注册一个消费者(接收消息),会返回一个用来读取消息的通道
	msgs, err := amqpCh.Consume(
		"hello world", // queue 队列名称
		"",            // consumer
		false,         // auto-ack 自动响应ack
		false,         // exclusive
		false,         // no-local
		false,         // no-wait
		nil,           // args
	)
	if nil != err {
		fmt.Println("注册消费者失败")
		os.Exit(1)
	}

	forever := make(chan bool)

	//2.启动协程接收消息
	go func() {
		//msgs是一个channel通道,会一直等待获取消息
		for d := range msgs {
			log.Printf("Received a message: %s", d.Body)
			//手动ack
			d.Ack(false)
		}
	}()

	//防止关闭阻塞
	<-forever
}

总结问题

  1. 通过streadway/amqp实现mq业务流程
  1. 通过amqp下的Dial()函数,连接mq服务端,获取到连接
  2. 拿到连接后调用连接下的Channel()函数,会返回一个核心通道,后续的操作都是基于这个通道完成的
  1. 断开重连怎么实现
  1. 在调用amqp下的Dial()函数拿到mq连接后,Connection下存在一个NotifyBlocked()方法与NotifyClose()方法
  2. 其中NotifyClose()可以实现,该方法需要一个通道,当断连后,会向该通道发送一个异常消息amqp.Error, 我们可以通过select监听这个通道,当接收到断连消息后,重新发起连接请求
  1. mq交换机有哪几种模式,在调用ExchangeDeclare()声明交换机时,该方法需要一个kind参数,通过这个参数设置消息模式,有以下几种
  1. direct 直连模式可以理解为1对1,生产方通过交换机将消息投递的队列要完全匹配
  2. fanout (扇出交换机)模式可以理解为分发,交换机会将消息广播给该交换机绑定的所有队列,与routingKey无关
  3. routing路由模式: 消息生产者将消息发送给交换机按照路由判断,路由是字符串(info) 当前产生的消息携带路由字符(对象的方法),交换机根据路由的key,只能匹配上路由key对应的消息队列,对应的消费者才能消费消息(注意真实并没有routing这种模式,kind也不能设置,只是topic以为的不支持正则匹配,可以放到几种模式最后讲,也可以不说这种模式)
  4. topic 主题模式,解决路由过多问题,在分发时路由键支持正则匹配,匹配成功的才能获取数据
  5. header 消息头模式(不常用): 此种方式为在消息头中设置识别标志,消费者根据消息头的识别标志进行消费
  1. 如何开启消息确认机制
  1. 在调用ExchangeDeclare()声明交换机时,需要一个auto-deleted变量是否自动删除,设置为false
  2. 消费方调用Consume()监听消费消息时,会拿到一个只读的Delivery类型channel,监听这个通道获取消息, 当获取到消息后调用Delivery下的Ack()方法,进行手动ack
  1. 如何实现消息持久化
  1. 在调用ExchangeDeclare()声明交换机时需要一个durable是否持久化,设置为true
  2. 同时调用QueueDeclare()声明队列时也需要一个durable是否持久化,设置为true
  3. 生产方调用Publish()发送消息时,需要将消息封装为Publishing结构体,该结构体上有一个DeliveryMode属性,设置为amqp.Persistent,这样即使mq服务重启,消息也不会丢失

二. 延时消息与死信

  1. 延时消息:有两种方式
  1. 在队列上设置延时: 调用QueueDeclare()声明队列时,传递一个amqp.Table变量,内部通过x-message-ttl指定延时时间(amqp.Table底层实际就是一个map)
  2. 发送消息时,针对消息封装延时: 在调用方Publish()发送消息时,要将消息封装为Publishing,在Publishing内部存在一个Expiration,用来指定延时时间
func initQueue(amqpCh *amqp.Channel) amqp.Queue {
	//1.使用x-message-ttl参数设置当前队列中所有消息的过期时间
	argsTable := amqp.Table{
		"x-message-ttl":             5000,              //消息过期时间,毫秒
	}

	q, _ := amqpCh.QueueDeclare(
		"direct_ttl", // 队列名
		false,        // 持久化
		false,        // 是否自动删除
		false,        // 排他性
		false,        // no-wait
		argsTable,         // 设置ttl为5s
	)
	return q
}
	err := amqpCh.Publish(
		"交换机名称",
		"当前使用routing-key",
		false,
		false,
		amqp.Publishing{
			DeliveryMode: amqp.Persistent, //消息持久化
			ContentType:  "text/plain",
			Body:         []byte("消息数据"),
			Expiration:   strconv.Itoa(1000), //ttl时间
		},
	)
  1. 死信
  1. 初始化死信交换机, 死信队列, 并绑定,也就是下方的initDeadExchangeAndQueue(),注意不要给死信队列设置过期时间
  2. 封装amqp.Table, 内部通过x-message-ttl指定延时时间, 通过x-dead-letter-exchange指定死信交换机,通过x-dead-letter-routing-key指定死信key, 将这些死信相关设置
  3. 声明普通业务交换机,队列等, 在通过QueueDeclare()方法声明队列时,将存有死信相关的amqp.Table,设置进去,将普通队列与死信进行绑定
func send(amqpCh *amqp.Channel) {

	//1.初始化死信交换机,死信队列,并绑定
	initDeadExchangeAndQueue(amqpCh)

	//2.封装死信信息
	argsTable := amqp.Table{
		"x-message-ttl":             5000,              //消息过期时间,毫秒
		"x-dead-letter-exchange":    "死信交换机",           // 指定死信交换机
		"x-dead-letter-routing-key": "指定死信routing-key", // 指定死信routing-key
	}

	//3.普通业务队列与死信绑定
	q, _ := amqpCh.QueueDeclare(
		"正常消息队列名称", // 队列名
		false,      // 持久化
		false,      // 是否自动删除
		false,      // 排他性
		false,      // no-wait
		argsTable,  // 绑定死信
	)

	amqpCh.QueueBind(
		q.Name,      //队列名称
		"",          // routing key
		"正常业务交换机名称", //交换机名称
		false,
		nil)

	err := amqpCh.Publish(
		"交换机名称",
		"当前使用routing-key",
		false,
		false,
		amqp.Publishing{
			DeliveryMode: amqp.Persistent, //消息持久化
			ContentType:  "text/plain",
			Body:         []byte("消息数据"),
		},
	)
	if nil != err {
		fmt.Println("消息发送失败")
	}
}

func initDeadExchangeAndQueue(amqpCh *amqp.Channel) {
	// 声明死信队列
	// args 为 nil。切记不要给死信队列设置消息过期时间,否则失效的消息进入死信队列后会再次过期。
	q, err := amqpCh.QueueDeclare(
		"死信队列名称",
		true,
		false,
		false,
		false,
		nil)

	// 声明交换机
	err = amqpCh.ExchangeDeclare(
		"死信交换机名称",
		amqp.ExchangeDirect,
		true,
		false,
		false,
		false,
		nil)

	// 队列绑定(将队列、routing-key、交换机三者绑定到一起)
	err = amqpCh.QueueBind(q.Name, "死信RoutingKey", "死信交换机名称", false, nil)

	if nil != err {

	}
}
  1. 并不是直接声明一个公共的死信队列,然后所以死信消息就自己跑到死信队列里去了。而是为每个需要使用死信的业务队列配置一个死信交换机,这里同一个项目的死信交换机可以共用一个,然后为每个业务队列分配一个单独的路由key,死信队列并不是什么特殊的队列,只不过是绑定在死信交换机上的队列。死信交换机也不是什么特殊的交换机,只不过是用来接受死信的交换机,所以可以为任何类型【Direct、Fanout、Topic】。一般来说,会为每个业务队列分配一个独有的路由key,并对应的配置一个死信队列进行监听,也就是说,一般会为每个重要的业务队列配置一个死信队列
  2. 死信被丢到死信队列中后的变化
  1. 如果队列配置了参数 x-dead-letter-routing-key,“死信”的路由key将会被替换成该参数对应的值。如果没有设置,则保留该消息原有的路由key, 举例: 如果原有消息的路由key是testA,被发送到业务Exchage中,然后被投递到业务队列QueueA中,如果该队列没有配置参数x-dead-letter-routing-key,则该消息成为死信后,将保留原有的路由keytestA,如果配置了该参数,并且值设置为testB,那么该消息成为死信后,路由key将会被替换为testB,然后被抛到死信交换机中
  2. 由于被抛到了死信交换机,所以消息的Exchange Name也会被替换为死信交换机的名称
  1. 死信消息的Header中存了哪些附加信息
    在这里插入图片描述

总结

  1. 下方几个问题上面都有
  1. 如何实现延时消息, 有两种方式
  2. 如何实现死信
  3. 消息变为死信后的变化

三. 一个生产示例

初始化连接

  1. 初始化mq连接
  1. 执行go get 执行获取amqp包
  2. 读取rabbitmq相关配置信息,初始化
import (
	"bytes"
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"encoding/hex"
	amqp "github.com/streadway/amqp"
	"hios_coupon/config"
	"hios_coupon/log"
	"strconv"
	"strings"
	"time"
)

const (
	RoomChangeExchange = "xpms"
	RoomChangeName     = "qhc"
)

var (
	//MMQ                  = New()
	ConnErrorChan        = make(chan *amqp.Error, 1)
	ReconnTimes    int64 = 0
	Conn           *amqp.Connection
	useAliQmqp     = 1
	AccessFromUser = 0   //用户名
	Colon          = ":" //分割符
)

// NewDirtyRoomRegChannel 创建mq连接
func NewMMQ() *amqp.Channel {
	address := ""
	username := ""
	password := ""

	if useAliQmqp == 1 {
		//阿里amqp使用配置
		instanceID, _ := config.Conf.GetValue("amqp", "instanceId")
		url, _ := config.Conf.GetValue("rabbitmq", "address")
		address = instanceID + url + ":5672"
		ak := "用户名"
		sk := "秘钥"
		username = GetUserName(ak, instanceID)
		password = GetPassword(sk)
	} else {
		//rabbitmq使用配置
		address, _ = config.Conf.GetValue("rabbitmq", "address")
		username, _ = config.Conf.GetValue("rabbitmq", "username")
		password, _ = config.Conf.GetValue("rabbitmq", "password")
	}
	msgId := "systemStartCall"
	url := "amqp://"
	url += username
	url += ":"
	url += password
	url += "@"
	url += address
	url += "/MMQHost"
	// 拨号连接mq服务器
	log.TraceLog(msgId, "rabbitMqError connection url: %s", url)
Reconnect:
	Conn, err := amqp.Dial(url)
	if err != nil {
		log.TraceLog(msgId, "rabbitMqError MMQ dialError %v", err)
		ReconnTimes += 1
		if ReconnTimes == 1 {
			goto Reconnect
		}
		return nil
	}
	//首次连接成功才会有 断开重连,首次连接失败,可能是网络不通
	Conn.NotifyClose(ConnErrorChan)
	go reopen(msgId)
	log.TraceLog(msgId, "rabbitmqSuccess MMQ dialOk")
	ch, err := Conn.Channel()
	if err != nil {
		log.TraceLog(msgId, "rabbitMqError MMQ connectError %v", err)
		// log2.ErrorLog("", "%+v", err)
		return nil
	}

	// channel重新断开重连 暂不考虑channel的异常,channel异常一般发生在rabbitmq上手动删除channel
	// ch.NotifyClose(ChErrorChan)
	log.TraceLog(msgId, "rabbitmqSuccess MMQ connectOk")
	// 初始化 channel,设置队列名称等
	initChannel(msgId, ch)
	return ch
}

// 断开重连
func reopen(msgId string) {
	select {
	case err := <-ConnErrorChan:
		log.TraceLog(msgId, "rabbitMqError MMQ connectError %v", err)
		time.Sleep(time.Second * 10)
		newMMQ := NewMMQ()
		//消费监听下方有该函数示例
		go Consume(newMMQ)
		break
	}
	log.TraceLog(msgId, "reopenSelectFinished")
}

// 初始化通道
func initChannel(msgId string, ch *amqp.Channel) {
	err2 := ch.QueueBind(RoomChangeName, "*.pms", RoomChangeExchange, false, nil)
	if err2 != nil {
		log.TraceLog(msgId, "rabbitMqError MMQ queueBindError {}", err2)
	}
}

// GetUserName 获取用户名称
func GetUserName(ak string, instanceID string) string {
	var buffer bytes.Buffer
	buffer.WriteString(strconv.Itoa(AccessFromUser))
	buffer.WriteString(Colon)
	buffer.WriteString(instanceID)
	buffer.WriteString(Colon)
	buffer.WriteString(ak)
	return base64.StdEncoding.EncodeToString(buffer.Bytes())
}

// GetPassword 获取密码
func GetPassword(sk string) string {
	now := time.Now()
	currentMillis := strconv.FormatInt(now.UnixNano()/1000000, 10)
	var buffer bytes.Buffer
	buffer.WriteString(strings.ToUpper(HmacSha1(currentMillis, sk)))
	buffer.WriteString(Colon)
	buffer.WriteString(currentMillis)
	return base64.StdEncoding.EncodeToString(buffer.Bytes())
}

// HmacSha1 sha1哈希
func HmacSha1(keyStr string, message string) string {
	key := []byte(keyStr)
	mac := hmac.New(sha1.New, key)
	mac.Write([]byte(message))
	return hex.EncodeToString(mac.Sum(nil))
}

提供发送消息函数

  1. 在上方已经编写了获取rabbitMQ连接句柄的New()函数, 调用该函数拿到amqp.Channel
  2. 通过amqp.Channel调用Publish()方法发送mq消息
func SendMessage(msgid, exchange, routingkey string, sendBody interface{}) {
	data, err := json.Marshal(sendBody)
	if nil != err {
		log.ErrorLog(msgid, " MQ SendBody Byte[] Decode Error |"+err.Error())
		return
	}
	PublishRabbitMQ(
		exchange,
		msgid,
		routingkey,
		false,
		false,
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte(data),
		})
}

func PublishRabbitMQ(exchange, msgid string, key string, mandatory, immediate bool, msg amqp.Publishing) {
	msg.MessageId = msgid
	err := MQ.Publish(exchange, key, mandatory, immediate, msg)
	if err != nil {
		log.TraceLog(msgid, "PublishRabbitMQ Err Exchange: %s, Key: %s Err: %v", exchange, key, err)
		MQ = New()
		err = MQ.Publish(exchange, key, mandatory, immediate, msg)
	}
	if err != nil {
		log.TraceLog(msgid, "PublishRabbitMQ Err Exchange: %s, Key: %s Err: %v", exchange, key, err)
	} else {
		log.TraceLog(msgid, "PublishRabbitMQ Success Exchange: %s, Key: %s", exchange, key)
	}
}

消费消息

  1. 在启动服务时要调用一下该消费消息函数
import (
	"encoding/json"
	"github.com/streadway/amqp"
	"hios_coupon/entity/request"
	"hios_coupon/log"
	couponService "hios_coupon/service/coupon"
	"strconv"
	"time"
)

const (
	payOrderCoupon  = "pay.order.coupon"  //购买优惠卷支付成功
	payOrderProduct = "pay.order.product" //购买商品支付成功(获取该消息中优惠卷相关的)
)

//启动服务时需要调用,启动消费
func Consume(ch *amqp.Channel) {
	msgId := strconv.Itoa(int(time.Now().Unix()))
	log.TraceLog(msgId, "rabbitmqSuccess MMQ queueBindOk")
	msg, err := ch.Consume(RoomChangeName, "hios_coupon", true, false, false, false, nil)
	if err != nil {
		log.TraceLog(msgId, "rabbitMqError MMQ consumeError {}", err)
		return
	}
	log.TraceLog(msgId, "rabbitmqSuccess MMQ consumeFuncOk")
	go func() {
		//当接收到消息后,会从msg中取出消息
		for data := range msg {
			//执行消费逻辑
			defaultConsumer(msgId, data)
		}
	}()
	log.TraceLog(msgId, "consumerStarted")
}

func defaultConsumer(msgid string, data amqp.Delivery) {
	log.TraceLog(msgid, "defaultConsumer data.RoutingKey: %s, Body: %s", data.RoutingKey, string(data.Body))
	log.TraceLog(msgid, "defaultConsumer Content-type: %s", data.ContentType) //data.ContentType="application/json"
	switch data.RoutingKey {
	case payOrderCoupon:
		//调用消费业务方法1
		PayCouponConsume(msgid, data)
		return
	case payOrderProduct:
		//调用消费业务方法2
		PayOrderProductConsume(msgid, data)
		return
	default:
		log.InfoLog(msgid, "未对接该 RoutingKey")
	}

}

//消费相关的业务方法1
func PayCouponConsume(msgid string, data amqp.Delivery) {
	payConsume := &request.PayConsume{}
	if err := json.Unmarshal(data.Body, payConsume); nil != err {
		log.ErrorLog(msgid, "jsonUnmarshalError data:%v error:%v", data, err)
		return
	}
	if payConsume.PayState != 1 {
		log.ErrorLog(msgid, "购买优惠卷,接收支付消息为失败 data: %v", payConsume)
		return
	}
	if len(payConsume.OrderCode) == 0 {
		log.ErrorLog(msgid, "购买优惠卷,接收支付消息,优惠卷创建流水为空 data: %v", payConsume)
		return
	}
	if len(payConsume.TransactionId) == 0 {
		log.ErrorLog(msgid, "购买优惠卷,接收支付消息,三方支付流水为空 data: %v", payConsume)
		return
	}
	if _, err := couponService.CouponOrderUpdateStatusOk(msgid, payConsume.TransactionId, payConsume.OrderCode); nil != err {
		log.ErrorLog(msgid, "购买优惠卷,接收支付消息,更新本地优惠卷信息异常 err: %v", err)
		return
	}
}

//消费相关的业务方法2
func PayOrderProductConsume(msgid string, data amqp.Delivery) {
	couponPay := &request.PayConsume{}
	if err := json.Unmarshal(data.Body, couponPay); nil != err {
		log.ErrorLog(msgid, "jsonUnmarshalError data:%v error:%v", data, err)
	}
	if len(couponPay.CouponCode) == 0 {
		return
	}

	if err := couponService.UseCouponApplyUpdate(couponPay.CouponCode); nil != err {
		log.ErrorLog(msgid, "接收使用优惠卷消息更新本地相关信息异常 err: %v", err)
		return
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值