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

安装教程

常用命令

启动监控管理器: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

常见故障

集群状态异常
  1. rabbitmqctl cluster_status检查集群健康状态,不正常节点重新加入集群
  2. 分析是否节点挂掉,手动启动节点。
  3. 保证网络连通正常
队列阻塞、数据堆积
  1. 保证网络连通正常
  2. 保证消费者正常消费,消费速度大于生产速度
  3. 保证服务器TCP连接限制合理
脑裂[1][2]
  1. 按正确顺序重启集群
  2. 保证网络连通正常
  3. 保证磁盘空间、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
 }
posted @ 2023-01-24 23:52  TomiokapEace  阅读(63)  评论(0编辑  收藏  举报