消息队列(Message Queue,简称MQ),指保存消息的一个容器,本质是个队列。许多人认为 MQ 通过消息的发送和接受来实现程序的异步和解耦,mq主要用于异步操作,这个不是 MQ 的真正目的,只不过是 MQ 的应用,MQ 真正的目的是为了通讯。
一、RabbitMQ介绍
RabbitMQ是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑RabbitMQ是一个快递站,一个快递员帮你传递快件。RabbitMQ与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据。
RabbitMQ 特点:
- 可靠性:通过一些机制例如,持久化,传输确认等来确保消息传递的可靠性
- 拓展性:多个RabbitMQ节点可以组成集群
- 高可用性:队列可以在RabbitMQ集群中设置镜像,如此一来即使部分节点挂掉了,但是队列仍然可以使用
- 多种协议:原生的支持AMQP,也能支持STOMP、MQTT等协议
- 丰富的客户端:我们常用的编程语言都支持RabbitMQ
- 管理界面:自带提供一个WEB管理界面
- 插件机制:RabbitMQ 自己提供了很多插件,可以按需要进行拓展 Plugins
总体上看RabbitMQ是一个生产者和消费者的模型, 接收,存储 ,转发
二、RabbitMQ安装(docker安装)
1、拉取镜像
该镜像包括web管理页面,不用单独启动
docker pull rabbitmq:management
2、创建和启动容器
注意需要提前开放相应端口
docker run -d --name rabbit -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin123 -p 15672:15672 -p 5672:5672 -p 25672:25672 -p 61613:61613 -p 1883:1883 rabbitmq:management
- -d:后台运行容器
- --name:指定容器名
- -p:指定服务运行的端口,5672:应用访问端口;15672:web管理控制面板端口;25672:RabbitMQ节点间的CLI通讯端口;61613:STOMP客户端插件启用时的端口;1883:MQTT插件启动时的端口。
- -e:指定环境变量,RABBITMQ_DEFAULT_VHOST:默认虚拟机名;RABBITMQ_DEFAULT_USER:默认的用户名;RABBITMQ_DEFAULT_PASS:默认用户名的密码
3、打开web管理页面
浏览器访问 ip:15672 ,使用启动时设置的用户名和密码登录,如果没有使用-e设置默认用户名密码,可以用账号:guest,密码:guest 进行登录,如下界面表示安装成功
三、Go 连接和使用RabbitMQ
1、安装RabbitMQ驱动
go get github.com/streadway/amqp
2、连接RabbitMQ
由于rabbitMQ是c/s架构,所以我们不论是生产者还是消费者,都是通过client端来操作。
(1)生产者和消费者都需要完成的步骤
- 创建与server端的连接 connection
- 创建channel(发送或接收消息通道)
(2)生产者要完成的步骤
- 通过channnel声明Queue(如果Queue已经存在,服务端则会忽略,不存在,服务端则会新建一个Queue)
- 通过channel声明Exchange,需要指定Exchange的Type类型(同Queue声明一样,有则忽略,没有则创建)
- 创建binding,指定binding key 将Queue绑定到Exchange上,可有多个绑定关系。
- 发送消息,指定消息的 bingding key 、Exchange(消息携带了bingding key 和 Exchange,到达Exchange后就知道根据什么type,以及绑定关系分发给哪个队列)
(3)消费者要完成的步骤
- 通过channel声明Queue(同生产者)
- 从Queue中取消息
- 要注意的是,消费和没必要去关注 Exchange和 绑定关系,因为这是生产者关注的点,尽管消费端也可以 创建交换机以及绑定关系。
(4)代码实现(生产者和消费者共用)
需要修改地方:MQURL 格式 amqp://账号:密码@rabbitmq服务器地址:端口号/vhost (默认是5672端口)
注意:生产者和消费者可以根据消息模式进行封装,直接调用。本代码实例即 topic模式
package rabbitMq
import (
"log"
"github.com/streadway/amqp"
) //导入mq包
// MQURL 格式 amqp://账号:密码@rabbitmq服务器地址:端口号/vhost (默认是5672端口)
// 端口可在 /etc/rabbitmq/rabbitmq-env.conf 配置文件设置,也可以启动后通过netstat -tlnp查看
const MQURL = "amqp://admin:mima@192.168.1.11:5672/"
// RabbitMQ 结构体
type RabbitMQ struct {
Conn *amqp.Connection
Channel *amqp.Channel
// 队列名称
QueueName string
// 交换机
Exchange string
// routing Key
RoutingKey string
//MQ链接字符串
Mqurl string
}
// 创建结构体实例
func NewRabbitMQ(queueName, exchange, routingKey string) *RabbitMQ {
rabbitMQ := RabbitMQ{
QueueName: queueName,
Exchange: exchange,
RoutingKey: routingKey,
Mqurl: MQURL,
}
var err error
//创建rabbitmq连接
rabbitMQ.Conn, err = amqp.Dial(rabbitMQ.Mqurl)
checkErr(err, "创建连接失败")
//创建Channel
rabbitMQ.Channel, err = rabbitMQ.Conn.Channel()
checkErr(err, "创建channel失败")
return &rabbitMQ
}
// 释放资源,建议NewRabbitMQ获取实例后 配合defer使用
func (mq *RabbitMQ) ReleaseRes() {
mq.Conn.Close()
mq.Channel.Close()
}
// 错误处理
func checkErr(err error, meg string) {
if err != nil {
log.Fatalf("%s:%s\n", meg, err)
}
}
3、生产者代码实现
package main
import (
"fmt"
"mq/rabbitMq"
"github.com/streadway/amqp"
)
//生产者发布流程
func main() {
// 初始化mq
mq := rabbitMq.NewRabbitMQ("queue_publisher", "exchange_publisher", "key1")
defer mq.ReleaseRes() // 完成任务释放资源
// 1.声明队列
/*
如果只有一方声明队列,可能会导致下面的情况:
a)消费者是无法订阅或者获取不存在的MessageQueue中信息
b)消息被Exchange接受以后,如果没有匹配的Queue,则会被丢弃
为了避免上面的问题,所以最好选择两方一起声明
ps:如果客户端尝试建立一个已经存在的消息队列,Rabbit MQ不会做任何事情,并返回客户端建立成功的
*/
_, err := mq.Channel.QueueDeclare( // 返回的队列对象内部记录了队列的一些信息,这里没什么用
mq.QueueName, // 队列名
true, // 是否持久化
false, // 是否自动删除(前提是至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除。注意:生产者客户端创建这个队列,或者没有消费者客户端与这个队列连接时,都不会自动删除这个队列)
false, // 是否为排他队列(排他的队列仅对“首次”声明的conn可见[一个conn中的其他channel也能访问该队列],conn结束后队列删除)
false, // 是否阻塞
nil, //额外属性(我还不会用)
)
if err != nil {
fmt.Println("声明队列失败", err)
return
}
// 2.声明交换器
err = mq.Channel.ExchangeDeclare(
mq.Exchange, //交换器名
"topic", //exchange type:一般用fanout、direct、topic
true, // 是否持久化
false, //是否自动删除(自动删除的前提是至少有一个队列或者交换器与这和交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑)
false, //设置是否内置的。true表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式
false, // 是否阻塞
nil, // 额外属性
)
if err != nil {
fmt.Println("声明交换器失败", err)
return
}
// 3.建立Binding(可随心所欲建立多个绑定关系)
err = mq.Channel.QueueBind(
mq.QueueName, // 绑定的队列名称
mq.RoutingKey, // bindkey 用于消息路由分发的key
mq.Exchange, // 绑定的exchange名
false, // 是否阻塞
nil, // 额外属性
)
// err = mq.Channel.QueueBind(
// mq.QueueName, // 绑定的队列名称
// "routingkey2", // bindkey 用于消息路由分发的key
// mq.Exchange, // 绑定的exchange名
// false, // 是否阻塞
// nil, // 额外属性
// )
if err != nil {
fmt.Println("绑定队列和交换器失败", err)
return
}
// 4.发送消息
mq.Channel.Publish(
mq.Exchange, // 交换器名
mq.RoutingKey, // routing key
false, // 是否返回消息(匹配队列),如果为true, 会根据binding规则匹配queue,如未匹配queue,则把发送的消息返回给发送者
false, // 是否返回消息(匹配消费者),如果为true, 消息发送到queue后发现没有绑定消费者,则把发送的消息返回给发送者
amqp.Publishing{ // 发送的消息,固定有消息体和一些额外的消息头,包中提供了封装对象
ContentType: "text/plain", // 消息内容的类型
Body: []byte("hello jochen"), // 消息内容
},
)
}
4、消费者代码实现
package main
import (
"fmt"
"mq/rabbitMq"
)
// 消费者订阅
func main() {
// 初始化mq
mq := rabbitMq.NewRabbitMQ("queue_publisher", "exchange_publisher", "key1")
defer mq.ReleaseRes() // 完成任务释放资源
// 1.声明队列(两端都要声明,原因在生产者处已经说明)
_, err := mq.Channel.QueueDeclare( // 返回的队列对象内部记录了队列的一些信息,这里没什么用
mq.QueueName, // 队列名
true, // 是否持久化
false, // 是否自动删除(前提是至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除。注意:生产者客户端创建这个队列,或者没有消费者客户端与这个队列连接时,都不会自动删除这个队列)
false, // 是否为排他队列(排他的队列仅对“首次”声明的conn可见[一个conn中的其他channel也能访问该队列],conn结束后队列删除)
false, // 是否阻塞
nil, // 额外属性(我还不会用)
)
if err != nil {
fmt.Println("声明队列失败", err)
return
}
// 2.从队列获取消息(消费者只关注队列)consume方式会不断的从队列中获取消息
msgChanl, err := mq.Channel.Consume(
mq.QueueName, // 队列名
"", // 消费者名,用来区分多个消费者,以实现公平分发或均等分发策略
true, // 是否自动应答
false, // 是否排他
false, // 是否接收只同一个连接中的消息,若为true,则只能接收别的conn中发送的消息
true, // 队列消费是否阻塞
nil, // 额外属性
)
if err != nil {
fmt.Println("获取消息失败", err)
return
}
for msg := range msgChanl {
// 这里写你的处理逻辑
// 获取到的消息是amqp.Delivery对象,从中可以获取消息信息
fmt.Println(string(msg.Body))
// msg.Ack(true) // 主动应答
}
}