RabbitMQ学习
这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天
简介
昨天提到了RabbitMQ是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP,STOMP,也正是如此,使的它变的非常重量级,更适合于企业级的开发。同时实现了Broker架构,核心思想是生产者不会将消息直接发送给队列,消息在发送给客户端时先在中心队列排队。对路由(Routing),负载均衡(Load balance)、数据持久化都有很好的支持。多用于进行企业级的ESB整合。
可靠性
通过一些机制例如,持久化,传输确认等来确保消息传递的可靠性
拓展性
多个RabbitMQ节点可以组成集群
高可用性
队列可以在RabbitMQ集群中设置镜像,如此一来即使部分节点挂掉了,但是队列仍然可以使用
多种协议
原生的支持AMQP,也能支持STOMP,MQTT等协议
丰富的客户端
我们常用的编程语言都支持RabbitMQ
管理界面
自带提供一个WEB管理界面
插件机制
RabbitMQ 自己提供了很多插件,可以按需要进行拓展 Plugins
应用场景
对于一个大型的软件系统来说,它会有很多的组件或者说模块或者说子系统或者(subsystem or Component or submodule)。那么这些模块的如何通信?这和传统的IPC有很大的区别。传统的IPC很多都是在单一系统上的,模块耦合性很大,不适合扩展(Scalability);如果使用socket那么不同的模块的确可以部署到不同的机器上,但是还是有很多问题需要解决。比如: 1)信息的发送者和接收者如何维持这个连接,如果一方的连接中断,这期间的数据如何方式丢失? 2)如何降低发送者和接收者的耦合度? 3)如何让Priority高的接收者先接到数据? 4)如何做到load balance?有效均衡接收者的负载? 5)如何有效的将数据发送到相关的接收者?也就是说将接收者subscribe 不同的数据,如何做有效的filter。 6)如何做到可扩展,甚至将这个通信模块发到cluster上? 7)如何保证接收者接收到了完整,正确的数据? AMDQ协议解决了以上的问题,而RabbitMQ实现了AMQP。
AMQP
一个提供统一消息服务的应用层标准高级消息队列协议,是一个通用的应用层协议
消息发送与接受的双方遵守这个协议可以实现异步通讯。这个协议约定了消息的格式和工作方式。
https://www.cnblogs.com/frankyou/p/5283539.html
安装教程
- Windows Linux安装教程
- Mac 安装教程 安装成功后打开浏览器,访问 http://localhost:15672
常用命令
启动监控管理器:rabbitmq-plugins enable rabbitmq_management 关闭监控管理器:rabbitmq-plugins disable rabbitmq_management 启动rabbitmq:rabbitmq-service start 关闭rabbitmq:rabbitmq-service stop 查看所有的队列:rabbitmqctl list_queues 清除所有的队列:rabbitmqctl reset 关闭应用:rabbitmqctl stop_app 启动应用:rabbitmqctl start_app
用户和权限设置
添加用户:rabbitmqctl add_user username password 分配角色:rabbitmqctl set_user_tags username administrator 新增虚拟主机:rabbitmqctl add_vhost vhost_name 将新虚拟主机授权给新用户:rabbitmqctl set_permissions -p vhost_name username “.*” “.*” “.*”
(后面三个”*”代表用户拥有配置、写、读全部权限)
角色说明
- 超级管理员(administrator) 可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。
- 监控者(monitoring) 可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)
- 策略制定者(policymaker) 可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。
- 普通管理者(management) 仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。
- 其他 无法登陆管理控制台,通常就是普通的生产者和消费者。
消息的可靠性
Message durability (消息持久化)
将保存在内存中的数据都写入磁盘,防止服务器重启后数据丢失;有哪些数据需要持久化保存呢?
元数据、消息需要持久化到磁盘;
磁盘节点:持久化的消息在到达队列时就被写入到磁盘,并且如果可以,持久化的消息也会在内存中保存一份备份,这样可以提高一定的性能,只有在内存吃紧的时候才会从内存中清除;
内存节点:非持久化的消息一般只保存在内存中,在内存吃紧的时候会被换入到磁盘中,以节省内存空间;
Message acknowledgment
在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完成就宕机(或出现其他意外)的情况,这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(Message acknowledgment)后才将该消息从Queue中移除。
如果一个Queue没被任何的Consumer Subscribe(订阅),当有数据到达时,这个数据会被cache,不会被丢弃。当有Consumer时,这个数据会被立即发送到这个Consumer。这个数据被Consumer正确收到时,这个数据就被从Queue中删除。
那么什么是正确收到呢?通过ACK。每个Message都要被acknowledged(确认,ACK)。我们可以显示的在程序中去ACK,也可以自动的ACK。如果有数据没有被ACK,那么RabbitMQ Server会把这个信息发送到下一个Consumer。
生产者消息确认机制
如何知道消息有没有正确到达exchange呢?
1、通过AMQP提供的事务机制实现:
消息中间件——RabbitMQ(三)理解RabbitMQ核心概念和AMQP协议! - 掘金 (juejin.cn)
2、通过生产者消息确认机制(publisher confirm)实现:
RabbitMQ系列(四)RabbitMQ事务和Confirm发送方消息确认--深入解读 - 掘金juejin.im/post/5b54681bf265da0f82023014
常见故障
集群状态异常
rabbitmqctl cluster_status
检查集群健康状态,不正常节点重新加入集群- 分析是否节点挂掉,手动启动节点。
- 保证网络连通正常
队列阻塞、数据堆积
- 保证网络连通正常
- 保证消费者正常消费,消费速度大于生产速度
- 保证服务器TCP连接限制合理
脑裂[1][2]
- 按正确顺序重启集群
- 保证网络连通正常
- 保证磁盘空间、cpu、内存足够
内存使用量超过阀值
=INFO REPORT==== 15-Mar-2022::03:21:13 ===
rabbit on node 'rabbit@hb-lvs-rabbitmq-mem-1' down
=INFO REPORT==== 15-Mar-2022::03:21:16 ===
vm_memory_high_watermark set. Memory used:3194473120 allowed:3188020019
=WARNING REPORT==== 15-Mar-2022::03:21:16 ===
memory resource limit alarm set on node 'rabbit@hb-lvs-rabbitmq-disk-2'.
**********************************************************
*** Publishers will be blocked until this alarm clears ***
**********************************************************
这时会阻塞生产者,以避免服务崩溃。
临时解决办法是调大内存阀值,默认是0.4;
rabbitmqctl set_vmmemory_high_watermark 0.6
Golang 操作RabbitMQ
RabbitMQ 支持我们常见的编程语言,此处我们使用 Golang 来操作
Golang操作RabbitMQ的前提我们需要有个RabbitMQ的服务端,至于RabbitMQ的服务怎么搭建我们此处就不详细描述了.
Golang操作RabbitMQ的客户端包,网上已经有一个很流行的了,而且也是RabbitMQ官网比较推荐的,不需要我们再从头开始构建一个RabbitMQ的Go语言客户端包.
go get github.com/streadway/amqp
项目目录
___lib
______commonFunc.go
___producer.go
___comsumer.go
commonFunc.go
package lib
import (
"github.com/streadway/amqp"
"log"
)
// RabbitMQ连接函数
func RabbitMQConn() (conn *amqp.Connection,err error){
// RabbitMQ分配的用户名称
var user string = "admin"
// RabbitMQ用户的密码
var pwd string = "123456"
// RabbitMQ Broker 的ip地址
var host string = "192.168.230.132"
// RabbitMQ Broker 监听的端口
var port string = "5672"
url := "amqp://"+user+":"+pwd+"@"+host+":"+port+"/"
// 新建一个连接
conn,err =amqp.Dial(url)
// 返回连接和错误
return
}
// 错误处理函数
func ErrorHanding(err error, msg string){
if err != nil{
log.Fatalf("%s: %s", msg, err)
}
}
基础队列使用
简单队列模式是RabbitMQ的常规用法,简单理解就是消息生产者发送消息给一个队列,然后消息的消息的消费者从队列中读取消息
当多个消费者订阅同一个队列的时候,队列中的消息是平均分摊给多个消费者处理
producer.go
定义一个消息的生产者
package main
import (
"encoding/json"
"log"
"myDemo/rabbitmq_demo/lib"
"github.com/streadway/amqp"
)
type simpleDemo struct {
Name string `json:"name"`
Addr string `json:"addr"`
}
func main() {
// 连接RabbitMQ服务器
conn, err := lib.RabbitMQConn()
lib.ErrorHanding(err, "Failed to connect to RabbitMQ")
// 关闭连接
defer conn.Close()
// 新建一个通道
ch, err := conn.Channel()
lib.ErrorHanding(err, "Failed to open a channel")
// 关闭通道
defer ch.Close()
// 声明或者创建一个队列用来保存消息
q, err := ch.QueueDeclare(
// 队列名称
"simple:queue", // name
false, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
lib.ErrorHanding(err, "Failed to declare a queue")
data := simpleDemo{
Name: "Tom",
Addr: "Beijing",
}
dataBytes,err := json.Marshal(data)
if err != nil{
lib.ErrorHanding(err,"struct to json failed")
}
err = ch.Publish(
"", // exchange
q.Name, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
Body: dataBytes,
})
log.Printf(" [x] Sent %s", dataBytes)
lib.ErrorHanding(err, "Failed to publish a message")
}
comsumer.go
定义一个消息的消费者
package main
import (
"log"
"myDemo/rabbitmq_demo/lib"
)
func main() {
conn, err := lib.RabbitMQConn()
lib.ErrorHanding(err,"failed to connect to RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
lib.ErrorHanding(err,"failed to open a channel")
defer ch.Close()
q, err := ch.QueueDeclare(
"simple:queue", // name
false, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
lib.ErrorHanding(err,"Failed to declare a queue")
// 定义一个消费者
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
lib.ErrorHanding(err,"Failed to register a consume")
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 {}
}
工作队列
工作队列也称为 任务队列
任务队列是为了避免等待执行一些耗时的任务,而是将需要执行的任务封装为消息发送给工作队列,后台运行的工作进程将任务消息取出来并执行相关任务 , 多个后台工作进程同时间进行,那么任务在他们之间共享
task.go
我们定义一个任务的生产者,用于生产任务消息
package main
import (
"github.com/streadway/amqp"
"log"
"myDemo/rabbitmq_demo/lib"
"os"
"strings"
)
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() {
// 连接RabbitMQ服务器
conn, err := lib.RabbitMQConn()
lib.ErrorHanding(err, "Failed to connect to RabbitMQ")
// 关闭连接
defer conn.Close()
// 新建一个通道
ch, err := conn.Channel()
lib.ErrorHanding(err, "Failed to open a channel")
// 关闭通道
defer ch.Close()
// 声明或者创建一个队列用来保存消息
q, err := ch.QueueDeclare(
// 队列名称
"task:queue", // name
false, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
lib.ErrorHanding(err, "Failed to declare a queue")
body := bodyFrom(os.Args)
err = ch.Publish(
"",
q.Name,
false,
false,
amqp.Publishing{
ContentType: "text/plain",
// 将消息标记为持久消息
DeliveryMode: amqp.Persistent,
Body: []byte(body),
})
lib.ErrorHanding(err, "Failed to publish a message")
log.Printf("sent %s", body)
}
worker.go
定义一个工作者,用于消费掉任务消息
package main
import (
"log"
"myDemo/rabbitmq_demo/lib"
)
func main() {
conn, err := lib.RabbitMQConn()
lib.ErrorHanding(err, "Failed to connect to RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
lib.ErrorHanding(err, "Failed to open a channel")
defer ch.Close()
q, err := ch.QueueDeclare(
"task:queue", // name
false, // durable
false, // delete when unused
false, // exclusive
false, // no-wait
nil, // arguments
)
lib.ErrorHanding(err, "Failed to declare a queue")
// 将预取计数器设置为1
// 在并行处理中将消息分配给不同的工作进程
err = ch.Qos(
1, // prefetch count
0, // prefetch size
false, // global
)
lib.ErrorHanding(err, "Failed to set QoS")
msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
false, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
lib.ErrorHanding(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("Done")
d.Ack(false)
}
}()
log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
<-forever
}