前言
文章内容有点多,可以根据需要进行跳转,一文看懂rabbitmq
最近公司用到rabbitmq做消息转发服务,将学到的一些内容记录在这,内容会有些繁杂有些内容也不全面,后面根据实际使用会进行一定的补充,文章引用了不少网上的内容这里就不一 一列举感谢大佬们的知识分享,由于刚刚开始学习消息队列,错误之处感谢指正。
2022-6-24 更新非root用户安装方式和systemctl配置
消息系统的作用:允许软件和应用之间相互连接、扩展,可以通过将消息的发送和接收分离来实现应用程序的异步和解耦。
AMPQ(高级消息队列协议),是面向消息中间件的开放标准的二进制应用层协议。是一个公开标准,大家都可以基于这个标准来实现消息中间件,不受到开发语言与产品的制约;
常用的消息中间件:JMS Kafka RocketMQ RabbitMQ ActiveMQ Pulsar等等
MQ技术的发展史:
Kafka是一种分布式流式系统,被设计为能够作为一个统一平台来处理大型公司可能拥有的所有实时数据馈送。为此,它必须具有高吞吐量才能支持大容量事件流。
RabbitMQ支持多种客户端,如Python、Java、.NET、C、Ruby等,在易用性、扩展性、高可用性等方面表现都不错,并且可以与SpringAMQP完美整合,API丰富易用。
消息队列概念
我们谈到消息队列就会想到:生产者、消费者和消息队列。生产者将消息发送到消息队列,消费者从消息队列中获取消息然后进行处理。队列可以主动将消息推送给订阅队列的消费者,消费者也可以自己主动从消息队列pull和fetch消息。
交换机是发送消息的实体,交换机收到消息然后将其路由到一个或多个队列中。
交换机共有四种类型:
交换类型 | 默认的预定义名称 | |
---|---|---|
直连交换机(Direct exchange) | 空字符串和amq.direct | 一对一 |
扇形交换机(Fanout exchange) | amq.fanout | 广播 |
主题交换机(Topic exchange) | amq.topic | 一对多,模糊匹配 |
头信息交换机(Headers exchange) | amq.match 和RabbitMQ中的 amq.headers |
交换机的属性
默认交换机
默认交换机是由broker预先声明的匿名直连交换机。使用默认交换机的时候每个新建的队列都会绑定到默认交换机上,绑定的路由与队列名称相同。
直连交换机根据消息路由键(router_key)
将消息传递到队列,消息将会投递到与路由键名称和队列名称相同的队列上。直接交换机是消息单播路由的理想选择(尽管它们也可以用于多播路由)。
扇形交换机将消息路由到绑定到它的所有队列,并且忽略路由键。也就是说,当新消息发布到该交换机时,该消息的副本将投递到所有绑定该交换机的队列。扇形交换机是消息广播路由的理想选择。
主题交换机根据消息路由键和和用于将队列绑定到交换机的模式匹配字符串之间的匹配将消息路由到一个或者多个队列。
也就是说通过消息的路由键去匹配到绑定到交换机的路由键匹配字符串,如果匹配上了,就进行投递消息。
头交换机不依赖路由键的匹配规则来路由消息,而是根据发送消息内容中的请求头属性进行匹配。
头交换机类似于直连交换机,但是直连交换机的路由键必须是一个字符串,而请求头信息则没有这个约束,它们甚至可以是整数或者字典。因此可以用作路由键不必是字符串的直连交换。
绑定一个队列到头交换机上的时候,会同时绑定多个用于匹配的头信息。
我们在使用队列之前需要先对队列进行声明,声明时如果队列不存在则创建一个队列,如果队列存在判断存在的队列属性是否与声明中的属性相同,相同则不再创建,否则引发错误。
队列名称
可主动设置队列名称,如果队列名称为空broker会为队列生成一个唯一的队列名称,一起返回给客户端。队列名称限制255字节以内的utf-8 字符。
不可声明
amq
开头的队列名称,此关键字保留给broker内部使用,否则会出现异常。
队列持久化
队列持久化的元数据会存储到硬盘中,broker重启后,队列依然存在。没有被持久化的队列称为暂存队列。发布的消息也有同样的区分,也就是说,持久化的队列并不会使得路由到它的消息也具有持久性,需要手动把消息也标记为持久化才能保证消息的持久性。
消息如果一直存储在队列中没有被消费就没有什么实际意义。
消费者获取消息的两种方式
一个队列可以有多个消费者进行订阅,队列可以向所有订阅的消费者发送广播。
消费者应用程序可能偶尔无法处理单个消息或有时会崩溃,另外网络问题也有可能导致问题。这就提出了一个问题:**Broker何时应该从队列中删除消息?**AMQP 0-9-1 规范中约定让消费者对此进行控制,有两种确认模式:
在显示模式下,应用程序选择何时发送确认消息。如果消费者在没有发送确认的情况下就挂掉了,那么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
发送者确认模式开启,消息持久化默认开启,消费者消费开启手动ack
生产者丢数据:RabbitMQ提供transaction(事务,支持回滚)和confirm模式(ACK给生产者)来确保生产者不丢消息;
消息队列丢数据:开启rabbitmq的持久化,就是消息写入之后会持久化到磁盘,持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack。
消费者丢失数据:消费者丢数据一般是因为采用了自动确认消息模式,改为手动确认消息,处理消息成功后,手动回复确认消息。
交换机备份也可以队列不可用的问题,工作流程当一个生产者发送一条消息时,当这条消息无法被路由到队列时,这消息就会被推到备用交换机上,备用交换机是一个广播交换机拥有2个队列,1备份队列(用于存储和备份消息),2告警队列(用于发送告警信息)在原有的基础上进行改造增加备用交换机,以及备份队列与告警队列,和2个消费者
内存告警和磁盘告警
内存告警
内存使用超过配置的阈值或者磁盘剩余空间低于配置阈值时,MQ会暂时阻塞客户端的连接,停止接收从客户端发来的消息从而避免服务崩溃,客服端与服务端的心跳检测也会失效。
1. 当内存告警时可通过一下命令临时调整内存大小
RabbitMQctl set_vm_memory_high_watermark
fraction为内存阈值,RabbitMQ默认是0.4,表示当RabbitMQ使用的内存超过总内存的40%时,就会产生告警并阻塞所有生产则连接。
通过此命令修改的阈值在RabbitMQ重启之后将会失效,通过修改配置文件的方式设置的阈值才会永久有效,但需要重启服务才会生效。
#相对值,也就是前面的fraction,建议设置在0.4~0.66之间,不要超过0.7
vm_memory_high_watermark.relative=0.4
#绝对值,单位为KB,MB,GB,对应的临时命令是:RabbitMQctl set_vm_memory_high_watermark absolute
#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
#设置相对值,建议取值为1.0~2.0(相对于内存的倍数,如内存大小是8G,若为1.0,则表示磁盘剩余8G时,阻塞)
RabbitMQctl set_disk_free_limit mem_relative
修改配置文件
对应的配置文件配置如下:
disk_free_limit.relative=2.0
#disk_free_limit_absolute=50MB
消息保存到磁盘中的格式
消息保存与$MNESIA/msg_store_persisten/x.rdq文件中
message又称消息,是服务器与应用程序之间传递的数据,有properties+body组成,properties中可以对消息的优先、传输格式、延迟等特性进行定义,body则是消息体内容。
消息的三种状态
mkdir /opt/rabbitmq
cd /opt/rabbitmq
docker pull rabbitmq
#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
docker ps -l
docker update rabbitmq --restart=always
docker exec -it rabbitmq /bin/bash
---------------------------------
user@7b295c46c99d /: rabbitmq-plugins enable rabbitmq_management
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
我基于centos8进行安装
yum install -y gcc gcc-c++ openssl openssl-devel ncurses-devel
[root@localhost /]# yum install socat -y
## 指定下载位置/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
提示:可以在 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
# 设置开机自启
[root@localhost download]# systemctl enable rabbitmq-server.service
# 启动服务
[root@localhost download]# systemctl start rabbitmq-server.service
# 查看状态
[root@localhost download]# systemctl status rabbitmq-server.service
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
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
浏览器输入:http://ip+端口(15672),例如:http://192.168.0.45:15672
首先在官网下载安装包
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
#设置密码。。。
yum install -y gcc gcc-c++ openssl openssl-devel ncurses-devel socat perl
#解压安装包到指定目录
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
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()
.
解压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/
先进入配置目录
cd /home/liuch/rabbitmq/rabbitmq_server-3.10.5/etc/rabbitmq/
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
cd ~
vim .bash_profile
在最后一行添加
export PATH=$PATH:/home/liuch/rabbitmq/rabbitmq_server-3.10.5/sbin
source .bash_profile
rabbitmq-server -detatched
查看启动状态
rabbitmqctl status
安装后不能用root用户使用systemctl命令进行启动,需要自己单独进行配置
配置systemctl启动
首先需要切换到root用户
修改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
source .bash_profile
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中对配置进行修改,这里就不演示了
cd /etc/rabbitmq
vim rabbitmq.conf
#server启动端口
listeners.tcp.default=6650
#web管理服务端口
management.tcp.port=6671
cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.10.4/sbin/
vim rabbitmq-defaults
#添加配置路径到文件中,保存退出
CONFIG_FILE=/etc/rabbitmq/rabbitmq.conf
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
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: #最大间隔
/**
* @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();
}
}
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+死信队列)重新投递消息,超过最大次数入库 人工干预处理
}
}
参数解释
/**
* @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})
交换机无应答时需要将消息重发或者将消息保存到死信队列中,方便我们后期进行消息的溯源和手动处理,交换机再消息发送失败后返回的回调信息中只包含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(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();
/**
* @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;
}
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);
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
log.info("消息发送到交换机成功:" + correlationData);
} else {
//消息发送到交换机失败,进行重发操作
this.retryMessage(correlationData);
}
}
/**
* 重新推送消息方法
*
* @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模式,所有在该信道上面发布
的消息都会被指派一个唯一的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回调,当向备份交换机转发消息失败是才会触发方法回调
概念:死信队列是一种消息机制,当消息出现一下情况时会将消息放入死信队列
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。
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命令即可。
启动插件后需要重启一下mq
[root@localhost ~]# systemctl restart rabbitmq-server.service
进入web管理界面,新增交换机这时可以发现新增了一种延迟交换机,此时代表插件安装成功
/**
* 延迟队列相关,用来错误消息重发
*/
@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)性能开销大,因为需要进行整个集群内部所有实例的数据同步;
(2)无法线性扩容: 因为每一个服务器中都包含整个集群服务节点中的所有数据, 这样如果一旦单个服务器节点的容量无法容纳了。
将多个单机应用连接到一起然后向外暴露出一个接口,内部多台机器进行连接但是在外部看来就是一个整体。
普通模式(提升了吞吐量,无法保证高可用)
数据不会同步,只是同步元数据(Queue的一些配置信息),队列中的消息还是将存在单一的服务中,不会同步到其他队列,比如现在数据存在A中,用户想访问A中的内容可以通过A进行访问也可以通过B进行访问,如果A发生了宕机,B也访问不了A的数据。
当我们消费消息的时候,如果连接到了另外一个实例,那么那个实例会通过元数据定位到 Queue 所在的位置,然后访问 Queue 所在的实例,拉取数据过来发送给消费者。
镜像模式
会将服务的数据进行同步,即每台服务上都是完整的数据内容。每次写入消息的时候都会自动把数据同步到多台实例上,这样一台发生故障其他的机器也可以继续提供服务。
搭建集群
hostnamectl set-hostname rabbit-node1
192.168.204.141 A
192.168.204.142 B
scp /var/lib/rabbitmq/.erlang.cookie 192.168.204.142:/var/lib/rabbitmq
修改cookie文件要重启服务器
rabbitmqctl cluster_status
#注意节点名称和之前配置一致,否则需要重新配置hostname或重启
rabbitmqctl stop_app
rabbitmqctl join_cluster rabbit@rabbitmq-node1
rabbitmqctl start_app
rabbitmqctl cluster_status
移除节点 rabbitmqctl forget_cluster_node rabbit@rabbitmq-node2
搭建集群结构之后,之前创建的交换机、队列、用户都属于单一结构,在新的集群环境中是不能用的
所以在新的集群中重新手动添加用户即可(任意节点添加,所有节点共享)
rabbitmqctl add_user root 123123
rabbitmqctl set_user_tags root administraor
rabbitmqctl set_permissions -p "/" root " . * " " . * " " . * "
注意:当节点脱离集群还原成单一结构后,交换机,队列和用户等数据都会重新回来
可以看到nodes中存在多个节点
在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
负载均衡
暂时没做,后期补充
配置镜像队列以后,新创建的队列按照规则成为镜像队列。每个镜像队列都包含一个主节点和若干个从节点,只有主节点会向用户提供服务。从节点接收到主节点的命令进行数据操作,从节点的状态是与主节点一致的。
配置方法
使用策略来配置镜像队列,策略使用正则表达式来配置需要应用镜像策略的队列名称,以及在参数中配置镜像队列的具体参数。
命令行(未测试)
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 将丢弃其所有数据,并需要同步来恢复全部数据冗余。
保证消息不会被重复消费
生产者保证消息不丢失,会将ack=false的消息重新发送,这样就可能导致我们的消息重复。
生产者为每一条消息设置一个MessageId,用于消费端去重,也可以利用correlation。
rabbitmq-plugins enable rabbitmq_tracing
每次服务停掉以后都需要重新开启并配置日志策略
从3.8版本以后可用,当我们对数据的安全性要求大于对响应速度和资源占用时
quorum queue是镜像队列mirror queue的替代品。它基于Raft 共识算法实现了一个持久的、复制的 FIFO 队列。
quorum 队列是持久的,不是排他的,支持队列的TTL不支持消息的TTL,支持死信。仲裁队列默认镜像数是5,集群节点不足5则都是镜像。+n表示有n个镜像节点。
特性:
/**
* 仲裁队列
*/
@Bean
public Queue quorumQueue() {
return QueueBuilder.durable("quorumQueue")
.quorum()
.build();
}
消费者动态创建队列并实现动态监听
在消费者中实现1.2.3
@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);
}
}
@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();
}
}
}
@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+" 成功";
}
}
@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 "发送成功";
}
}
http://localhost:8081/send
然后看rabbitmqAdmin中
通过http://127.0.0.1:8082/queue/1 增加监听,发现客户端可以获取到消息
http://127.0.0.1:8082/delete/server_app.1 移除监听后再发发送消息,无法进行接收