RabbitMQ基础
RabbitMQ 用于消息代理,其工作就是接收和转发消息。
术语
生产:即发布消息,发送消息的程序就是一个生产者;Producing
队列:本质是一个巨大的消息缓冲区,大小只受主机内存和硬盘限制,用于存储传输而来的消息,多个生产者可以把消息发送给同一个队列,多个消费者可以从同一个队列获取消息;Queue
消费:即接收信息,接收消息的程序就是一个消费者;Consuming
基本关系:
安装
go get github.com/streadway/amqp
简单操作
检查
创建辅助函数faliOnError
用于检查每个 amqp
调用的返回值
func failOnError(err error, mag string) {
if err != nil {
log.Fatalf("%s: %s", msg, err)
}
}
生产(发送消息)
生产者的创建
amqp.Dial()
函数用于连接 RabbitMQ 服务器,链接嵌套字段用于定义链接的协议和身份验证
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close()
创建一个channel
来传递消息
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close()
创建一个队列供生产者发送消息,可以对队列进行不同的配置用于满足不同的需求,以下是一个匿名队列
声明队列只有在它不存在的情况下才会创建,且消息内容是一个字节数组,可自由进行编写
q, err := ch.QueueDeclare(
"hello", // name
false, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue")
生产者选择交换机等进行生产(消息发送)
body := "Hello World!"
err = ch.Publish(
"", // exchange
q.Name, // routing key
false, // mandatory
false, // immediate
amqp.Publishing {
ContentType: "text/plain",
Body: []byte(body),
})
failOnError(err, "Failed to publish a message")
消费(接收消息)
消费者需要持续运行监听消息。
消费仍旧需要错误返回辅助函数failOnError
消费与生产相似,需要先打开一个连接和一个通道 channel,并声明我们要消费的队列(这里与发送的队列匹配)
// 连接 RabbitMQ
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close()
// 创建通道 Channel
ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close()
// 声明队列,在消费之前可能已经生产了,要确保使用消息之前队列已经存在
q, err := ch.QueueDeclare(
"hello", // name
false, // durable
false, // delete when usused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue")
生产者会异步地向我们发送消息,在goroutine 中读取来自 channel 的消息。
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer")
forever := make(chan bool)
go func() {
for d := range msgs {
log.Printf("Received a message: %s", d.Body)
}
}()
log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
<-forever
工作队列
工作队列:也叫任务队列 Task Queues,为了避免等待一些占用大量资源时间的操作。
当我们把任务 Task 当做消息发送到队列中,运行后台的工作者 Worker 进程会取出任务处理,多个运行中的工作者可以共享任务队列里的任务。
示例中使用 time.sleep()
函数模拟工作者处理任务的情况——字符串的一个.
耗时一秒
示例
消费者
根据示例中的消费代码进行更改,它需要为消息体中的每一个.
模拟 1s 的操作。
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer")
forever := make(chan bool)
go func() {
for d := range msgs {
log.Printf("Received a message: %s", d.Body)
// 获取消息体中的点的数量
dot_count := bytes.Count(d.Body, []byte("."))
// 模拟
t := time.Duration(dot_count)
time.Sleep(t * time.Second)
log.Printf("Done")
}
}()
log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
<-forever
生产者
根据示例中的生产代码进行更改,让其允许从命令行发送任意消息。
消息体定义bodyFrom
func bodyFrom(args []string) string {
var s string
if (len(args) < 2) || os.Args[1] == "" {
s = "hello"
} else {
s = strings.Join(args[1:], " ")
}
return s
}
生产,发送消息
body := bodyFrom(os.Args)
err = ch.Publish(
"", // exchange
q.Name, // routing key
false, // mandatory
false,
amqp.Publishing {
DeliveryMode: amqp.Persistent,
ContentType: "text/plain",
Body: []byte(body),
})
failOnError(err, "Failed to publish a message")
log.Printf(" [x] Sent %s", body)
循环调度
工作队列能够并行处理队列,如果堆积了很多的任务,只需要添加更多的工作者就可以了。
我们同时运行两个消费者进行测试,然后发布多个任务
然后发现两个消费者会轮流对工作队列里的任务进行处理:
终端:productor
pillow@Mac-Pro rabbitMQ % go run new_task.go
2023/08/14 17:43:45 [x] Sent hello.....................
pillow@Mac-Pro rabbitMQ % go run new_task.go First message.
2023/08/14 17:44:22 [x] Sent First message.
pillow@Mac-Pro rabbitMQ % go run new_task.go Second message..
go run new_task.go Third message...
go run new_task.go Fourth message....
go run new_task.go Fifth message.....
2023/08/14 17:44:36 [x] Sent Second message..
2023/08/14 17:44:36 [x] Sent Third message...
2023/08/14 17:44:37 [x] Sent Fourth message....
2023/08/14 17:44:37 [x] Sent Fifth message.....
pillow@Mac-Pro rabbitMQ %
终端:consumer1
2023/08/14 17:43:45 Received a message: hello.....................
2023/08/14 17:44:06 Done
2023/08/14 17:44:36 Received a message: Second message..
2023/08/14 17:44:38 Done
2023/08/14 17:44:38 Received a message: Fourth message....
2023/08/14 17:44:42 Done
终端:consumer2
2023/08/14 17:44:22 Received a message: First message.
2023/08/14 17:44:23 Done
2023/08/14 17:44:36 Received a message: Third message...
2023/08/14 17:44:39 Done
2023/08/14 17:44:39 Received a message: Fifth message.....
2023/08/14 17:44:44 Done
消息确认
消息任务会在被发送给消费者后从内存中移除,此时若是把工作者停止,正在处理的消息任务就会消失
防止消息丢失,使用消息响应功能,消费者通过 ack 响应告诉 RabbitMQ 已经接收到并处理了某条消息,然后 RabbitMQ 就会释放并删除消息。
示例中使用手动消息确认,通过auto-ack
参数传递false
,有消息完成时,d.Ack(falsez)
想 RabbitMQ 发送消费完成的确认(单词传递)
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
false, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
failOnError(err, "Failed to register a consumer")
forever := make(chan bool)
go func() {
for d := range msgs {
log.Printf("Received a message: %s", d.Body)
dot_count := bytes.Count(d.Body, []byte("."))
t := time.Duration(dot_count)
time.Sleep(t * time.Second)
log.Printf("Done")
d.Ack(false)
}
}()
log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
<-forever
注意:忘记确认会使消息任务在 RabbitMQ 缓存堆积,使其占用越来越多的内存
可以使用rabbitmqctl
命令,输出messages_unacknowledged
字段。
消息持久化
默认情况下,RabbitMQ 在退出或者崩溃时,将会丢失所有的队列和消息,我们可以将队列和消息通过设置进行持久化存储,以避免消息丢失。
队列持久化
durable = true
q, err := ch.QueueDeclare(
"task_queue", // name
true, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue")
消息持久化
amqp.Publishing.DeliveryMode = amqp.Persistent
err = ch.Publish(
"", // exchange
q.Name, // routing key
false, // mandatory
false,
amqp.Publishing {
DeliveryMode: amqp.Persistent,
ContentType: "text/plain",
Body: []byte(body),
})
此种方法使用 faync(2)
的消息会被存储到硬盘,其他可能会暂存在缓存中,强力的保证需要使用publisher confirms
公平调度
每个工作者处理的消息有轻重,我们可以告诉 RabbitMQ 一次只向一个工作者发送一条消息,并在确认前一个消息之前,不要向工作者发送新的消息。
err = ch.Qos(
1, // prefetch count
0, // prefetch size
false, // global
)
failOnError(err, "Failed to set QoS")
发布订阅
将一个消息同时发给多个消费者,这种模式被称为发布/订阅,应用场景比如一个日志系统。
交换机
发布者的工作很简单,仅仅是将消息发出,但不会将任何消息直接发送给队列,也不知道消息是否被投递到队列。
发布者发布的消息被投递到交换机,交换机一遍接收消息,一边将其推送到队列或是忽略消息,对于消息的处理规则通过交换机定义。
rabbitmqctl
:列出服务器上所有的交换器;
sudo rabbitmqctl list_exchanges
:列出所有交换器包括匿名交换器(amq.*)
供选择的交换机类型:
- direct:直连交换机
- topic:主题交换机
- headers:
- fanout:将消息发送给它知道的所有队列
绑定
绑定用于描述交换机与队列之间的联系。
rabbitmqctl list_bindings
:列出所有现存的绑定
routing key = ""
为交换机配置绑定健,绑定建的意义取决于交换机的类型。
direct 直连交换机
直连交换机会对binding key
和routing key
进行精确匹配,从而确定消息该分发到哪个队列。
消息推送到routing key = orange
的交换机时,会被路由到队列 Q1 里,routing key = black || green
会被路由到 Q2 里。
多个队列使用相同的binding key
是合法的,当有与其相同的routing key
的消息推送到交换机时,拥有相同的binding key
的队列会同时被交换机匹配。
topic 主题交换机
direct 直连交换机无法基于多个标准执行路由操作,使用 topic主题交换机可以实现。
发送的 topic 主题交换机的消息携带的routing key
可以总长度不超过 255 字节的多个字段,这些字段以.
分隔开,像是user.order.cancel
表示用户订单取消,runner.order.cancel
表示骑手订单取消,user.errand.cancel
表示用户服务取消。
接收信息的 topic 主题交换机的binding key
也需要有相同的格式,但是是使用类似通配符匹配的规则来对消息进行匹配(或者叫筛选),规则为:
*
(星号) 用来表示一个单词#
(井号) 用来表示任意数量(零个或多个)单词
对于每个字段的含义要提前思考清晰,例如上述的三个例子的字段含义为<table>.<table>.<cancel>
第一个表对象对第二个表对象执行的操作。
根据上述规则创建三个绑定,像是:
binding key: *.order.*
表示被执行操作为 order 表对象的消息被路由到此交换机指定的队列
binding key: runner.*.*
表示执行操作为 runner 表对象的消息会被路由到此交换机指定的队列
若是有发送的消息的routing key
字段数不正确,此消息不会被投递到任何一个队列,而是会丢失
??
- 绑定键为
*
的队列会取到一个routing key为空的消息吗? - 绑定键为
#.*
的队列会获取到一个名为..
的路由键的消息吗?它会取到一个routing key为单个单词的消息吗? a.*.#
和a.#
的区别在哪儿?
RPC远程过程调用
RPC(Remote Procedure Call)远程过程调用协议,一种通过网络从远程计算机上请求服务,而不需要了解底层网络技术的协议。
通过 RabbitMQ 实现 RPC 很容易——一个客户端发送请求消息,服务器端将其应用到一个回复信息中,为了接收到回复信息,客户端需要在发送请求的同时发送一个回调队列callback queue
的地址:
q, err := ch.QueueDeclare(
"", // name
false, // durable
false, // delete when usused
true, // exclusive
false, // noWait
nil, // arguments
)
err = ch.Publish(
"", // exchange
"rpc_queue", // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
CorrelationId: corrId,
ReplyTo: q.Name,
Body: []byte(strconv.Itoa(n)),
})
amqp协议
给消息预定义了 14 个属性,常用的有:
persistent
(持久性):将消息标记为持久性(值为true)或瞬态(false)。第二篇教程中有说明这个属性。content_type
(内容类型):用来描述编码的mime-type。例如在实际使用中常常使用application/json
来描述JOSN编码类型。reply_to
(回复目标):通常用来命名回调队列。correlation_id
(关联标识):用来将RPC的响应和请求关联起来。
correlation_id
用于分辨响应对应的请求,我们为每个请求设置唯一值,当我们从回调队列中接收到一个消息时,我们可以查看这条消息的correlation_id
属性从而将响应与请求匹配起来,若是属性值是未知的,则直接销毁即可。
工作流程:
- 客户端启动,创建匿名独享的回调队列
- RPC 中,客户端发送带有两个属性的消息:
reply_to
指定回调队列,correlation_id
设定唯一值用于匹配请求和响应。 - RPC 中,服务端接受请求,执行请求并将结果消息发送到
reply_to
指定队列。 - 客户端等待回调队列里的数据,通过
correlation_id
属性匹配请求并返回应用。
总结
RabbitMQ 的工作模型为:
配置
生产者和消费者都需要先创建连接 Conn 以及创建通道 Channel
创建连接 Conn
// 连接到 RabbitMQ 服务器
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
// 处理错误
FailOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close()
MQURL 格式 amqp://账号:密码@rabbitmq服务器地址:端口号/vhost (默认是5672端口)
端口可在 /etc/rabbitmq/rabbitmq-env.conf 配置文件设置,也可以启动后通过netstat -tlnp查看
创建通道 Channel
// 配置一个 channel 传递消息
ch, err := conn.Channel()
// 处理错误
FailOnError(err, "Failed to open a channel")
defer ch.Close()
生产者
声明队列 Queue
若是消费者未声明队列,消息被 Exchange 交换机接收后没有在消费者中匹配到 Queue 队列,消息会被丢弃;
若是生产者未声明队列,消费者是无法定于或者获取不存在的 MessageQueue 中的信息;
为避免以上两种可能的情况,最好两者一起声明,不必担心队列的重复声明,若是生产者尝试建立一个已经存在的消息队列,RabbitMQ 不会做任何事情,返回客户端建立成功。如果声明中的属性与已存在队列的属性有差异,那么一个错误代码为406的通道级异常就会被抛出。
// 发送前需声明一个队列供我们发送,然后才能够向队列发送消息
q, err := ch.QueueDeclare(
"", //name
false, //durable
false, //delete when unused
false, //exclusive
false, //no-wait
nil, //arguments
)
// 处理错误
FailOnError(err, "Failed to declare a queue")
name
:队列名称,限制为 255 字节 utf-8 字符串durable
:是否持久化,RabbitMQ 关闭或者崩溃后持久化的队列仍旧在磁盘上delete when unused
:是否自动删除,只有在队列有消费者连接过且连接这个队列的消费者都断开时才会自动删除exclusive
:是否独占,仅对当前 Conn 连接可见,属于此 Conn 的队列可访问该队列no-wait
:是否阻塞arguments
:一些消息代理用他来完成类似与TTL的某些额外功能
声明交换机 Exchange
err = ch.ExchangeDeclare(
"", // name
"", // type
true, // durable
false, // auto-deleted
false, // internal
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare an exchange")
交换机是用来发送消息的 AMQP 实体,交换机接收到消息后根据路由算法将其路由到队列,路由算法是由交换机类型和绑定规则决定的,AMQP 0-9-1 代理提供了四种交换机
direct
:直连交换机fanout
:扇型交换机topic
:主题交换机header
:头交换机
直连交换机
直连交换机会根据信息携带的路由键routing key
将信息投递给对应队列。
直连交换机会将一个队列绑定到某个交换机上,同时赋予该绑定一个路由键,当一个携带者路由键为R
的信息被发送给直连交换机时,交换机会把它路由给绑定值同样为R
的队列。
扇型交换机
扇形交换机会将信息路由给绑定到它身上的所有队列
当有信息发送给此扇型交换机时,交换机会将信息的拷贝分别发给这所有的 N 个队列。
主题交换机
主题交换机通过对信息路由健和队列到交换机的绑定模式之间的匹配,将信息路由给一个或者多个队列
发送的 topic 主题交换机的消息携带的routing key
可以总长度不超过 255 字节的多个字段,这些字段以.
分隔开,像是user.order.cancel
表示用户订单取消,runner.order.cancel
表示骑手订单取消,user.errand.cancel
表示用户服务取消。
接收信息的 topic 主题交换机的binding key
也需要有相同的格式,但是是使用类似通配符匹配的规则来对消息进行匹配(或者叫筛选),规则为:
*
(星号) 用来表示一个单词#
(井号) 用来表示任意数量(零个或多个)单词
头交换机
头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。
当"x-match"设置为“any”时,消息头的任意一个值被匹配就可以满足条件,而当"x-match"设置为“all”的时候,就需要消息头的所有值都匹配成功。
头交换机可以视为直连交换机的另一种表现形式。头交换机能够像直连交换机一样工作,不同之处在于头交换机的路由规则是建立在头属性值之上,而不是路由键。路由键必须是一个字符串,而头属性值则没有这个约束,它们甚至可以是整数或者哈希值(字典)等。
创建 Binding
绑定用于描述交换机与队列之间的联系。
// 建立绑定,可建立多个绑定
err = ch.QueueBind(
q.Name, // queue name
"", // routing key
"logs", // exchange
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to bind a queue")
queue name
:绑定的队列名称routing key
:用于消息路由分发exchange
:绑定的交换机名称no-wait
:是否阻塞arguments
:额外属性
发送消息
err = ch.Publish(
"logs_topic", // exchange
severityFrom(os.Args), // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(body),
})
failOnError(err, "Failed to publish a message")
exchange
:交换机名称routing key
:用于信息路由发送mandatory
:是否返回信息(匹配队列)immediate
:是否返回信息(匹配消费者)ContentType
:信息内容类型Body
:信息内容
消费者
声明队列 Queue
q, err := ch.QueueDeclare(
"hello", // name
false, // durable
false, // delete when usused
false, // exclusive
false, // no-wait
nil, // arguments
)
FailOnError(err, "Failed to declare a queue")
与生产者一样
name
:队列名称,限制为 255 字节 utf-8 字符串durable
:是否持久化,RabbitMQ 关闭或者崩溃后持久化的队列仍旧在磁盘上delete when unused
:是否自动删除,只有在队列有消费者连接过且连接这个队列的消费者都断开时才会自动删除exclusive
:是否独占,仅对当前 Conn 连接可见,属于此 Conn 的队列可访问该队列no-wait
:是否阻塞arguments
:一些消息代理用他来完成类似与TTL的某些额外功能
取消息
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
FailOnError(err, "Failed to register a consumer")
queue
:队列名称consumer
:消费者名称,用来区分多个消费者,可用于实现公平分发等auto-ack
:是否自动应答exclusive
:是否独占,为 true 则队列中只能有一个消费者no-local
:是否为非本地,为 true 则仅接受其他 Conn 中发送的消息no-wait
:队列消费是否阻塞args
:额外属性