RabbitMQ学习笔记(原理、多方式安装和配置修改、整合Springboot、死信队列、延迟队列、备份交换机、动态监听、集群搭建)

前言
文章内容有点多,可以根据需要进行跳转,一文看懂rabbitmq
最近公司用到rabbitmq做消息转发服务,将学到的一些内容记录在这,内容会有些繁杂有些内容也不全面,后面根据实际使用会进行一定的补充,文章引用了不少网上的内容这里就不一 一列举感谢大佬们的知识分享,由于刚刚开始学习消息队列,错误之处感谢指正。
2022-6-24 更新非root用户安装方式和systemctl配置


作用: 同步变异步、流量削峰、应用解耦、可扩展

1. 概念

消息系统的作用:允许软件和应用之间相互连接、扩展,可以通过将消息的发送和接收分离来实现应用程序的异步和解耦。
AMPQ(高级消息队列协议),是面向消息中间件的开放标准的二进制应用层协议。是一个公开标准,大家都可以基于这个标准来实现消息中间件,不受到开发语言与产品的制约;

常用的消息中间件:JMS Kafka RocketMQ RabbitMQ ActiveMQ Pulsar等等
MQ技术的发展史:
image.png
Kafka是一种分布式流式系统,被设计为能够作为一个统一平台来处理大型公司可能拥有的所有实时数据馈送。为此,它必须具有高吞吐量才能支持大容量事件流。
RabbitMQ支持多种客户端,如Python、Java、.NET、C、Ruby等,在易用性、扩展性、高可用性等方面表现都不错,并且可以与SpringAMQP完美整合,API丰富易用。
消息队列概念
我们谈到消息队列就会想到:生产者、消费者和消息队列。生产者将消息发送到消息队列,消费者从消息队列中获取消息然后进行处理。队列可以主动将消息推送给订阅队列的消费者,消费者也可以自己主动从消息队列pull和fetch消息。

image.pngimage.png

2. AMPQ

交换机

交换机是发送消息的实体,交换机收到消息然后将其路由到一个或多个队列中。
交换机共有四种类型:

交换类型 默认的预定义名称
直连交换机(Direct exchange) 空字符串和amq.direct 一对一
扇形交换机(Fanout exchange) amq.fanout 广播
主题交换机(Topic exchange) amq.topic 一对多,模糊匹配
头信息交换机(Headers exchange) amq.match 和RabbitMQ中的 amq.headers

交换机的属性

  • Name – 交换机名称
  • 持久性 – 保证交换机的broker重启后仍然存在,如果交换机没有指定持久那么重启之后交换机的broker就不存在了,需要重新声明
  • 自动删除 – 当交换机与所绑定的最后一个队列解除绑定时,交换机就会被删除
  • 参数 – 可选

默认交换机
默认交换机是由broker预先声明的匿名直连交换机。使用默认交换机的时候每个新建的队列都会绑定到默认交换机上,绑定的路由与队列名称相同。
直连交换机根据消息路由键(router_key)将消息传递到队列,消息将会投递到与路由键名称和队列名称相同的队列上。直接交换机是消息单播路由的理想选择(尽管它们也可以用于多播路由)。
扇形交换机将消息路由到绑定到它的所有队列,并且忽略路由键。也就是说,当新消息发布到该交换机时,该消息的副本将投递到所有绑定该交换机的队列。扇形交换机是消息广播路由的理想选择
主题交换机根据消息路由键和和用于将队列绑定到交换机的模式匹配字符串之间的匹配将消息路由到一个或者多个队列。
也就是说通过消息的路由键去匹配到绑定到交换机的路由键匹配字符串,如果匹配上了,就进行投递消息。
头交换机不依赖路由键的匹配规则来路由消息,而是根据发送消息内容中的请求头属性进行匹配。
头交换机类似于直连交换机,但是直连交换机的路由键必须是一个字符串,而请求头信息则没有这个约束,它们甚至可以是整数或者字典。因此可以用作路由键不必是字符串的直连交换。
绑定一个队列到头交换机上的时候,会同时绑定多个用于匹配的头信息。

队列

我们在使用队列之前需要先对队列进行声明,声明时如果队列不存在则创建一个队列,如果队列存在判断存在的队列属性是否与声明中的属性相同,相同则不再创建,否则引发错误。
队列名称
可主动设置队列名称,如果队列名称为空broker会为队列生成一个唯一的队列名称,一起返回给客户端。队列名称限制255字节以内的utf-8 字符。

不可声明amq开头的队列名称,此关键字保留给broker内部使用,否则会出现异常。

队列持久化
队列持久化的元数据会存储到硬盘中,broker重启后,队列依然存在。没有被持久化的队列称为暂存队列。发布的消息也有同样的区分,也就是说,持久化的队列并不会使得路由到它的消息也具有持久性,需要手动把消息也标记为持久化才能保证消息的持久性。

消费者

消息如果一直存储在队列中没有被消费就没有什么实际意义。
消费者获取消息的两种方式

  • 消费者订阅队列,队列可以直接将消息push到对应的消费者
  • 主动去队列获取,主动去消息队列轮询pull,效率低

一个队列可以有多个消费者进行订阅,队列可以向所有订阅的消费者发送广播。

消息确认

消费者应用程序可能偶尔无法处理单个消息或有时会崩溃,另外网络问题也有可能导致问题。这就提出了一个问题:**Broker何时应该从队列中删除消息?**AMQP 0-9-1 规范中约定让消费者对此进行控制,有两种确认模式:

  • 自动确认模式:在Broker向应用程序发送消息之后(使用basic.deliver或basic.get-ok方法),将消息从消息队列中删除;
  • 显示确认模式:在应用程序向broker发回确认之后(使用basic.ack方法),将消息从消息队列中删除。

在显示模式下,应用程序选择何时发送确认消息。如果消费者在没有发送确认的情况下就挂掉了,那么Broker会将其重新投递给另一个消费者,如果此时没有可用的消费者,那么Broker将等到至少有一个消费者注册到该队列时,再尝试重新投递消息。
另外,如果应用程序崩溃(当连接关闭时 AMQP Broker会感知到这一点),并且AMQP Broker在预期的时间内未收到消息确认,则消息将重新入队,如果此时有其他消费者,可能立即传递给另一个消费者。为此,我们的消费者做好业务的幂等处理也是非常重要的

持久化

持久化的消息会同时写入磁盘和内存,非持久化的消息会写入内存,内存不够时会写入磁盘(重启就丢失了)。
队列持久化:
rabbitMQ声明队列时durable参数设置为true
消息持久化:
生产者发送消息时修改代码属性,将props参数设置为MessageProperties.PERSISTENT_TEXT_PLAIN
spring中默认的message就是持久化的,如何改变持久化属性?
1、使用send方法,发送message。设置message中MessageProperties的属性deliveryMode
2、自定义MessageConverter,在消息转换时,设置MessageProperties的属性deliveryMode
3、自定MessagePropertiesConverter,在MessageProperties对象转换成BasicProperties时,设置deliveryMode

保证消息不丢失

  • 队列持久化
  • 消息持久化
  • 交换机持久化
  • 生产者引入事务机制和confirm机制确保消息正确发送到broker
  • 可以引入mirrored-queue镜像队列,相当于配置了主从
  • 备份交换机
发送者确认模式开启,消息持久化默认开启,消费者消费开启手动ack
生产者丢数据:RabbitMQ提供transaction(事务,支持回滚)和confirm模式(ACK给生产者)来确保生产者不丢消息;
消息队列丢数据:开启rabbitmq的持久化,就是消息写入之后会持久化到磁盘,持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack。
消费者丢失数据:消费者丢数据一般是因为采用了自动确认消息模式,改为手动确认消息,处理消息成功后,手动回复确认消息。

交换机备份也可以队列不可用的问题,工作流程当一个生产者发送一条消息时,当这条消息无法被路由到队列时,这消息就会被推到备用交换机上,备用交换机是一个广播交换机拥有2个队列,1备份队列(用于存储和备份消息),2告警队列(用于发送告警信息)在原有的基础上进行改造增加备用交换机,以及备份队列与告警队列,和2个消费者


内存告警和磁盘告警
内存告警
内存使用超过配置的阈值或者磁盘剩余空间低于配置阈值时,MQ会暂时阻塞客户端的连接,停止接收从客户端发来的消息从而避免服务崩溃,客服端与服务端的心跳检测也会失效。
1. 当内存告警时可通过一下命令临时调整内存大小

RabbitMQctl set_vm_memory_high_watermark <fraction>

fraction为内存阈值,RabbitMQ默认是0.4,表示当RabbitMQ使用的内存超过总内存的40%时,就会产生告警并阻塞所有生产则连接。
通过此命令修改的阈值在RabbitMQ重启之后将会失效,通过修改配置文件的方式设置的阈值才会永久有效,但需要重启服务才会生效。

  1. 修改配置文件 rabbitMQ.conf
#相对值,也就是前面的fraction,建议设置在0.4~0.66之间,不要超过0.7
vm_memory_high_watermark.relative=0.4
#绝对值,单位为KB,MB,GB,对应的临时命令是:RabbitMQctl set_vm_memory_high_watermark absolute <value>
#vm_memory_high_watermark.absolute=1GB

内存换页
当broker的结点触发内存并阻塞生产者之前,会尝试将队列内存中消息换页存储到磁盘以释放内存空间。持久化和非持久化的消息都被转储到磁盘中,持久化的消息在磁盘中存在备份,所以这里会将持久化的消息清除掉。一般当内存达到内存阈值的50%时就会进行换页操作。
可以通过配置文件配置换页设置

vm_memory_high_watermark_paging_ratio=0.75

磁盘告警
当磁盘的剩余空间低于设定的阈值时会阻塞生产者,这样可以避免因非持久化消息持续换页耗尽磁盘空间导致服务崩溃。一般默认的磁盘阈值是50M,一般将阈值设置为与操作系统内存大小相同。
通过命令临时修改磁盘阈值

v#设置具体大小,单位为KB/MB/GB
RabbitMQctl set_disk_free_limit <disk_limit>
#设置相对值,建议取值为1.0~2.0(相对于内存的倍数,如内存大小是8G,若为1.0,则表示磁盘剩余8G时,阻塞)
RabbitMQctl set_disk_free_limit mem_relative <fraction>

修改配置文件

对应的配置文件配置如下:
disk_free_limit.relative=2.0
#disk_free_limit_absolute=50MB

消息保存到磁盘中的格式
消息保存与$MNESIA/msg_store_persisten/x.rdq文件中

message消息

message又称消息,是服务器与应用程序之间传递的数据,有properties+body组成,properties中可以对消息的优先、传输格式、延迟等特性进行定义,body则是消息体内容。
消息的三种状态

  • ready - 待消费的消息总数
  • unacked - 待应答的消息总数
  • totle - 上面总和

properties中的属性对应含义
image.png

3. Centos安装RabbitMQ

docker方式(首先保证安装了docker)

  1. 创建文件夹存放rabbitmq
mkdir /opt/rabbitmq
cd /opt/rabbitmq
  1. 拉取镜像,这里拉取的是最新版本可以指定版本
docker pull rabbitmq
  1. 运行
#docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.10-management

docker run -d --hostname my-rabbit --name rabbitmq -p 15672:15672 -p 5672:5672 rabbitmq
  1. 查看docker运行的镜像
docker ps -l

image.png

  1. 设置docker启动的时候自动启动
docker update rabbitmq --restart=always
  1. 启动rabbitmq-management
docker exec -it rabbitmq /bin/bash
---------------------------------
user@7b295c46c99d /: rabbitmq-plugins enable rabbitmq_management

image.png

  1. 访问web管理页面,默认的用户名和密码都是 guest

http://ip:15672/

不能访问时查看防火墙是否开启对应端口

firewall-cmd --zone=public --add-port=5672/tcp --permanent
firewall-cmd --zone=public --add-port=15672/tcp --permanent        
firewall-cmd --reload 

yum方式

  • https://github.com/rabbitmq/erlang-rpm/releases 中复制对应的版本erlang下载地址
  • https://github.com/rabbitmq/rabbitmq-server/tags 中复制对应的版本rabbitmq的下载地址

我基于centos8进行安装

  1. 安装erlang语言环境注意版本对应关系,查看elang版本与rabbitmq版本的对应关系https://www.rabbitmq.com/which-erlang.html
  2. 安装相关依赖
yum install -y gcc gcc-c++  openssl openssl-devel  ncurses-devel 
  1. 下载socat
[root@localhost /]# yum install socat -y
  1. 安装elang
## 指定下载位置/home/download 
wget -P /home/download  https://github.com/rabbitmq/erlang-rpm/releases/download/v25.0/erlang-25.0-1.el8.x86_64.rpm


[root@localhost download]# rpm -Uvh /home/download/erlang-25.0-1.el8.x86_64.rpm
  1. 安装rabbitmq-server

提示:可以在 https://github.com/rabbitmq/rabbitmq-server/tags 或者 https://github.com/rabbitmq/rabbitmq-server/releases 下载历史版本

wget -P /home/download https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.10.4/rabbitmq-server-3.10.4-1.el8.noarch.rpm


[root@localhost download]# rpm -Uvh /home/download/rabbitmq-server-3.10.4-1.el8.noarch.rpm

rpm -Uvh 升级rpm
rpm -ivh xxx.rpm 安装
rpm -e xxx.rpm 卸载

若通过wget下载rpm包失败,可以直接本地导入包内容 下载连接(蓝奏云)
https://wwb.lanzouj.com/b0315a5ch
密码:e1mk

  1. 启动服务
# 设置开机自启
[root@localhost download]# systemctl enable rabbitmq-server.service
# 启动服务
[root@localhost download]# systemctl start rabbitmq-server.service
# 查看状态
[root@localhost download]# systemctl status rabbitmq-server.service
  1. 开启web管理插件(默认账号guest 密码 guest)
rabbitmq-plugins enable rabbitmq_management

rabbitmq有一个默认的guest用户,但只能通过localhost访问,所以需要添加一个能够远程访问的用户。

rabbitmqctl add_user admin admin
rabbitmqctl set_user_tags admin administrator
rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"
# 查看用户信息
[root@localhost download]# rabbitmqctl list_users
  1. 关闭对应端口的防火墙
firewall-cmd --zone=public --add-port=5672/tcp --permanent
firewall-cmd --zone=public --add-port=15672/tcp --permanent    
firewall-cmd --reload
#查看开启的端口
firewall-cmd --zone=public --list-ports

image.png

  1. 访问测试

浏览器输入:http://ip+端口(15672),例如:http://192.168.0.45:15672

非Root用户安装

首先在官网下载安装包
erlang:https://www.erlang.org/downloads
在这里插入图片描述
可以选择不同的版本然后下载
rabbitmq:http://www.rabbitmq.com/download.html
在这里插入图片描述

https://github.com/rabbitmq/rabbitmq-server/releases
进入github选择合适的版本也可以

将下载好的两个安装包上传到指定目录
创建用户

useradd liuch -m -d /home/liuch -s /bin/bash

#设置密码
passwd liuch
#设置密码。。。
  1. 安装依赖

yum install -y gcc gcc-c++ openssl openssl-devel ncurses-devel socat perl

  1. 安装erlang,将安装包解压至 /liuch/rabbitmq目录下

#解压安装包到指定目录
tar -zxvf otp_src_25.0.2.tar.gz -C /home/liuch/rabbitmq
#进入目录
cd /home/liuch/rabbitmq/otp_src_25.0.2/
#配置下目录前缀
./configure --prefix=/home/liuch/rabbitmq/erlang
#编译 安装
make && make install

  1. 配置环境变量
cd ~
vim .bash_profile

如果没安装vim 需要切换到root 然后 yum install -y vim (vi也可以)

bash_profile 最后加入

#erlang
export PATH=$PATH:/home/liuch/rabbitmq/erlang/bin

保存退出
4. 检测erlang是否安装成功

刷新环境变量使配置生效

source .bash_profile
erl

在这里插入图片描述

退出系统使用

halt()
.
  1. 安装rabbitmq,首先需要解压xz,然后再解压tar

解压xz

xz -d /home/liuch/software/rabbitmq-server-generic-unix-3.10.5.tar.xz 

解压tar

tar xvf /home/liuch/software/rabbitmq-server-generic-unix-3.10.5.tar -C /home/liuch/rabbitmq/
  1. rabbitmq是解压以后就可以直接使用的,我们可以通过配置文件指定一些自定义设置

先进入配置目录

cd /home/liuch/rabbitmq/rabbitmq_server-3.10.5/etc/rabbitmq/
  1. 增加配置文件rabbitmq.env.conf指定数据节点和路径,增加rabbitmq.conf配置端口信息等(不配置就使用默认的可省略)

vim rabbitmq.env.conf
#添加内容如下:
#node name
NODENAME=rabbit
#data dir
MNESIA_BASE=/home/liuch/rabbitmq/rabbitmq_server-3.10.5/data
vim rabbitmq.conf
#添加内容如下:
#listen port
listeners.tcp.default = 5672
#log dir
log.dir =/home/liuch/rabbitmq/rabbitmq_server-3.10.5/logs
#open remote request
loopback_users = none

配置完后记得手动创建数据存储目录和日志目录data和logs。

mkdir /home/liuch/rabbitmq/rabbitmq_server-3.10.5/data  /home/liuch/rabbitmq/rabbitmq_server-3.10.5/logs -p
  1. 配置环境变量,同样的还是vim .bash_profile
cd ~
vim .bash_profile

在最后一行添加

export PATH=$PATH:/home/liuch/rabbitmq/rabbitmq_server-3.10.5/sbin

在这里插入图片描述

  1. 刷新环境变量
source .bash_profile 
  1. 以后台的方式启动rabbitmq
 rabbitmq-server -detatched

查看启动状态

 rabbitmqctl status

安装后不能用root用户使用systemctl命令进行启动,需要自己单独进行配置
配置systemctl启动

  1. 首先需要切换到root用户

  2. 修改bash._profile

cd ~
vim .bash._profile 

添加一下内容

export PATH=$PATH:/home/liuch/rabbitmq/erlang/bin
export PATH=$PATH:/home/liuch/rabbitmq/rabbitmq_server-3.10.5/sbin
  1. 配置文件生效
source .bash_profile 
  1. 在 /lib/systemd/system/新增文件rabbitmq-server.service
vim rabbitmq-server.service

文件内容如下

[Unit]
Description=RabbitMQ broker
After=syslog.target network.target

[Service]
Type=forking
User=root
Group=root
Restart=on-failure
RestartSec=10
WorkingDirectory=/home/liuch/rabbitmq/rabbitmq_server-3.10.5
ExecStart=/home/liuch/rabbitmq/rabbitmq_server-3.10.5/sbin/rabbitmq-server -detached
ExecStop=/home/liuch/rabbitmq/rabbitmq_server-3.10.5/sbin/rabbitmqctl shutdown
# See rabbitmq/rabbitmq-server-release#51
SuccessExitStatus=69
PrivateTmp=true
[Install]
WantedBy=multi-user.target

重新加载一下配置

 systemctl daemon-reload

采用systemctl启动rabbitmq尝试

systemctl start rabbitmq-server.service

如果出现启动失败的情况

journalctl -xe查看出现报错信息
在这里插入图片描述
修改rabbitmq-server配置文件,增加下面的代码,根据错误的提示 line 73在第73行增加

export PATH=$PATH:/home/liuch/rabbitmq/erlang/bin

重试可以成功启动

修改默认配置

docker 安装的也是进入docker中对配置进行修改,这里就不演示了

  1. 进入目录
cd /etc/rabbitmq
  1. 创建配置文件
vim rabbitmq.conf
  1. 配置文件内容
#server启动端口
listeners.tcp.default=6650
#web管理服务端口
management.tcp.port=6671

  1. 修改rabbitmq-defaults文件
cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.10.4/sbin/
vim rabbitmq-defaults
#添加配置路径到文件中,保存退出
CONFIG_FILE=/etc/rabbitmq/rabbitmq.conf

image.png

  1. 重启服务即可

4. 整合SpringBoot

  1. 引入pom
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  1. rabbitmq yml配置文件详解
spring:
  rabbitmq:
    host: 127.0.0.1 #ip
    port: 5672      #端口
    username: guest #账号
    password: guest #密码
    virtualHost:    #链接的虚拟主机
    addresses: 127.0.0.1:5672     #多个以逗号分隔,与host功能一样。
    requestedHeartbeat: 60 #指定心跳超时,单位秒,0为不指定;默认60s
    publisherConfirms: true  #发布确认机制是否启用
    publisherConfirmType:发布者确认模式修改
    NONE(无)
    correlated(异步确认):发布消息到交换机成功后触发RabbitTemplate.ConfirmCallback回调
    simple(同步确认):测试效果与correlated 会一样触发回调,并且在发送消息成功后可以使用				  rabbitTemplate的waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结构,根据返回结果判断下一步逻辑,要注意的是如果waitForConfirmsOrDie方法返回false则会关闭channel,接下来无法发送消息到broker
    publisherReturns: #发布返回是否启用,消息无法路由时回退消息
    connectionTimeout: #链接超时。单位ms。0表示无穷大不超时
    requested-channel-max: 2047 #最大连接数,设置为0表示无限制 默认为2047
    ### ssl相关
    ssl:
      enabled: #是否支持ssl
      keyStore: #指定持有SSL certificate的key store的路径
      keyStoreType: #key store类型 默认PKCS12
      keyStorePassword: #指定访问key store的密码
      trustStore: #指定持有SSL certificates的Trust store
      trustStoreType: #默认JKS
      trustStorePassword: #访问密码
      algorithm: #ssl使用的算法,例如,TLSv1.1
      verifyHostname: #是否开启hostname验证
    ### cache相关
    cache:
      channel: 
        size: #缓存中保持的channel数量
        checkoutTimeout: #当缓存数量被设置时,从缓存中获取一个channel的超时时间,单位毫秒;如果为0,则总是创建一个新channel
      connection:
        mode: #连接工厂缓存模式:CHANNEL 和 CONNECTION
        size: #缓存的连接数,只有是CONNECTION模式时生效
    ### listener
    listener:
       type: #两种类型,SIMPLE,DIRECT
       ## simple类型
       simple:
         concurrency: #最小消费者数量
         maxConcurrency: #最大的消费者数量
         transactionSize: #指定一个事务处理的消息数量,最好是小于等于prefetch的数量
         missingQueuesFatal: #是否停止容器当容器中的队列不可用
         ## 与direct相同配置部分
         autoStartup: #是否自动启动容器
         acknowledgeMode: #表示消息确认方式,其有三种配置方式,分别是none、manual和auto;默认auto
         prefetch: #指定一个请求能处理多少个消息,如果有事务的话,必须大于等于transaction数量
         defaultRequeueRejected: #决定被拒绝的消息是否重新入队;默认是true(与参数acknowledge-mode有关系)
         idleEventInterval: #container events发布频率,单位ms
         ##重试机制
         retry: 
           stateless: #有无状态
           enabled:  #是否开启
           maxAttempts: #最大重试次数,默认3
           initialInterval: #重试间隔
           multiplier: #对于上一次重试的乘数
           maxInterval: #最大重试时间间隔
       direct:
         consumersPerQueue: #每个队列消费者数量
         missingQueuesFatal:
         #...其余配置看上方公共配置
     ## template相关
     template:
       mandatory: #是否启用强制信息;默认false
       receiveTimeout: #`receive()`接收方法超时时间
       replyTimeout: #`sendAndReceive()`超时时间
       exchange: #默认的交换机
       routingKey: #默认的路由
       defaultReceiveQueue: #默认的接收队列
       ## retry重试相关
       retry: 
         enabled: #是否开启
         maxAttempts: #最大重试次数
         initialInterval: #重试间隔
         multiplier: #失败间隔乘数
         maxInterval: #最大间隔

  1. 配置文件
/**
 * @desc: mq配置类
 * @author: LiuChang
 * @since: 2022/6/1
 */
@Configuration
public class RabbitMQConfig {
    
    
    @Autowired
    public ConnectionFactory connectionFactory;

    @Bean
    public RabbitAdmin rabbitAdmin() {
    
    
        return new RabbitAdmin(connectionFactory);
    }

    /**
     * 声明交换机
     *
     * @return durable开启交换机持久化
     */
    @Bean("mqExchange")

    public Exchange mqExChange() {
    
    
        return ExchangeBuilder.directExchange(RabbitMQConstant.MQ_EXCHANGE).durable(true).build();
    }

    /**
     * 声明一个队列
     *
     * @return
     */
    @Bean("mqQueue")
    public Queue mqQueue() {
    
    
        //durable 持久化 nonDurable不持久化
        return QueueBuilder.durable(RabbitMQConstant.MQ_QUEUE)
                //绑定死信队列
                .deadLetterExchange(RabbitMQConstant.DEAD_LETTER_EXCHANGE)
                .deadLetterRoutingKey(RabbitMQConstant.DEAD_LETTER_ROUTER_KEY)
                //设置ttl 超过多少时间不被接收就进入死信队列
//                .ttl(RabbitMQConstant.MQ_QUEUE_TTL)
                //当前队列最多存储消息数量
//                .maxLength(2)
                .build();
    }

    /**
     * 将队列和交换机绑定
     * with 交换机与队列的router-key
     *
     * @param queue
     * @param exchange
     * @return
     */
    @Bean
    public Binding mqBinding(@Qualifier("mqQueue") Queue queue, @Qualifier("mqExchange") Exchange exchange) {
    
    
        return BindingBuilder.bind(queue).to(exchange).with(RabbitMQConstant.MQ_ROUTER_KEY).noargs();
    }

    /**
     * 备份交换机相关
     *
     * @return
     */
    @Bean("backupExchange")
    public FanoutExchange backupExchange() {
    
    
        return ExchangeBuilder.fanoutExchange(RabbitMQConstant.APP_BACK_UP_EXCHANGE).durable(true).build();
    }

    @Bean("backupQueue")
    public Queue backupQueue() {
    
    
        return QueueBuilder.durable(RabbitMQConstant.APP_BACK_UP_QUEUE)
                .build();
    }

    @Bean
    public Binding backupBinding(@Qualifier("backupQueue") Queue queue, @Qualifier("backupExchange") FanoutExchange exchange) {
    
    
        return BindingBuilder.bind(queue).to(exchange);
    }

    /**
     * 接收app消息的队列
     */
    @Bean("appQueue")
    public Queue appQueue() {
    
    
        return QueueBuilder.durable(RabbitMQConstant.APP_QUEUE)
                .deadLetterExchange(RabbitMQConstant.DEAD_LETTER_EXCHANGE)
                .deadLetterRoutingKey(RabbitMQConstant.DEAD_LETTER_ROUTER_KEY)
//                .ttl(RabbitMQConstant.MQ_QUEUE_TTL)
                .build();
    }

    /**
     * 一个队列绑定多个交换机,都可以向这个队列推送消息
     *
     * @return
     */
    @Bean("appExchange")
    public Exchange appExChange() {
    
    
        return ExchangeBuilder.directExchange(RabbitMQConstant.APP_EXCHANGE)
                .durable(true)
                .withArgument("alternate-exchange", RabbitMQConstant.APP_BACK_UP_EXCHANGE)
                .build();
    }

    @Bean("app2Exchange")
    public Exchange app2ExChange() {
    
    
        return ExchangeBuilder.directExchange(RabbitMQConstant.APP2_EXCHANGE).durable(true).build();
    }

    @Bean
    public Binding appBinding(@Qualifier("appQueue") Queue queue, @Qualifier("appExchange") Exchange exchange) {
    
    
        return BindingBuilder.bind(queue).to(exchange).with(RabbitMQConstant.APP_ROUTER_KEY).noargs();
    }

    @Bean
    public Binding app2Binding(@Qualifier("appQueue") Queue queue, @Qualifier("app2Exchange") Exchange exchange) {
    
    
        return BindingBuilder.bind(queue).to(exchange).with(RabbitMQConstant.APP_ROUTER_KEY).noargs();
    }


    /**
     * 死信队列
     */
    @Bean("deadExchange")
    public Exchange deadExChange() {
    
    
        return ExchangeBuilder.directExchange(RabbitMQConstant.DEAD_LETTER_EXCHANGE).durable(true).build();
    }

    @Bean("deadQueue")
    public Queue deadQueue() {
    
    
        return QueueBuilder.durable(RabbitMQConstant.DEAD_LETTER_QUEUE).build();
    }

    @Bean
    public Binding deadBinding(@Qualifier("deadQueue") Queue queue, @Qualifier("deadExchange") Exchange exchange) {
    
    
        return BindingBuilder.bind(queue).to(exchange).with(RabbitMQConstant.DEAD_LETTER_ROUTER_KEY).noargs();
    }

}

  1. 生产者实例
package com.hengan.rabbitmq.mqserver.MQProducer;

import com.hengan.rabbitmq.mqserver.constants.RabbitMQConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.UUID;

/**
 * @desc:
 * @author: LiuChang
 * @since: 2022/6/2
 */
@Component
@Slf4j
public class MsgProducer implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
    
    
    @Resource
    private RabbitTemplate rabbitTemplate;

    public void sendDirectQueue(String exchange, String routerKey, byte[] message, String correlationId) {
    
    
//        String id = UUID.randomUUID().toString();
        //开启Mandatory
        rabbitTemplate.setMandatory(true);
        // 执行rabbitmq消息发送后回调方法,只负责推送到exchange
        rabbitTemplate.setConfirmCallback(this);
        // 执行return回调,只负责推到队列
        rabbitTemplate.setReturnsCallback(this);
        /**
         * 消息发送
         * 路由、Key、消息、设置消息id
         * */
        MessageProperties properties = new MessageProperties();
        properties.setCorrelationId(correlationId);
        try {
    
    
            //convertAndSend 有很多构造,可以根据需要选择
            rabbitTemplate.convertAndSend(exchange, routerKey, message);
        } catch (Exception e) {
    
    
            //发送数据可以采用异常捕获
            log.info("消息发送失败");
        }


    }

    /**
     * 消息发送回调
     * 找不到exchange
     *
     * @param correlationData
     * @param ack             是否发送成功
     * @param cause           发送失败的信息,发送成功为null
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
    
    
        if (ack) {
    
    
            log.info("消息发送到交换机成功:" + correlationData);
        } else {
    
    
            log.error("消息发送到交换机失败:" + correlationData);
            // 需要根据id向app客户端回推消息id 客户端找到消息后重发

        }
    }

    /**
     * 一旦出现错误则调用该方法  人工去做
     * exchange -> queue 失败触发
     *
     * @param returnedMessage message  消息本身
     *                        replyCode 响应的状态码
     *                        replyText 错误的信息描述
     *                        exchange 交换机
     *                        routingKey 路由key
     */
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
    
    
        log.error("消息从交换机发送到队列失败的回调函数" + returnedMessage.getExchange() + returnedMessage.getRoutingKey());
        //利用延迟队列(延迟队列插件或者 TTL+死信队列)重新投递消息,超过最大次数入库 人工干预处理
        }

}

参数解释

  • mandatory - 如果交换机找不到匹配的队列不能将消息放到队列中就会产生一个回调,将信息返回
  • immediate - 判断即将放入的队列是否有消费者进行监听,如果没有消费者监听这个队列将消息直接丢弃
  1. 消费者实例
/**
 * @desc: 消息接收
 * @author: LiuChang
 * @since: 2022/6/2
 */
@Component
@Slf4j
public class AppMsgReceiver {
    
    

        @RabbitListener(queues = RabbitMQConstant.APP_QUEUE)
    public void messageListener(Message message, Channel channel) throws IOException {
    
    
        //消息编号
        long tag = message.getMessageProperties().getDeliveryTag();
        String correlationId = message.getMessageProperties().getCorrelationId();
        try {
    
    
            log.info("app-queue接收到的消息:" + new String(message.getBody()));
            //手动签收 参数二是否开启批处理,会将小于tag的消息全部进行确认
            channel.basicAck(tag, false);
        } catch (Exception e) {
    
    
            //消息处理失败 重新入队
            //参数3:true将消息放回原来队列中,false不把消息放入原队列中 放到死信队列
            channel.basicNack(tag, false, false);
        }
    }
    

}

上面生产者和消费者的例子比较简单
监听多个队列可以在类上添加多个@RabbitListener(queues = ${QueueName})

5. RabbitMQ进阶

推送交换机失败

交换机无应答时需要将消息重发或者将消息保存到死信队列中,方便我们后期进行消息的溯源和手动处理,交换机再消息发送失败后返回的回调信息中只包含correlationData(),我们需要将消息内容放到correlationData中,方便我们交换机无应答时进行消息的处理。

public void sendDirectQueue(String exchange, String routerKey, byte[] message, String correlationId) {
    
    
    rabbitTemplate.setMandatory(true);
    // 执行rabbitmq消息发送后回调方法,只负责推送到exchange
    rabbitTemplate.setConfirmCallback(this);
    // 执行return回调,只负责推到队列
    rabbitTemplate.setReturnsCallback(this);
    MessageProperties properties = new MessageProperties();
    properties.setCorrelationId(correlationId);
    rabbitTemplate.convertAndSend(exchange, routerKey, new Message(message, properties));

} 



@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
    
    
    if (ack) {
    
    
        log.info("消息发送到交换机成功:" + correlationData);
    } else {
    
    
        log.error("消息发送到交换机失败:" + correlationData + ",原因是:" + cause);
        //消息发送到交换机失败:CorrelationData null,原因是:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'mq_center_exchange1' in vhost '/', class-id=60, method-id=40)

    }
}

我们需要在发送消息时,第四个参数中放我们的消息内容,用来应对交换机无应答的情况。
在这里插入图片描述

//消息发送失败时 回调返回值的对象 写一个子类扩充属性,可以用来保存交换机、routingKey和消息体
CorrelationData correlationData = new CorrelationData();

  1. 创建一个子类,用来扩展属性
/**
 * @desc: 写一个子类扩充属性,可以用来保存交换机、routingKey和消息体
 * @author: LiuChang
 * @since: 2022/6/20
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class CorrelationDataPojo extends CorrelationData {
    
    
    /**
     * 消息体
     */
    private Object message;
    /**
     * 交换机
     */
    private String exchange;
    /**
     * routingKey
     */
    private String routingKey;
    /**
     * 重试次数
     */
    private int retryCount = 0;

}
  1. 然后发送消息时我们将经过我们扩展后的CorrelationData放到convertAndSend的第四个参数中,这样我们交换机无应答时就可以将消息再次进行重发。
public void sendDirectQueue(String exchange, String routerKey, byte[] message, String correlationId) {
    
    
    rabbitTemplate.setMandatory(true);
    // 执行rabbitmq消息发送后回调方法,只负责推送到exchange
    rabbitTemplate.setConfirmCallback(this);
    // 执行return回调,只负责推到队列
    rabbitTemplate.setReturnsCallback(this);
    //消息发送失败时 回调返回值的对象
    CorrelationDataPojo correlationData = new CorrelationDataPojo();
    correlationData.setExchange(exchange);
    correlationData.setRoutingKey(routerKey);
    correlationData.setMessage(message);
    MessageProperties properties = new MessageProperties();
    properties.setCorrelationId(correlationId);
    rabbitTemplate.convertAndSend(exchange, routerKey, new Message(message, properties), correlationData);

}
  1. 交换机无应答的回调中,调用重发方法
 @Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
    
    
    if (ack) {
    
    
        log.info("消息发送到交换机成功:" + correlationData);
    } else {
    
    
        //消息发送到交换机失败,进行重发操作
        this.retryMessage(correlationData);
    }
}
  1. 重新发送消息的方法
/**
* 重新推送消息方法
*
* @param correlationData 交换机+routingKey+消息体+次数
*/
public void retryMessage(CorrelationData correlationData) {
    
    
    CorrelationDataPojo data = (CorrelationDataPojo) correlationData;
    MessageProperties properties = new MessageProperties();
    properties.setCorrelationId(data.getCorrelationId());
    //超过三次丢入死信队列
    log.info("推送交换机失败,消息重试" + data.getRetryCount());
    if (data.getRetryCount() < 2) {
    
    
        data.setRetryCount(data.getRetryCount() + 1);
        rabbitTemplate.convertAndSend(data.getExchange(),
                                      data.getRoutingKey(),
                                      new Message((byte[]) data.getMessage(), properties),
                                      data);
    } else {
    
    
        this.sendDeadQueue(data.getMessage());
    }
}

生产者confirm模式

生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布
的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,
broker就会发送一个确认给生产者 (包含消息的唯一ID) ,这就使得生产者知道消息已经正
确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会将消息写入磁盘之后发出,broker回传给生产者的确认消息中deliver-tag 域包含了确认消息的序列号,此外
broker也可以设置basic.ack的multiple 域,表示到这个序列号之前的所有消息都已经得
到了处理。Confrm模式最大的好处在于他是异步。
注意confirm模式跟事务机制不能在同一个队列中
开启confirm模式

channel.confimSelect()

事务机制

TxSelect TxCommit TxRollBack

TxSelect : 用于将当前channel设置成transation模式
TxCommit : 用于提交事务
TxRollBack : 用于回滚事务

整合Springboot直接修改配置文件

publisher-confirm-type: correlated
    # 确认消息已发送到队列(Queue)
    publisher-returns: true
    #消费者手动签收机制
    listener:
      simple:
        # 手动模式 需要消费端自己自定义ack
        # MANUAL 手动签收(推荐) NONE 自动签收
        acknowledge-mode: MANUAL

备份交换机

消息发送失败时可以进行消息的重发操作,但是若交换机无法进行消息的路由,我们需要手动去人工干预处理,这时我们可以为交换机声明一个备份交换机,当交换机接收到一条不可路由的消息时会将消息转发到备份交换机,由备份交换机再次进行转发处理,一般将备份交换机的类型设置为Fanout可以将消息投递到所有与他所绑定的队列中。

/**
     * MQ_EXCHANGE的备份交换机
     */
public static final String MQ_BACK_UP_EXCHANGE = "mq_back_up_exchange";
public static final String MQ_BACK_UP_QUEUE = "mq_back_up_queue";
public static final String MQ_BACK_UP_ROUTING_KEY = "mq_back_up_routing_key";
@Bean("backupExchange")
public FanoutExchange backupExchange() {
    
    
    return ExchangeBuilder.fanoutExchange(RabbitMQConstant.APP_BACK_UP_EXCHANGE).durable(true).build();
}

@Bean("backupQueue")
public Queue backupQueue() {
    
    
    return QueueBuilder.durable(RabbitMQConstant.APP_BACK_UP_QUEUE)
        .build();
}

@Bean
public Binding backupBinding(@Qualifier("backupQueue") Queue queue, @Qualifier("backupExchange") FanoutExchange exchange) {
    
    
    return BindingBuilder.bind(queue).to(exchange);
}

//绑定备份交换机
@Bean("mqCenterExchange")
public Exchange mqCenterChange() {
    
    
    return ExchangeBuilder.directExchange(RabbitConstant.MQ_CENTER_EXCHANGE)
        .withArgument("alternate-exchange", RabbitConstant.SERVER_CENTER_BACK_UP_EXCHANGE)
        .durable(true)
        .build();
}

备份交换机是优先于ReturnsCallback回调,当向备份交换机转发消息失败是才会触发方法回调

死信队列

概念:死信队列是一种消息机制,当消息出现一下情况时会将消息放入死信队列

  • 消息在队列的存活时间超过了设置的TTL,就会将本条消息放入死信队列
  • 消息队列的消息数量超过队列所容纳的最大长度
  • 消息被否定basicNack或者basicReject其中方法的Requeue属性设置为false

上面的消息会进入死信队列(必须创建队列时配置了死信队列才可以否则会被丢弃)。
配置死信队列

/**
* 死信队列
*/
@Bean("deadExchange")
public Exchange deadExChange() {
    
    
    return ExchangeBuilder.directExchange(RabbitMQConstant.DEAD_LETTER_EXCHANGE).durable(true).build();
}

@Bean("deadQueue")
public Queue deadQueue() {
    
    
    return QueueBuilder.durable(RabbitMQConstant.DEAD_LETTER_QUEUE).build();
}

@Bean
public Binding deadBinding(@Qualifier("deadQueue") Queue queue, @Qualifier("deadExchange") Exchange exchange) {
    
    
    return BindingBuilder.bind(queue).to(exchange).with(RabbitMQConstant.DEAD_LETTER_ROUTER_KEY).noargs();
}

//1.绑定死信队列方式一 为appQueue队列绑定死信队列
@Bean("appQueue")
public Queue appQueue() {
    
    
    return QueueBuilder.durable(RabbitMQConstant.APP_QUEUE)
        .deadLetterExchange(RabbitMQConstant.DEAD_LETTER_EXCHANGE)
        .deadLetterRoutingKey(RabbitMQConstant.DEAD_LETTER_ROUTER_KEY)
        //ttl过期时间 过期的消息会进入死信队列
        .ttl(RabbitMQConstant.MQ_QUEUE_TTL)
        .build();
}
//2.绑定死信队列方式二
@Bean("appQueue")
public Queue appQueue() {
    
    
    Map<String, Object> arguments = new HashMap<>();
    arguments.put("x-dead-letter-exchange", RabbitMQConstant.DEAD_LETTER_EXCHANGE);
    arguments.put("x-dead-letter-routing-key", RabbitMQConstant.DEAD_LETTER_ROUTER_KEY);
    return QueueBuilder.durable(RabbitMQConstant.APP_QUEUE)
         .withArguments(arguments)
        //ttl过期时间 过期的消息会进入死信队列
        .ttl(RabbitMQConstant.MQ_QUEUE_TTL)
        .build();
}

优先级队列

可以为队列设置优先级,优先级高的队列具有优先被消费的特权。

    @Bean("backupQueue")
    public Queue backupQueue() {
    
    
        return QueueBuilder.durable(RabbitMQConstant.APP_BACK_UP_QUEUE)
                //设置队列优先级
                .maxPriority(10)
                .build();
    }
// 也可以通过Arguments进行实现,maxPriority方法就是做了一层封装

底层实现

	public QueueBuilder maxPriority(int maxPriority) {
    
    
		return withArgument("x-max-priority", maxPriority);
	}

惰性队列

惰性队列数据基于磁盘存储,消息上限
要设置一个队列为惰性队列,只需要在声明队列时,指定x-queue-mode属性为lazy即可。
基于磁盘存储,消息时效性会降低;
性能受限于磁盘的IO。

延迟队列

  • 可以使用延迟队列,利用 rabbitmq_delayed_message_exchange 插件 ,将发送失败的消息再次投递到业务队列
  • 使用TTL和DLX死信队列实现延迟队列,业务队列消费失败时,会发送一条TTL 消息,消息包含业务数据、交换机、队列、次数、最大次数等,TTL 消息过期后会进入死信队列,此时监听死信队列接收消息,校验是否达到重试次数,再重新投递给业务队列,业务队列二次收到消息时,再次消费失败,校验最大次数,判断是否再次重试。超过最大次数入库,人工干预处理

TTL和DLX死信队列实现延迟队列:会造成消息的阻塞,例如:发送第一个延时消息,10分钟过期,再发送第二个延时消息,5分钟过期。第二个消息肯定要比第一个消息提前过期,但此时因为前一个消息没有过期也就没有出队列,那第二个消息只能等待第一个出队列之后才能出队列。这样就照成了消息的阻塞。业务上允许的情况下,可以使用这种方式。

延迟队列
在rabbitmq 3.6版本之前,都是使用TTL+DLX来实现延迟队列,后来官方提供了延迟队列的插件。
使用命令行开启延迟队列插件

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

延迟队列插件安装
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases
下载对应版本的插件,我这里下载的是10.2
将下载的rabbitmq_delayed_message_exchange-3.10.2.ez 放到rabbitmq的plugins文件夹下,我这里采用rpm安装的路径为/usr/lib/rabbitmq/lib/rabbitmq_server-3.10.4/plugins,然后执行上面的bash命令即可。
image.png
启动插件后需要重启一下mq

[root@localhost ~]# systemctl restart rabbitmq-server.service

进入web管理界面,新增交换机这时可以发现新增了一种延迟交换机,此时代表插件安装成功
image.png

  /**
     * 延迟队列相关,用来错误消息重发
     */
    @Bean("delayedQueue")
    public Queue delayedQueue() {
    
    
        return QueueBuilder.durable(MQConstant.DELAYED_QUEUE)
                .build();
    }

    @Bean("delayedExchange")
    public CustomExchange delayedExChange() {
    
    
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("x-delayed-type", "direct");
        return new CustomExchange(MQConstant.DELAYED_EXCHANGE, "x-delayed-message", true, false, arguments);
    }

    @Bean
    public Binding delayedBinding(@Qualifier("delayedQueue") Queue queue, @Qualifier("delayedExchange") Exchange exchange) {
    
    
        return BindingBuilder.bind(queue).to(exchange).with(MQConstant.DELAYED_ROUTER_KEY).noargs();
    }

延迟队列生产者使用时在消息中注入延迟的时间

Message message = new Message(body[]);
message.getMessageProperties().setDelay(times);
rabbitTemplate.convertAndSend(exchange, routerKey, message, new CorrelationData(id));

集群

高可用RabbitMQ:

  1. 普通集群模式:多台机器上启动多个rabbitmq实例,每个机器启动一个。但是你创建的queue,只会放在一个rabbtimq实例上,但是每个实例都同步queue的元数据。完了你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从queue所在实例上拉取数据过来。如果那个放queue的实例宕机了,会导致接下来其他实例就无法从那个实例拉取,如果你开启了消息持久化,让rabbitmq落地存储消息的话,消息不一定会丢,得等这个实例恢复了,然后才可以继续从这个queue拉取数据。
  2. 镜像集群模式:创建的queue,无论元数据还是queue里的消息都会存在于多个实例上,然后每次写消息到queue的时候,都会自动把消息到多个实例的queue里进行消息同步。

缺点:
(1)性能开销大,因为需要进行整个集群内部所有实例的数据同步;
(2)无法线性扩容: 因为每一个服务器中都包含整个集群服务节点中的所有数据, 这样如果一旦单个服务器节点的容量无法容纳了。

将多个单机应用连接到一起然后向外暴露出一个接口,内部多台机器进行连接但是在外部看来就是一个整体。
普通模式(提升了吞吐量,无法保证高可用)
数据不会同步,只是同步元数据(Queue的一些配置信息),队列中的消息还是将存在单一的服务中,不会同步到其他队列,比如现在数据存在A中,用户想访问A中的内容可以通过A进行访问也可以通过B进行访问,如果A发生了宕机,B也访问不了A的数据。
当我们消费消息的时候,如果连接到了另外一个实例,那么那个实例会通过元数据定位到 Queue 所在的位置,然后访问 Queue 所在的实例,拉取数据过来发送给消费者。
镜像模式
会将服务的数据进行同步,即每台服务上都是完整的数据内容。每次写入消息的时候都会自动把数据同步到多台实例上,这样一台发生故障其他的机器也可以继续提供服务。

搭建集群

  1. 将集群节点的hostname修改为映射文件对应的名称
hostnamectl set-hostname rabbit-node1
  1. 首先修改/etc/hosts 映射文件,多个服务器都添加节点名称解析
192.168.204.141 A
192.168.204.142 B
  1. 服务相互通信,保持cookie的一致性,将rabbitmqA的cookie信息拷贝到B
scp /var/lib/rabbitmq/.erlang.cookie 192.168.204.142:/var/lib/rabbitmq

修改cookie文件要重启服务器

  1. 停止防火墙(否则会出现连接不通),逐个节点启动rabbitmq服务
rabbitmqctl cluster_status
#注意节点名称和之前配置一致,否则需要重新配置hostname或重启
  1. 加入加群节点(在子节点中使用以上命令)
rabbitmqctl stop_app
rabbitmqctl join_cluster  rabbit@rabbitmq-node1
rabbitmqctl start_app

image.png

  1. 查看节点状态
rabbitmqctl cluster_status

image.png

移除节点 rabbitmqctl forget_cluster_node rabbit@rabbitmq-node2

  1. 查看管理端

搭建集群结构之后,之前创建的交换机、队列、用户都属于单一结构,在新的集群环境中是不能用的
所以在新的集群中重新手动添加用户即可(任意节点添加,所有节点共享)

rabbitmqctl add_user  root  123123
rabbitmqctl set_user_tags root  administraor
rabbitmqctl set_permissions -p "/"  root " . * " " . * " " . * "

注意:当节点脱离集群还原成单一结构后,交换机,队列和用户等数据都会重新回来

image.png
可以看到nodes中存在多个节点
image.png
在RabbitMQ集群中的节点只有两种类型:内存节点/磁盘节点,单节点系统只运行磁盘类型的节点。而在集群中,可以选择配置部分节点为内存节点。
内存节点将所有的队列,交换器,绑定关系,用户,权限,和vhost的元数据信息保存在内存中。而磁盘节点将这些信息保存在磁盘中,但是内存节点的性能更高,为了保证集群的高可用性,必须保证集群中有两个以上的磁盘节点,来保证当有一个磁盘节点崩溃了,集群还能对外提供访问服务。在上面的操作中,可以通过如下的方式,设置新加入的节点为内存节点还是磁盘节点:
加入时候设置节点为内存节点(默认加入的为磁盘节点)
rabbitmqctl join_cluster rabbit@rabbitM1 --ram
也通过下面方式修改的节点的类型
rabbitmqctl changeclusternode_type disc | ram
springboot连接时修改配置文件地址

spring:
  rabbitmq:
    addresses: 192.168.0.45:5672,192.168.0.24:5672

负载均衡
暂时没做,后期补充

镜像队列

配置镜像队列以后,新创建的队列按照规则成为镜像队列。每个镜像队列都包含一个主节点和若干个从节点,只有主节点会向用户提供服务。从节点接收到主节点的命令进行数据操作,从节点的状态是与主节点一致的。
配置方法

  • 管理界面配置

image.png
image.png
使用策略来配置镜像队列,策略使用正则表达式来配置需要应用镜像策略的队列名称,以及在参数中配置镜像队列的具体参数。

  • Name: policy的名称,用户自定义。
  • Pattern: queue的匹配模式(正则表达式)。^表示所有队列都是镜像队列。
  • Definition: 镜像定义,包括三个部分ha-sync-mode、ha-mode、ha-params。
    • ha-mode: 指明镜像队列的模式,有效取值范围为all/exactly/nodes。
      • all:表示在集群所有的代理上进行镜像。
      • exactly:表示在指定个数的代理上进行镜像,代理的个数由ha-params指定。
      • nodes:表示在指定的代理上进行镜像,代理名称通过ha-params指定。
    • ha-params: ha-mode模式需要用到的参数。
    • ha-sync-mode: 表示镜像队列中消息的同步方式,有效取值范围为:automatic,manually。
      • automatic:表示自动向master同步数据。
      • manually:表示手动向master同步数据。
  • Priority: 可选参数, policy的优先级。

命令行(未测试)

rabbitmqctl set_policy [-p Vhost] Name Pattern Definition [Priority]
-p Vhost: 可选参数,针对指定vhost下的queue进行设置
Name: policy的名称
Pattern: queue的匹配模式(正则表达式)
Definition:镜像定义,包括三个部分ha-mode, ha-params, ha-sync-mode
        ha-mode:指明镜像队列的模式,有效值为 all/exactly/nodes
            all:表示在集群中所有的节点上进行镜像
            exactly:表示在指定个数的节点上进行镜像,节点的个数由ha-params指定
            nodes:表示在指定的节点上进行镜像,节点名称通过ha-params指定
        ha-params:作为参数,为ha-mode的补充
        ha-sync-mode:进行队列中消息的同步方式,有效值为automatic和manual
priority:可选参数,policy的优先级

rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}' --apply-to all

rabbitmqctl set_policy [-p vhost] [–priority priority] [–apply-to apply-to] name pattern definition、

rabbitmqctl set_policy --priority 0 --apply-to queues mirror_queue "^mirror_" '{"ha-mode":"exactly","ha-params":3,"ha-sync-mode":"automatic"}'


镜像队列最大的问题是其同步算法造成的低性能。镜像队列有如下几个设计缺陷
broker离线后重新上线
基本的问题是,当 broker 离线并再次恢复时,它在镜像中的任何数据都将被丢弃。这是关键的设计缺陷。现在,镜像已恢复在线,但为空,管理员需要做出决定:是否同步镜像。“同步”意味着将当前消息从 leader 复制到镜像。
同步阻塞
此时第二个致命的设计缺陷显露了出来。如果要同步消息,会阻塞整个队列,让这个队列不可用。当队列比较短的时候这通常不是什么问题,但当队列很长或者消息总大小很大的时候,同步将会需要很长时间。不仅如此,同步会导致集群中与内存相关的问题,有时甚至会导致同步卡住,需要重新启动。默认情况下,所有镜像队列都会自动同步,但也有人用户不同步镜像。这样,所有新消息都将被复制,老消息都不会被复制,这将减少冗余,会使消息丢失的概率加大。这个问题也引发滚动升级的问题,因为重新启动的 broker 将丢弃其所有数据,并需要同步来恢复全部数据冗余。

消息幂等性

保证消息不会被重复消费

  • 消费者拿到数据需要写库,根据主键查询如果存在就update
  • 放到redis数据库
  • 利用日志表,记录已经成功处理的id,如果新的消息id在日志表则不进行处理

生产者保证消息不丢失,会将ack=false的消息重新发送,这样就可能导致我们的消息重复。
生产者为每一条消息设置一个MessageId,用于消费端去重,也可以利用correlation。

开启日志插件

rabbitmq-plugins enable rabbitmq_tracing

在web管理端进行配置 admin–tracing
image.png

每次服务停掉以后都需要重新开启并配置日志策略

仲裁队列

从3.8版本以后可用,当我们对数据的安全性要求大于对响应速度和资源占用时
quorum queue是镜像队列mirror queue的替代品。它基于Raft 共识算法实现了一个持久的、复制的 FIFO 队列。
quorum 队列是持久的,不是排他的,支持队列的TTL不支持消息的TTL,支持死信。仲裁队列默认镜像数是5,集群节点不足5则都是镜像。+n表示有n个镜像节点。
特性:

  1. 与镜像队列一样,都是主从模式,支持主从数据同步
  2. 使用非常简单,没有复杂的配置
  3. 主从同步基于Raft协议,强一致
    /**
     * 仲裁队列
     */
    @Bean
    public Queue quorumQueue() {
    
    
        return QueueBuilder.durable("quorumQueue")
                .quorum()
                .build();
    }

动态监听队列

消费者动态创建队列并实现动态监听

  • SimpleMessageListenerContainer:用来动态设置要监听的队列
  • RabbitAdmin:用来设置新增队列与交换机的绑定关系
  • MyAckReceiver:消息处理类,实现了ChannelAwareMessageListener接口

在消费者中实现1.2.3

  1. 自定义监听
@Configuration
public class MessageListenerConfig {
    
    
 
    @Autowired
    private CachingConnectionFactory connectionFactory;
 
    @Autowired
    private MyAckReceiver myAckReceiver;//消息接收处理类
 
    @Bean
    public SimpleMessageListenerContainer simpleMessageListenerContainer() {
    
    
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
        container.setConcurrentConsumers(1); 
        container.setMaxConcurrentConsumers(1); 
        container.setAcknowledgeMode(AcknowledgeMode.MANUAL); // RabbitMQ默认是自动确认,这里改为手动确认消息
        container.setMessageListener(myAckReceiver);
 
        return container;
    }
 
    @Bean
    public RabbitAdmin rabbitAdmin(){
    
    
        return new RabbitAdmin(connectionFactory);
    }
}
  1. 消息处理类
@Component
public class MyAckReceiver implements ChannelAwareMessageListener {
    
    
 
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
    
    
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
    
    
            String consumerQueue = message.getMessageProperties().getConsumerQueue();
            String msg = new String(message.getBody());
            System.out.println("MyAckReceiver的 "+consumerQueue+" 队列,收到消息 "+msg);
            channel.basicAck(deliveryTag, true);
        } catch (Exception e) {
    
    
            channel.basicReject(deliveryTag, false);
            e.printStackTrace();
        }
    }
}
  1. 提供监听队列的接口
@RestController
public class QueueController {
    
    
    @Autowired
    private SimpleMessageListenerContainer listenerContainer;
 
    @Autowired
    private RabbitAdmin rabbitAdmin;
 
    @Resource
    private DirectExchange getDirectExchange;
 
    private static final String ROUTING_KEY = "dynamic_queue";
 
    @GetMapping("/queue/{queueName}")
    public String addQueue(@PathVariable String queueName){
    
    
        Queue queue = new Queue("server_app.1");
        rabbitAdmin.declareQueue(queue);
        listenerContainer.addQueues(queue);
        DirectExchange exchange = new DirectExchange("app_exchange",true,false);
        rabbitAdmin.declareBinding(BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY));
        return "增加监听队列:"+queueName+" 成功";
    }

    @GetMapping("/delete/{queueName}")
    public String deleteQueue(@PathVariable String queueName){
    
    
        listenerContainer.removeQueueNames(queueName);
//        rabbitAdmin.deleteQueue(queueName);
        return "移除监听队列:"+queueName+" 成功";
    }
}
  1. 生产者发送消息
@Component
@Slf4j
public class MsgProducer implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {
    
    
    @Resource
    private RabbitTemplate rabbitTemplate;

    public void sendDirectQueue(String exchange, String routerKey, byte[] message, String correlationId) {
    
    
//        String id = UUID.randomUUID().toString();
        rabbitTemplate.setMandatory(true);
        // 执行rabbitmq消息发送后回调方法,只负责推送到exchange
        rabbitTemplate.setConfirmCallback(this);
        // 执行return回调,只负责推到队列
        rabbitTemplate.setReturnsCallback(this);
        /**
         * 消息发送
         * 路由、Key、消息、设置消息id
         * */
        MessageProperties properties = new MessageProperties();
        properties.setCorrelationId(correlationId);
        rabbitTemplate.convertAndSend(exchange, routerKey, new Message(message, properties), new CorrelationData(correlationId));

    }


    /**
     * 消息发送回调
     * 找不到exchange
     *
     * @param correlationData
     * @param ack             是否发送成功
     * @param cause           发送失败的信息,发送成功为null
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
    
    
        if (ack) {
    
    
            log.info("消息发送到交换机成功:" + correlationData);
        } else {
    
    
            log.error("消息发送到交换机失败:" + correlationData);
            // 需要根据id向app客户端回推消息id 客户端找到消息后重发

        }
    }

    /**
     * 一旦出现错误则调用该方法  人工去做
     * exchange -> queue 失败触发
     *
     * @param returnedMessage message  消息本身
     *                        replyCode 响应的状态码
     *                        replyText 错误的信息描述
     *                        exchange 交换机
     *                        routingKey 路由key
     */
    @Override
    public void returnedMessage(ReturnedMessage returnedMessage) {
    
    
        log.error("消息从交换机发送到队列失败的回调函数" + returnedMessage.getExchange() + returnedMessage.getRoutingKey());
    }
}

@RestController
public class sendMsgController {
    
    
    /**
     * 注入模板
     */
    @Resource
    private MsgProducer msgProducer;

    @GetMapping("/send")
    public String send() {
    
    
        msgProducer.sendDirectQueue("app_exchange", "dynamic_queue", JSON.toJSONBytes("测试消息内容"), "232ad1das324412424dd");
        return "发送成功";
    }

}

  1. 此时不开启消费者的监听服务,通过调用send接口发送一条消息

http://localhost:8081/send
然后看rabbitmqAdmin中
image.png
通过http://127.0.0.1:8082/queue/1 增加监听,发现客户端可以获取到消息
image.png
http://127.0.0.1:8082/delete/server_app.1 移除监听后再发发送消息,无法进行接收

猜你喜欢

转载自blog.csdn.net/chang100111/article/details/125332869