一. 基础示例
- github.com/streadway/amqp: 操作rabbitmq相关库
- 连接mq, 初始化交换机,队列,并绑定
import (
"fmt"
"github.com/streadway/amqp"
"log"
"os"
"testing"
"time"
)
var ConnErrorChan = make(chan *amqp.Error, 1)
func newRabbitMQConn() *amqp.Channel {
conn, err := amqp.Dial("amqp://用户名:密码@localhost:5672/")
if nil != err {
fmt.Println("连接mq失败")
os.Exit(1)
}
ch, err := conn.Channel()
if nil != err {
fmt.Println("连接mq失败")
os.Exit(1)
}
conn.NotifyClose(ConnErrorChan)
go reopen()
return ch
}
func reopen() {
select {
case err := <-ConnErrorChan:
fmt.Println("连接关闭开始重连 err:" + err.Error())
time.Sleep(time.Second * 10)
_ = newRabbitMQConn()
break
}
}
func initExchangeAndQueue(amqpCh *amqp.Channel) {
err := amqpCh.ExchangeDeclare(
"logs",
"fanout",
true,
false,
false,
false,
nil,
)
if nil != err {
fmt.Println("声明交换机异常")
os.Exit(1)
}
q, err := amqpCh.QueueDeclare(
"hello world",
true,
false,
false,
false,
nil,
)
if nil != err {
fmt.Println("声明交队列异常")
os.Exit(1)
}
err = amqpCh.QueueBind(
q.Name,
"",
"logs",
false,
nil)
if nil != err {
fmt.Println("交换机队列绑定异常")
os.Exit(1)
}
}
- 生产者发送消息
func Publish(amqpCh *amqp.Channel, data interface{}) {
body := "消息数据"
err := amqpCh.Publish(
"logs",
"",
false,
false,
amqp.Publishing{
DeliveryMode: amqp.Persistent,
ContentType: "text/plain",
Body: []byte(body),
})
if nil != err {
fmt.Println("消息发送失败")
}
}
func TestProduction(t *testing.T) {
amqpCh := newRabbitMQConn()
initExchangeAndQueue(amqpCh)
Publish(amqpCh, "消息数据")
}
- 消费者接收消息
func TestConsume(t *testing.T) {
amqpCh := newRabbitMQConn()
initExchangeAndQueue(amqpCh)
Consume(amqpCh)
}
func Consume(amqpCh *amqp.Channel) {
msgs, err := amqpCh.Consume(
"hello world",
"",
false,
false,
false,
false,
nil,
)
if nil != err {
fmt.Println("注册消费者失败")
os.Exit(1)
}
forever := make(chan bool)
go func() {
for d := range msgs {
log.Printf("Received a message: %s", d.Body)
d.Ack(false)
}
}()
<-forever
}
总结问题
- 通过streadway/amqp实现mq业务流程
- 通过amqp下的Dial()函数,连接mq服务端,获取到连接
- 拿到连接后调用连接下的Channel()函数,会返回一个核心通道,后续的操作都是基于这个通道完成的
- 断开重连怎么实现
- 在调用amqp下的Dial()函数拿到mq连接后,Connection下存在一个NotifyBlocked()方法与NotifyClose()方法
- 其中NotifyClose()可以实现,该方法需要一个通道,当断连后,会向该通道发送一个异常消息amqp.Error, 我们可以通过select监听这个通道,当接收到断连消息后,重新发起连接请求
- mq交换机有哪几种模式,在调用ExchangeDeclare()声明交换机时,该方法需要一个kind参数,通过这个参数设置消息模式,有以下几种
- direct 直连模式可以理解为1对1,生产方通过交换机将消息投递的队列要完全匹配
- fanout (扇出交换机)模式可以理解为分发,交换机会将消息广播给该交换机绑定的所有队列,与routingKey无关
- routing路由模式: 消息生产者将消息发送给交换机按照路由判断,路由是字符串(info) 当前产生的消息携带路由字符(对象的方法),交换机根据路由的key,只能匹配上路由key对应的消息队列,对应的消费者才能消费消息(注意真实并没有routing这种模式,kind也不能设置,只是topic以为的不支持正则匹配,可以放到几种模式最后讲,也可以不说这种模式)
- topic 主题模式,解决路由过多问题,在分发时路由键支持正则匹配,匹配成功的才能获取数据
- header 消息头模式(不常用): 此种方式为在消息头中设置识别标志,消费者根据消息头的识别标志进行消费
- 如何开启消息确认机制
- 在调用ExchangeDeclare()声明交换机时,需要一个auto-deleted变量是否自动删除,设置为false
- 消费方调用Consume()监听消费消息时,会拿到一个只读的Delivery类型channel,监听这个通道获取消息, 当获取到消息后调用Delivery下的Ack()方法,进行手动ack
- 如何实现消息持久化
- 在调用ExchangeDeclare()声明交换机时需要一个durable是否持久化,设置为true
- 同时调用QueueDeclare()声明队列时也需要一个durable是否持久化,设置为true
- 生产方调用Publish()发送消息时,需要将消息封装为Publishing结构体,该结构体上有一个DeliveryMode属性,设置为amqp.Persistent,这样即使mq服务重启,消息也不会丢失
二. 延时消息与死信
- 延时消息:有两种方式
- 在队列上设置延时: 调用QueueDeclare()声明队列时,传递一个amqp.Table变量,内部通过x-message-ttl指定延时时间(amqp.Table底层实际就是一个map)
- 发送消息时,针对消息封装延时: 在调用方Publish()发送消息时,要将消息封装为Publishing,在Publishing内部存在一个Expiration,用来指定延时时间
func initQueue(amqpCh *amqp.Channel) amqp.Queue {
argsTable := amqp.Table{
"x-message-ttl": 5000,
}
q, _ := amqpCh.QueueDeclare(
"direct_ttl",
false,
false,
false,
false,
argsTable,
)
return q
}
err := amqpCh.Publish(
"交换机名称",
"当前使用routing-key",
false,
false,
amqp.Publishing{
DeliveryMode: amqp.Persistent,
ContentType: "text/plain",
Body: []byte("消息数据"),
Expiration: strconv.Itoa(1000),
},
)
- 死信
- 初始化死信交换机, 死信队列, 并绑定,也就是下方的initDeadExchangeAndQueue(),注意不要给死信队列设置过期时间
- 封装amqp.Table, 内部通过x-message-ttl指定延时时间, 通过x-dead-letter-exchange指定死信交换机,通过x-dead-letter-routing-key指定死信key, 将这些死信相关设置
- 声明普通业务交换机,队列等, 在通过QueueDeclare()方法声明队列时,将存有死信相关的amqp.Table,设置进去,将普通队列与死信进行绑定
func send(amqpCh *amqp.Channel) {
initDeadExchangeAndQueue(amqpCh)
argsTable := amqp.Table{
"x-message-ttl": 5000,
"x-dead-letter-exchange": "死信交换机",
"x-dead-letter-routing-key": "指定死信routing-key",
}
q, _ := amqpCh.QueueDeclare(
"正常消息队列名称",
false,
false,
false,
false,
argsTable,
)
amqpCh.QueueBind(
q.Name,
"",
"正常业务交换机名称",
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) {
q, err := amqpCh.QueueDeclare(
"死信队列名称",
true,
false,
false,
false,
nil)
err = amqpCh.ExchangeDeclare(
"死信交换机名称",
amqp.ExchangeDirect,
true,
false,
false,
false,
nil)
err = amqpCh.QueueBind(q.Name, "死信RoutingKey", "死信交换机名称", false, nil)
if nil != err {
}
}
- 并不是直接声明一个公共的死信队列,然后所以死信消息就自己跑到死信队列里去了。而是为每个需要使用死信的业务队列配置一个死信交换机,这里同一个项目的死信交换机可以共用一个,然后为每个业务队列分配一个单独的路由key,死信队列并不是什么特殊的队列,只不过是绑定在死信交换机上的队列。死信交换机也不是什么特殊的交换机,只不过是用来接受死信的交换机,所以可以为任何类型【Direct、Fanout、Topic】。一般来说,会为每个业务队列分配一个独有的路由key,并对应的配置一个死信队列进行监听,也就是说,一般会为每个重要的业务队列配置一个死信队列
- 死信被丢到死信队列中后的变化
- 如果队列配置了参数 x-dead-letter-routing-key,“死信”的路由key将会被替换成该参数对应的值。如果没有设置,则保留该消息原有的路由key, 举例: 如果原有消息的路由key是testA,被发送到业务Exchage中,然后被投递到业务队列QueueA中,如果该队列没有配置参数x-dead-letter-routing-key,则该消息成为死信后,将保留原有的路由keytestA,如果配置了该参数,并且值设置为testB,那么该消息成为死信后,路由key将会被替换为testB,然后被抛到死信交换机中
- 由于被抛到了死信交换机,所以消息的Exchange Name也会被替换为死信交换机的名称
- 死信消息的Header中存了哪些附加信息

总结
- 下方几个问题上面都有
- 如何实现延时消息, 有两种方式
- 如何实现死信
- 消息变为死信后的变化
三. 一个生产示例
初始化连接
- 初始化mq连接
- 执行go get 执行获取amqp包
- 读取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 (
ConnErrorChan = make(chan *amqp.Error, 1)
ReconnTimes int64 = 0
Conn *amqp.Connection
useAliQmqp = 1
AccessFromUser = 0
Colon = ":"
)
func NewMMQ() *amqp.Channel {
address := ""
username := ""
password := ""
if useAliQmqp == 1 {
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 {
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"
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)
return nil
}
log.TraceLog(msgId, "rabbitmqSuccess MMQ connectOk")
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)
}
}
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())
}
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())
}
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))
}
提供发送消息函数
- 在上方已经编写了获取rabbitMQ连接句柄的New()函数, 调用该函数拿到amqp.Channel
- 通过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)
}
}
消费消息
- 在启动服务时要调用一下该消费消息函数
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() {
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)
switch data.RoutingKey {
case payOrderCoupon:
PayCouponConsume(msgid, data)
return
case payOrderProduct:
PayOrderProductConsume(msgid, data)
return
default:
log.InfoLog(msgid, "未对接该 RoutingKey")
}
}
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
}
}
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
}
}