golang-rabbitMQ客户端
1. RabbitMQ介绍
RabbitMQ是采用Erlang编程语言实现的高级消息队列协议AMQP(Advanced Message Queuing Protocol)的开源消息队列中间件
消息队列中间件的作用:
- 应用解耦
- 流量削峰
- 异步处理
- 缓存存储
- 消息通信
- 提高系统扩展性
1.1 RabbitMQ特点
- 可靠性:通过持久化和传输确认等来确保消息传递的可靠性
- 扩展性:多个RabbitMQ节点可以组成集群
- 高可用:队列可以在RabbitMQ集群中设置镜像,如此一来即使部分节点挂掉,队列仍然可以使用
- 多种协议支持:原生支持AMQP,也能支持STOMP、MQTT等协议
- 丰富的客户端:常用的编程语言都有客户端
- 管理界面:自带一个webUI界面
- 插件机制:RabbitMQ自己提供了多种插件,可以按需扩展Plugins
1.2 RabbitMQ基本概念
总体上看RabbitMQ是一个生产者和消费者模型,用于实现消息的接收、存储、转发
Producer(生产者):消息的生产方,投递方。
Consumer(消费者):消息的消费者。
RabbitMQ Broker(RabbitMQ代理):RabbitMQ服务节点。在单机环境中,就是代表RabbitMQ服务器。
Queue(队列):在RabbitMQ中Queue是存储消息数据的唯一形式。
Binding(绑定):在RabbitMQ中的Binding是Exchange将message路由给Queue所需遵循的规则。如果要指定“交换机E将消息路由给队列Q”,那么Q就需要与E进行绑定。绑定操作需要定义一个可选的路由键(routing key)属性给某些类型的交换机。路由键的意义在于从发送给交换机的众多消息中选择出某些消息,将其路由给绑定的队列。
RoutingKey(路由键):消息投递给交换机,通常会指定一个RoutingKey,通过这个路由键来明确消息的路由规则。RoutingKey 通常是生产者和消费者有协商一致的key策略,消费者就可以合法从生产者手中获取数据。这个RoutingKey主要当Exchange交换机模式为设定为direct和topic模式的时候使用,fanout模式不使用RoutingKey。
Exchange(交换机):生产者将消息发送给交换机,再由交换机将消息路由到对应的队列中。交换机有四种类型:fanout、direct、topic、headers
1.3 Exchange
交换机有四种类型:fanout、direct、topic、headers
1.3.1 fanout
可以把fanout理解为扇形交换机
其将发送带该类型交换机的消息路由到所有与该交换机绑定的队列中,如同一个扇形一样扩散给各个队列
fanout类型的交换机会忽略RoutingKey的存在,将消息直接广播到绑定的所有队列中
1.3.2 direct
可以把direct理解为直连交换机
其根据消息携带的RoutingKey将消息投递到相应的队列中
1.3.3 topic
可以把topic理解为主题交换机
topic交换机在RoutingKey和BindKey匹配规则上更加灵活,同样是将消息路由到RoutingKey和BindKey相匹配的队列中,但是匹配规则有如下特点:
- RoutingKey是一个使用
.
分割的字符串,例如:go.log.info
、java.log.error
- BindKey也是一个使用
.
分割的字符串,但是在BindKey中可以使用两种特殊字符*
和#
用于匹配一个单词,#
用于匹配多规格单词(零个或多个单词)
RoutingKey和BindKey是一种“模糊匹配”,那么一个消息可能会被发送到一个或者多个队列中
无法匹配的消息将会被丢弃或者返回给生产者
1.3.4 headers
可以把headers理解为头交换机
headers类型的交换机使用的不是很多
关于headers exchange比较容易理解的解释是:
1.4 RabbitMQ工作流程
1.4.1 消息生产流程
- 消息生产者与RabbitMQ Broker建立一个连接,建立连接之后开启一个信道channel
- 声明一个交换机,并设置与其相关的属性(交换机类型、持久化等)
- 声明一个队列,并设置其相关属性(排他性、持久化、自动删除等)
- 通过路由键将交换机和队列绑定
- 消息生产者发送消息给RabbitMQ Broker,消息中包含了路由键、交换机等信息,交换机根据接收的路由键查找匹配的队列
- 查找匹配成功,将消息存储到队列中
- 查找匹配失败,根据生产者配置的属性选择丢弃或者退回给生产者
- 关闭信道channel,关闭连接
1.4.2 消息消费流程
- 消费者与RabbitMQ Broker建立连接,连接建立之后开启一个channel
- 消费者向RabbitMQ Broker请求消费者相应队列中的消息
- 等待RabbitMQ Broker回应并投递相应队列中的消息,消费者接收消息
- 消费者确认接收消息(ACK),RabbitMQ Broker删除已经确认的消息
- 关闭信道channel,关闭连接
2. RabbitMQ安装
个人学习推荐使用docker安装
直接安装management版本即可,搜索镜像
$ docker search rabbitmq:management
拉取镜像
$ docker pull rabbitmq:management
management: Pulling from library/rabbitmq
7b1a6ab2e44d: Pull complete
37f453d83d8f: Pull complete
e64e769bc4fd: Pull complete
c288a913222f: Pull complete
12addf9c8bf9: Pull complete
eaeb088e057d: Pull complete
b63d48599313: Pull complete
05c99d3d2a57: Pull complete
43665bfbc3f9: Pull complete
f14c7d7911b1: Pull complete
Digest: sha256:4c4b66ad5ec40b2c27943b9804d307bf31c17c8537cd0cd107236200a9cd2814
Status: Downloaded newer image for rabbitmq:management
docker.io/library/rabbitmq:management
启动RabbitMQ容器
$ docker run -d -p 15672:15672 -p 5672:5672 -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin --name rabbitmq --hostname=rabbitmqhostone rabbitmq:management
参数含义如下:
- -d 后台运行
- -p 端口映射
- -name 指定容器名称
- RABBITMQ_DEFAULT_USER指定用户账号,不指定默认为guest
- RABBITMQ_DEFAULT_PASS指定用户密码,不指定默认为guest
访问http://ip:15672是RabbitMQ的webUI界面,默认用户名密码为guest/guest
3. go-rabbitMQ客户端
推荐依赖包:github.com/streadway/amqp
demo的目录结构如下:
$ tree
.
├── consumer
│ └── consumer.go
├── go.mod
├── go.sum
├── lib
│ ├── common-func.go
│ └── error.go
├── producer
│ └── producer.go
├── task
│ └── task.go
└── worker
└── worker.go
3.1 客户端连接
// RabbitMQConn 获取rabbitMQ Broker连接
func RabbitMQConn() (conn *amqp.Connection, err error) {
var (
user string = "admin"
pwd string = "admin"
host string = "xx.xx.xx.xx"
port string = "5672"
)
url := "amqp://" + user + ":" + pwd + "@" + host + ":" + port + "/"
// 新建连接
conn, err = amqp.Dial(url)
return
}
3.2 基础队列使用
简单队列模式是RabbitMQ的常规用法,简单理解就是消息生产者发送消息给一个队列,然后消息的消费者从队列中读取消息
当多个消费者订阅同一个队列的时候,队列中的消息是平均分摊给多个消费者处理
首先定义一个消息的生产者producer:
type simpleDemo struct {
Name string `json:"name"`
Addr string `json:"addr"`
}
func main() {
// 连接rabbitMQ服务器
conn, err := lib.RabbitMQConn()
lib.ErrorHandle(err, lib.ErrConnectRabbit)
defer conn.Close()
// 新建一个channel
ch, err := conn.Channel()
lib.ErrorHandle(err, lib.ErrOpenChannel)
defer ch.Close()
// 声明或者创建一个队列来保存消息
q, err := ch.QueueDeclare(
"simple:queue", // name
false, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // argument
)
lib.ErrorHandle(err, lib.ErrDeclareQueue)
data := simpleDemo{
Name: "Tom",
Addr: "Shanghai",
}
dataBytes, err := json.Marshal(data)
lib.ErrorHandle(err, lib.ErrMarshalJSON)
err = ch.Publish(
"", // exchange
q.Name, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
Body: dataBytes,
},
)
lib.ErrorHandle(err, lib.ErrPublishMsg)
log.Printf(" [x] Sent %s", dataBytes)
}
定义消息消费者consumer:
func main() {
conn, err := lib.RabbitMQConn()
lib.ErrorHandle(err, lib.ErrConnectRabbit)
defer conn.Close()
ch, err := conn.Channel()
lib.ErrorHandle(err, lib.ErrOpenChannel)
defer ch.Close()
q, err := ch.QueueDeclare(
"simple:queue", // name
false, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // args
)
lib.ErrorHandle(err, lib.ErrDeclareQueue)
// 定义一个消费者
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
lib.ErrorHandle(err, lib.ErrRegisterConsumer)
go func() {
for d := range msgs {
log.Printf("Received a message: %s", d.Body)
}
}()
log.Printf("[*] Waiting for messages. To exit press CTRL+C")
select {}
}
我们开启一个生产者和一个消费者,运行结果为:
$ go run producer.go
2022/05/19 16:06:25 [x] Sent {"name":"Tom","addr":"Shanghai"}
$ go run consumer.go
2022/05/19 16:06:33 [*] Waiting for messages. To exit press CTRL+C
2022/05/19 16:06:33 Received a message: {"name":"Tom","addr":"Shanghai"}
3.3 工作队列
工作队列也被称为任务队列
任务队列是为了避免等待执行一些耗时的任务,而是将需要执行的任务封装为消息发送给工作队列,后台运行的工作进程将任务消息取出来并执行相关任务
多个后台工作进程同时进行,他们之间共享任务(抢占)
定义一个任务生产者,用于生产任务消息
func bodyFrom(args []string) string {
var s string
if (len(args) < 2) || os.Args[1] == "" {
s = "no task"
} else {
s = strings.Join(args[1:], " ")
}
return s
}
func main() {
conn, err := lib.RabbitMQConn()
lib.ErrorHandle(err, lib.ErrConnectRabbit)
defer conn.Close()
ch, err := conn.Channel()
lib.ErrorHandle(err, lib.ErrOpenChannel)
defer ch.Close()
q, err := ch.QueueDeclare(
"task:queue", // name
false, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // args
)
lib.ErrorHandle(err, lib.ErrDeclareQueue)
body := bodyFrom(os.Args)
err = ch.Publish(
"",
q.Name,
false,
false,
amqp.Publishing{
ContentType: "text/plain",
DeliveryMode: amqp.Persistent,
Body: []byte(body),
},
)
lib.ErrorHandle(err, lib.ErrPublishMsg)
log.Printf("sent %s", body)
}
定义worker:
func main() {
conn, err := lib.RabbitMQConn()
lib.ErrorHandle(err, lib.ErrConnectRabbit)
defer conn.Close()
ch, err := conn.Channel()
lib.ErrorHandle(err, lib.ErrOpenChannel)
defer ch.Close()
q, err := ch.QueueDeclare(
"task:queue",
false,
false,
false,
false,
nil)
lib.ErrorHandle(err, lib.ErrDeclareQueue)
// 将预取计数器设置为1
// 在并行处理中将消息分配给不同的工作进程
err = ch.Qos(
1, // prefetch count
0, // prefetch size
false, // global
)
lib.ErrorHandle(err, lib.ErrSetQoS)
msgs, err := ch.Consume(
q.Name,
"",
false,
false,
false,
false,
nil,
)
lib.ErrorHandle(err, lib.ErrRegisterConsumer)
forever := make(chan bool)
go func() {
for d := range msgs {
log.Printf("Received a message: %s", d.Body)
log.Printf("Done")
d.Ack(false)
}
}()
log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
<-forever
}
我们开启两个worker,运行结果为:
$ go run task.go hello world
2022/05/19 16:03:31 sent hello world
$ go run task.go hello golang
2022/05/19 16:03:53 sent hello golang
$ go run task.go hello rabbitmq
2022/05/19 16:03:59 sent hello rabbitmq
$ go run worker.go
2022/05/19 16:03:44 [*] Waiting for messages. To exit press CTRL+C
2022/05/19 16:03:44 Received a message: hello world
2022/05/19 16:03:44 Done
2022/05/19 16:03:53 Received a message: hello golang
2022/05/19 16:03:53 Done
$ go run worker.go
2022/05/19 16:03:47 [*] Waiting for messages. To exit press CTRL+C
2022/05/19 16:03:59 Received a message: hello rabbitmq
2022/05/19 16:03:59 Done
同时可以查看一下RabbitMQ的webUI,看看我们的工作队列情况
参考:
https://developer.aliyun.com/article/769883
https://blog.csdn.net/qq_42402854/article/details/124820511