Rabbit
(1条消息) linux防火墙查看状态firewall_老师好我是大白的博客-CSDN博客_firewall状态
1、查看firewall服务状态
systemctl status firewalld
1
出现Active: active (running)切高亮显示则表示是启动状态。
2、查看firewall的状态
firewall-cmd --state
1
3、开启、重启、关闭、firewalld.service服务
### 开启
service firewalld start
### 重启
service firewalld restart
### 关闭
service firewalld stop
123456
4、查看防火墙规则
firewall-cmd --list-all
1
5、查询、开放、关闭端口
### 查询端口是否开放
firewall-cmd --query-port=8080/tcp
### 开放80端口
firewall-cmd --permanent --add-port=80/tcp
### 移除端口
firewall-cmd --permanent --remove-port=8080/tcp
### 重启防火墙(修改配置后要重启防火墙)
firewall-cmd --reload
1234567891011
参数解释
firwall-cmd:是Linux提供的操作firewall的一个工具;
--permanent:表示设置为持久;
--add-port:标识添加的端口;
报错
Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'x-message-ttl' for queue 'normal_queue' in vhost '/': received the value '10000' of type 'signedint' but current is none, class-id=50, method-id=10)
- 原本的队列已经生成,再启动时改变参数就相当于重新生成一个新的队列,但是此时这个名称的队列已经生成了
为什么要用 MQ
1.流量消峰
举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正 常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限 制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分 散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体 验要好。
2.应用解耦
以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合 调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于 消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在 这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流 系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。
3.异步处理
有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可 以执行完,以前一般有两种方式,A 过一段时间去调用 B 的查询 api 查询。或者 A 提供一个 callback api, B 执行完之后调用 api 通知 A 服务。这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题, A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此 消息转发给 A 服务。这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样 B 服务也不 用做这些操作。A 服务还能及时的得到异步处理成功的消息。
四大核心概念
生产者
产生数据发送消息的程序是生产者
交换机
交换机是 RabbitMQ 非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息 推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推 送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定
队列
队列是 RabbitMQ 内部使用的一种数据结构,尽管消息流经 RabbitMQ 和应用程序,但它们只能存 储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可 以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式
### 消费者
消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费 者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。
工作原理(图)
Broker:接收和分发消息的应用,RabbitMQ Server 就是 Message Broker Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似 于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出 多个 vhost,每个用户在自己的 vhost 创建 exchange/queue 等
Connection:publisher/consumer 和 broker 之间的 TCP 连接
Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程 序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客 户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销
Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发 消息到 queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)
Queue:消息最终被送到这里等待 consumer 取走
Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保 存到 exchange 中的查询表中,用于 message 的分发依据
安装教程
安装
1.官网地址
https://www.rabbitmq.com/download.html
2.文件上传
上传到/usr/local/software 目录下(如果没有 software 需要自己创建)
3.安装文件(分别按照以下顺序安装)
rpm -ivh erlang-21.3-1.el7.x86_64.rpm
yum install socat -y
rpm -ivh rabbitmq-server-3.8.8-1.el7.noarch.rpm
3.常用命令(按照以下顺序执行)
添加开机启动 RabbitMQ 服务
chkconfig rabbitmq-server on
启动服务
/sbin/service rabbitmq-server start
查看服务状态
/sbin/service rabbitmq-server status
停止服务(选择执行)
/sbin/service rabbitmq-server stop
开启 web 管理插件
rabbitmq-plugins enable rabbitmq_management
用默认账号密码(guest)访问地址 http://47.115.185.244:15672/出现权限问题
4.添加一个新的用户
创建账号
rabbitmqctl add_user admin 123
设置用户角色
rabbitmqctl set_user_tags admin administrator
设置用户权限
set_permissions [-p <vhostpath>] <user> <conf> <write> <read>
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
用户 user_admin 具有/vhost1 这个 virtual host 中所有资源的配置、写、读权限
当前用户和角色
rabbitmqctl list_users
5.再次利用 admin 用户登录
6. 重置命令
关闭应用的命令为
rabbitmqctl stop_app
清除的命令为
rabbitmqctl reset
重新启动命令为
rabbitmqctl start_app
hello
原生依赖
<dependencies>
<!--rabbitmq 依赖客户端-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
<!--操作文件流的一个依赖-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
生产者
/**
* Created by KingsLanding on 2022/9/16 1:22
* <p>
* 生产者
*/
public class Producer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) {
//1.创建链接工厂
ConnectionFactory factory = new ConnectionFactory();
//2.设置连接基本信息
factory.setHost("192.168.222.128");
factory.setUsername("admin");
factory.setPassword("123123");
//3.获取一个链接,创建信道(实现自动close接口)
try {
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
/**
* 4.生成一个队列
* 1、队列名称
* 2、队列里的消息是否持久化,默认消息存储在内存中
* 3、如果声明的是独占队列(仅限于此连接),则为true,共享为false
* 4、是否自动删除,最后一个消费者断开连接后该队列是否自动删除
* 5、其他参数
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String massage = "hello,RabbitMQ";
/**
* 5.发送一个消息
* 1、发送到哪个交换机
* 2、路由的key是哪个,也就是哪个消息队列
* 3、其他的参数信息
* 4、发送消息的消息体
*/
channel.basicPublish("", QUEUE_NAME, null, massage.getBytes());
System.out.println("消息发送成功!");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
消费者
/**
* Created by KingsLanding on 2022/9/16 1:42
* <p>
* 消费者
*/
public class Consumer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
//1.创建链接工厂
ConnectionFactory factory = new ConnectionFactory();
//2.设置连接基本信息
factory.setHost("192.168.222.128");
factory.setUsername("admin");
factory.setPassword("123123");
//3.获取一个链接,创建信道(实现自动close接口)
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//等待接收消息
//4.推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
byte[] body = message.getBody();
String massage = new String(body);
System.out.println(massage);
}
};
//5.取消消费的一个回调接口,如在消费时队列被删除
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
System.out.println("消费中断");
}
};
/**
* 6.消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true自动
* 3.成功
* 3.消费失败的回调
*/
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
System.out.println("消息接收");
}
}
轮询
公平分发
模式说明:消息发送到一个队列,由两个或以上消费者进行消费,此时队列中的消息消费是公平模式,一个消费者消费一个消息,消费完就由下一个消费者消费。
抽取工具类
/**
* Created by KingsLanding on 2022/9/16 15:25
*/
public class Utils {
//队列名称
public static final String QUEUE_NAME = "hello";
//获取信道
public static Channel RabbitChannel() throws IOException, TimeoutException {
//连接池信息
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.222.128");
factory.setUsername("admin");
factory.setPassword("123123");
//创建链接
Connection connection = factory.newConnection();
//获取信道
Channel channel = connection.createChannel();
return channel;
}
}
生产者
/**
* Created by KingsLanding on 2022/9/16 15:25
*/
public class Producer {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
//获取信道
Channel channel = Utils.RabbitChannel();
/**
* 4.生成一个队列
* 1.队列名称
* 2.队列里的消息是否持久化,默认消息存储在内存中
* 3.如果声明的是独占队列(仅限于此连接),则为true,共享为false
* 4.是否自动删除,最后一个消费者断开连接后该队列是否自动删除
* 5.其他参数
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String massage = scanner.next();
/**
* 5.发送一个消息
* 1.发送到哪个交换机
* 2.路由的key是哪个,也就是哪个消息队列
* 3.其他的参数信息
* 4.发送消息的消息体
*/
channel.basicPublish("", QUEUE_NAME, null, massage.getBytes());
System.out.println("消息发送成功!");
}
}
}
work01
/**
* Created by KingsLanding on 2022/9/16 15:35
*/
public class work01 {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = Utils.RabbitChannel();
//等待接收消息
//4.推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
byte[] body = message.getBody();
String massage = new String(body);
System.out.println(massage);
}
};
//5.取消消费的一个回调接口,如在消费时队列被删除
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
System.out.println("消费中断");
}
};
/**
* 6.消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true自动
* 3.成功
* 3.消费失败的回调
*/
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
System.out.println("c1消息接收");
}
}
work02
/**
* Created by KingsLanding on 2022/9/16 15:35
*/
public class work01 {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = Utils.RabbitChannel();
//等待接收消息
//4.推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
byte[] body = message.getBody();
String massage = new String(body);
System.out.println(massage);
}
};
//5.取消消费的一个回调接口,如在消费时队列被删除
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
System.out.println("消费中断");
}
};
/**
* 6.消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true自动
* 3.成功
* 3.消费失败的回调
*/
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
System.out.println("c2消息接收");
}
}
消息应答
为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,消息应答就是:消费者在接 收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。
自动应答
消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权 衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel 关闭,那么消息就丢 失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制, 当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终 使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并 以某种速率能够处理这些消息的情况下使用。
消息自动重新入队
如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息 未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者 可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确 保不会丢失任何消息。
手动应答
默认消息采用的是自动应答,所以我们要想实现消息消费过程中不丢失,需要把自动应答改为手动应答
multiple 的 true 和 false 代表不同意思
true 代表批量应答 channel 上未应答的消息比如说 channel 上有传送 tag 的消息 5,6,7,8 当前 tag 是 8 那么此时 5-8 的这些还未应答的消息都会被确认收到消息应答
false 同上面相比 只会应答 tag=8 的消息 5,6,7 这三个消息依然不会被确认收到消息应答
//等待接收消息
//4.推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
//中断测试
Sleep.sleep(1);
byte[] body = message.getBody();
String massage = new String(body);
System.out.println("接收到消息" + massage);
//手动应答
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
}
};
RabbitMQ 持久化
如何保障当 RabbitMQ 服务停掉以后消 息生产者发送过来的消息不丢失。默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化。
队列持久化(durable)
/*
durable:队列是否持久化
*/
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
消息持久化
MessageProperties.PERSISTENT_TEXT_PLAIN
//props:消息持久化 MessageProperties.PERSISTENT_TEXT_PLAIN持久化属性
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, massage.getBytes());
注:
将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是 这里依然存在当消息刚准备存储在磁盘的时候但是还没有存储完,消息还在缓存的一个间隔点。此时并没 有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。如果需要更强有力的持久化策略,需要用到发布确认
不公平分发
比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2 处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间 处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是 RabbitMQ 并不知道这种情况它依然很公平的进行分发。
预取值 channel.basicQos();
/*
1、概念一:设置不公平分发channel.basicQos(1);
能者多劳,相比之前的轮询,两个队列交替处理消息会导致处理较慢的队列拖慢整体进程
设置prefetchCount为1 ,代表如果某一个队列处理较慢,立马又空闲队列进行消息处理
2、概念二:预取值:channel.basicQos(2);也就是预先取两个消息进入信道排队,哪怕我 很快但是只取两个在我的信道里排队
*/
// channel.basicQos(1);
channel.basicQos(2);
注:预取值为 1 是最保守的。当然这 将使吞吐量变得很低,特别是消费者连接延迟很严重的情况下,特别是在消费者连接等待时间较长的环境 中。对于大多数应用来说,稍微高一点的值将是最佳的。
发布确认
生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的 消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传 给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置 basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。
单个确认发布
一种同步确认发布的方式
是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认 的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。
缺点:发布速度特别的慢,因为如果没有确认发布的消息就会 阻塞所有后续消息的发布
//单个确认
public static void PublishOne() throws IOException, TimeoutException, InterruptedException {
Channel channel = Utils.RabbitChannel();
//开启发步确认功能
channel.confirmSelect();
//队列名称
String queue = UUID.randomUUID().toString();
//创建队列
channel.queueDeclare(queue, false, false, false, null);
//测试耗时
long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
String message = i + "";
channel.basicPublish("", queue, null, message.getBytes());
//服务端返回 false 或超时时间内未返回,生产者可以消息重发
boolean confirms = channel.waitForConfirms();
if (confirms) {
System.out.println("消息发布成功");
}
}
long end = System.currentTimeMillis();
System.out.println("单个发布确认耗时为:" + (end - start));//发布耗时为:415
}
批量确认发布
与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量
缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
//批量确认
public static void PublishMore() throws IOException, TimeoutException, InterruptedException {
Channel channel = Utils.RabbitChannel();
//开启发步确认功能
channel.confirmSelect();
//队列名称
String queue = UUID.randomUUID().toString();
//创建队列
channel.queueDeclare(queue, false, false, false, null);
//测试耗时
long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
String message = i + "";
channel.basicPublish("", queue, null, message.getBytes());
//服务端返回 false 或超时时间内未返回,生产者可以消息重发
//消息发送一百次后,批量确认
if ((i + 1) % 100 == 0) {
boolean confirms = channel.waitForConfirms();
if (confirms) {
System.out.println("消息发布成功");
}
}
}
long end = System.currentTimeMillis();
System.out.println("批量发布耗时为:" + (end - start));//批量发布耗时为:89
}
异步确认发布
//异步确认
public static void PublishAsync() throws IOException, TimeoutException, InterruptedException {
Channel channel = Utils.RabbitChannel();
//开启发步确认功能
channel.confirmSelect();
//队列名称
String queue = UUID.randomUUID().toString();
//创建队列
channel.queueDeclare(queue, false, false, false, null);
//消息保存
/*
线程安全有序的哈希表,适用于高并发的情况
1、将序号与消息关联
2、批量删除条目,只要拿到序列号
3、支持并发访问
*/
ConcurrentSkipListMap<Long, String> skipListMap = new ConcurrentSkipListMap<>();
//消息确认成功,回调函数
/*
deliveryTag:消息的标记
multiple:是否是批量确认
*/
ConfirmCallback ackCallback = new ConfirmCallback() {
@Override
public void handle(long deliveryTag, boolean multiple) throws IOException {
//如果批量确认;获取批量确认清除
if (multiple) {
ConcurrentNavigableMap<Long, String> navigableMap = skipListMap.headMap(deliveryTag, true);
navigableMap.clear();
} else {
//非批量确认,直接从集合中删除
skipListMap.remove(deliveryTag);
}
System.out.println("确认的消息" + deliveryTag + "---批量" + multiple);
}
};
//消息确认失败,回调函数
ConfirmCallback nackCallback = new ConfirmCallback() {
@Override
public void handle(long deliveryTag, boolean multiple) throws IOException {
//删除了已经确认的消息,集合中就只剩下未被确认的消息
String message = skipListMap.get(deliveryTag);
System.out.println("未确认的消息:" + message + "序列号" + deliveryTag + "");
}
};
//添加一个异步确认监听器
/*
ackCallback:监听哪些消息成功了
nackCallback:监听哪些消息失败了
*/
channel.addConfirmListener(ackCallback, nackCallback);
//测试耗时
long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
String message = i + "";
/*
将发送的所有消息存到map中
*/
skipListMap.put(channel.getNextPublishSeqNo(), message);
channel.basicPublish("", queue, null, message.getBytes());
}
long end = System.currentTimeMillis();
System.out.println("异步发布耗时为:" + (end - start));//异步发布耗时为:39
}
交换机Exchanges
RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不会直接发送到队列。实际上,通常生产者甚至都不知道这些消息传递传递到了哪些队列中。
生产者只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接收来自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们。这就的由交换机的类型来决定。
Exchanges 的类型
直接(direct), 主题(topic) ,标题(headers) , 扇出(fanout)
Fanout
接收到的所有消息广播到它知道的 所有队列中。
生产者
/**
* Created by KingsLanding on 2022/9/18 15:35
* 广播模式:fanout
*/
public class Emit {
public static final String Exchanges_name = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = Utils.RabbitChannel();
//交换机:名称、类型(扇出or广播)
channel.exchangeDeclare(Exchanges_name, "fanout");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String message = scanner.next();
channel.basicPublish(Exchanges_name, "", null, message.getBytes());
System.out.println("消息发送成功");
}
}
}
消费者1
/**
* Created by KingsLanding on 2022/9/18 15:07
*/
public class Receive {
public static final String Exchanges_name = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = Utils.RabbitChannel();
//生成交换机
/*
Fanout 这种类型非常简单。正如从名称中猜到的那样,它是将接收到的所有消息广播到它知道的所有队列中。
*/
channel.exchangeDeclare(Exchanges_name, "fanout");
/*
生成一个临时队列:用完即删
消费者断开该队列连接时,队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
//将该临时队列与交换机进行绑定
/*
1.队列名称
2.交换机名称
3.routingKey路由key,指定哪个队列,广播模式下不用管
*/
channel.queueBind(queueName, Exchanges_name, "");
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery delivery) throws IOException {
String message = new String(delivery.getBody());
System.out.println("C1接收:" + message);
}
};
CancelCallback confirmCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
System.out.println("为确认的消息");
}
};
channel.basicConsume(queueName, true, deliverCallback, confirmCallback);
}
}
消费者2
/**
* Created by KingsLanding on 2022/9/18 15:34
*/
public class Receive02 {
public static final String Exchanges_name = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = Utils.RabbitChannel();
//生成交换机
/*
Fanout 这种类型非常简单。正如从名称中猜到的那样,它是将接收到的所有消息广播到它知道的所有队列中。
*/
channel.exchangeDeclare(Exchanges_name, "fanout");
/*
生成一个临时队列:用完即删
消费者断开该队列连接时,队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
//将该临时队列与交换机进行绑定
/*
1.队列名称
2.交换机名称
3.routingKey路由key,指定哪个队列,广播模式下不用管
*/
channel.queueBind(queueName, Exchanges_name, "");
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery delivery) throws IOException {
String message = new String(delivery.getBody());
System.out.println("C2接收:" + message);
}
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
两个消费者都能收到消息
Direct
这种类型的工作方式是,消息只去到它绑定的 routingKey 队列中去。
生产者
/**
* Created by KingsLanding on 2022/9/18 16:36
* 路由模式direct
*/
public class EmitDirect {
public static final String Exchanges_name = "logs_direct";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = Utils.RabbitChannel();
//声明交换机名称及其类型
channel.exchangeDeclare(Exchanges_name, "direct");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String message = scanner.next();
channel.basicPublish(Exchanges_name, "receive01", null, message.getBytes());
System.out.println("消息已经发送");
}
}
}
消费者1
/**
* Created by KingsLanding on 2022/9/18 16:36
* <p>
* 路由模式
*/
public class Receive01 {
public static final String Exchanges_name = "logs_direct";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = Utils.RabbitChannel();
//声明交换机名称及其类型
channel.exchangeDeclare(Exchanges_name, "direct");
//生成一个临时队列
String queueName = channel.queueDeclare().getQueue();
//将该临时队列与交换机进行绑定
//设置routingKey指定路由key
channel.queueBind(queueName, Exchanges_name, "receive01");
//绑定多个key
channel.queueBind(queueName, Exchanges_name, "receive0101");
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
String strMessage = new String(message.getBody());
System.out.println("R1消息接收:" + strMessage);
}
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
消费者2
/**
* Created by KingsLanding on 2022/9/18 16:36
*/
public class Receive02 {
public static final String Exchanges_name = "logs_direct";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = Utils.RabbitChannel();
//声明交换机
channel.exchangeDeclare(Exchanges_name, "direct");
//生成一个临时队列
String queueName = channel.queueDeclare().getQueue();
//将该临时队列与交换机进行绑定
//设置routingKey指定路由key
channel.queueBind(queueName, Exchanges_name, "receive02");
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
String strMessage = new String(message.getBody());
System.out.println("R2消息接收:" + strMessage);
}
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
只有路由key绑定为receive01的队列才会收到消息
Topics
按照路由key进行消息的动态分配
发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开。这些单词可以是任意单词,比如说:"stock.usd.nyse", "nyse.vmw", "quick.orange.rabbit".这种类型的。当然这个单词列表最多不能超过 255 个字节
-
在这个规则列表中,其中有两个替换符是大家需要注意的
*(星号)可以代替一个单词
#(井号)可以替代零个或多个单词
生产者
/**
* Created by KingsLanding on 2022/9/18 17:58
*/
public class EmitTopics {
public static final String Exchanges_name = "logs_topics";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = Utils.RabbitChannel();
/**
* Q1-->绑定的是
* 中间带 orange 带 3 个单词的字符串(*.orange.*)
* Q2-->绑定的是
* 最后一个单词是 rabbit 的 3 个单词(*.*.rabbit)
* 第一个单词是 lazy 的多个单词(lazy.#)
*
*/
Map<String, String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("quick.orange.rabbit", "被队列 Q1Q2 接收到");
bindingKeyMap.put("lazy.orange.elephant", "被队列 Q1Q2 接收到");
bindingKeyMap.put("quick.orange.fox", "被队列 Q1 接收到");
bindingKeyMap.put("lazy.brown.fox", "被队列 Q2 接收到");
bindingKeyMap.put("lazy.pink.rabbit", "虽然满足两个绑定但只被队列 Q2 接收一次");
bindingKeyMap.put("quick.brown.fox", "不匹配任何绑定不会被任何队列接收到会被丢弃");
bindingKeyMap.put("quick.orange.male.rabbit", "是四个单词不匹配任何绑定会被丢弃");
bindingKeyMap.put("lazy.orange.male.rabbit", "是四个单词但匹配 Q2");
for (Map.Entry<String, String> bindingKeyEntry : bindingKeyMap.entrySet()) {
String bindingKey = bindingKeyEntry.getKey();
String message = bindingKeyEntry.getValue();
channel.basicPublish(Exchanges_name, bindingKey, null,
message.getBytes("UTF-8"));
System.out.println("生产者发出消息" + message);
}
}
}
消费者1
/**
* Created by KingsLanding on 2022/9/18 17:58
*/
public class Receive01 {
public static final String Exchanges_name = "logs_topics";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = Utils.RabbitChannel();
//声明交换机
channel.exchangeDeclare(Exchanges_name, "topic");
//声明队列
channel.queueDeclare("R1", false, false, false, null);
//保定交换机,设置topics规则
channel.queueBind("R1", Exchanges_name, "*.*.rabbit");
channel.queueBind("R1", Exchanges_name, "lazy.#");
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
String massage = new String(message.getBody());
System.out.println("R1消息接收:" + massage);
}
};
channel.basicConsume("R1", true, deliverCallback, consumerTag -> {
});
}
}
消费者2
/**
* Created by KingsLanding on 2022/9/18 17:58
*/
public class Receive02 {
public static final String Exchanges_name = "logs_topics";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = Utils.RabbitChannel();
//声明交换机
channel.exchangeDeclare(Exchanges_name, "topic");
//声明队列
channel.queueDeclare("R2", false, false, false, null);
//保定交换机,设置topics规则
channel.queueBind("R2", Exchanges_name, "*.orange.*");
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
String massage = new String(message.getBody());
System.out.println("R2消息接收:" + massage);
}
};
channel.basicConsume("R2", true, deliverCallback, consumerTag -> {
});
}
}
对应的路由key对应消息只会被发送到相对应的队列
临时队列
每当我们连接到 Rabbit 时,我们都需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者能让服务器为我们选择一个随机队列名称那就更好了。其次一旦我们断开了消费者的连接,队列将被自动删除。
String queueName = channel.queueDeclare().getQueue();
绑定(bindings)
exchange 和那个队列建立了绑定关系
死信队列
producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生异常时,将消息投入死信队列中.还有比如说: 用户在商城下单成功并点击去支付后在指定时间未支付时自动失效
死信的来源
-
消息 TTL 过期
-
队列达到最大长度(队列满了,无法再添加数据到 mq 中)
-
消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false.
消息 TTL 过期
1、过期消息由队列确定x-message-ttl
//声明普通队列
/*
arguments:map类型,用于在队列中绑定死信队列的信息
*/
HashMap<String, Object> arguments = new HashMap<>();
//设置过期时间 注:key一般都是指定的key,相当于绑定配置名,但是一般都在生产者端预先设置过期时间
arguments.put("x-message-ttl",10000);
2、过期时间由生产者携带
//设置消息过期时间
AMQP.BasicProperties properties = new AMQP.BasicProperties()
.builder().expiration("10000").build();
队列达到最大长度 x-max-length
//设置队列最大长度
arguments.put("x-max-length",6);
消息被拒
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
//设置消息拒收
String msg = new String(message.getBody());
if (msg.equals("消息5")) {
System.out.println(msg + ":此消息被C1拒收");
//拒收 参数二:是否返回队列
channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
} else {
String strMessage = new String(message.getBody());
System.out.println("C1消息接收:" + strMessage);
//手动应答,不开启,自动应答会导致拒收的消息被删除
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
}
}
};
DeadConsumer
/**
* Created by KingsLanding on 2022/9/20 15:15
*/
public class DeadConsumer {
public static final String DEAD_EXCHANGE = "dead_exchange";
public static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = Utils.RabbitChannel();
channel.exchangeDeclare(DEAD_EXCHANGE, "direct");
channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "com/dead");
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
String strMessage = new String(message.getBody());
System.out.println("C2消息接收" + strMessage);
}
};
channel.basicConsume(DEAD_QUEUE, deliverCallback, consumerTag -> {
});
}
}
NormalConsumer
/**
* Created by KingsLanding on 2022/9/20 15:16
*/
public class NormalConsumer {
//交换机:普通交换机、死信交换机
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static final String DEAD_EXCHANGE = "dead_exchange";
//队列:普通队列、死信队列
public static final String NORMAL_QUEUE = "normal_queue";
public static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = Utils.RabbitChannel();
//声明交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, "direct");
//声明死信交换机
channel.exchangeDeclare(DEAD_EXCHANGE, "direct");
//声明普通队列
/*
arguments:map类型,用于在队列中绑定死信队列的信息
*/
HashMap<String, Object> arguments = new HashMap<>();
//设置过期时间 注:key一般都是指定的key,相当于绑定配置名,但是一般都在生产者端预先设置过期时间
// arguments.put("x-message-ttl",10000);
//正常队列设置死信交换机
arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//设置死信的RoutingKey
arguments.put("x-dead-letter-routing-key", "com/dead");
//设置队列最大长度
// arguments.put("x-max-length",6);
channel.queueDeclare(NORMAL_QUEUE, false, false, false, arguments);
//死信队列
channel.queueDeclare(DEAD_QUEUE, false, false, false, null);
//交换机与队列的绑定
channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "normal");
channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "com/dead");
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
//设置消息拒收
String msg = new String(message.getBody());
if (msg.equals("消息5")) {
System.out.println(msg + ":此消息被C1拒收");
//拒收 参数二:是否返回队列
channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
} else {
String strMessage = new String(message.getBody());
System.out.println("C1消息接收:" + strMessage);
//手动应答,不开启,自动应答会导致拒收的消息被删除
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
}
}
};
//开启手动应答
channel.basicConsume(NORMAL_QUEUE, false, deliverCallback, consumerTag -> {
});
}
}
Producer
/**
* Created by KingsLanding on 2022/9/20 15:16
*/
public class Producer {
//交换机:普通交换机、死信交换机
public static final String NORMAL_EXCHANGE = "normal_exchange";
//队列:普通队列、死信队列
public static final String NORMAL_QUEUE = "normal_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = Utils.RabbitChannel();
channel.exchangeDeclare(NORMAL_EXCHANGE, "direct");
channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "normal");
//设置消息过期时间
AMQP.BasicProperties properties = new AMQP.BasicProperties()
.builder().expiration("10000").build();
for (int i = 1; i < 11; i++) {
String str = "消息" + i;
channel.basicPublish(NORMAL_EXCHANGE, "normal", properties, str.getBytes());
System.out.println("消息已经发送");
}
}
}
SpringBoot整合RabbitMQ
配置文件
#配置RabbitMQ基本信息
spring:
rabbitmq:
host: 192.168.222.128
port: 5672
username: admin
password: 123123
X:普通交换机
QA:普通10秒延迟队列
QB:普通40秒延迟队列
Y:死信交换机
QD:死信队列
RabbitMQ_Config
使用配置类,将交换机、队列等添加到容器中
/**
* Created by KingsLanding on 2022/9/20 22:11
*/
@Configuration
public class RabbitMQ_Config {
//正常交换机、队列名称
public static final String NORMAL_EXCHANGE_NAME_X="x_exchange";
public static final String NORMAL_QUEUE_NAME_QA="qa_queue";
public static final String NORMAL_QUEUE_NAME_QB="qb_queue";
//死信交换机、队列名称
public static final String DEAD_EXCHANGE_NAME_Y="dead_y_exchange";
public static final String DEAD_QUEUE_NAME_QD="dead_qd_queue";
//组件声明
//x交换机
@Bean
public DirectExchange xExchange(){
return new DirectExchange(NORMAL_EXCHANGE_NAME_X);
}
//y交换机
@Bean
public DirectExchange yExchange(){
return new DirectExchange(DEAD_EXCHANGE_NAME_Y);
}
//qa队列
@Bean
public Queue qaQueue(){
HashMap<String, Object> args = new HashMap<>();
//存放队列相关参数
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME_Y);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key","YD");
//声明队列的 TTL
args.put("x-message-ttl",10000);
//队列声明
return QueueBuilder.durable(NORMAL_QUEUE_NAME_QA).withArguments(args).build();
}
//qb队列
@Bean
public Queue qbQueue(){
HashMap<String, Object> args = new HashMap<>();
//存放队列相关参数
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME_Y);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key","YD");
//声明队列的 TTL
args.put("x-message-ttl",40000);
//队列声明
return QueueBuilder.durable(NORMAL_QUEUE_NAME_QB).withArguments(args).build();
}
//qd死信队列
@Bean
public Queue qdQueue(){
return new Queue(DEAD_QUEUE_NAME_QD);
}
//绑定
//qa与x绑定 qb与x绑定
@Bean
public Binding qaQueueBindingX(@Qualifier("qaQueue") Queue qaQueue,
@Qualifier("xExchange") DirectExchange xExchange){
//可直接调用方法
// Queue queue = qaQueue();
return BindingBuilder.bind(qaQueue).to(xExchange).with("XA");
}
@Bean
public Binding qbQueueBindingX(@Qualifier("qbQueue") Queue qbQueue,
@Qualifier("xExchange") DirectExchange xExchange){
//可直接调用方法
// Queue queue = qaQueue();
return BindingBuilder.bind(qbQueue).to(xExchange).with("XB");
}
//绑定qa与死信交换机Y qb-->Y
@Bean
public Binding qaQueueBindingY(@Qualifier("qaQueue") Queue qaQueue,
@Qualifier("yExchange") DirectExchange yExchange){
//可直接调用方法
// Queue queue = qaQueue();
return BindingBuilder.bind(qaQueue).to(yExchange).with("YD");
}
@Bean
public Binding qbQueueBindingY(@Qualifier("qbQueue") Queue qbQueue,
@Qualifier("yExchange") DirectExchange yExchange){
//可直接调用方法
// Queue queue = qaQueue();
return BindingBuilder.bind(qbQueue).to(yExchange).with("YD");
}
//绑定死信QD与死信交换机
@Bean
public Binding qdQueueBindingY(@Qualifier("qdQueue") Queue qdQueue,
@Qualifier("yExchange") DirectExchange yExchange){
//可直接调用方法
// Queue queue = qaQueue();
return BindingBuilder.bind(qdQueue).to(yExchange).with("YD");
}
}
生产者
//正常交换机、队列名称
public static final String NORMAL_EXCHANGE_NAME_X="x_exchange";
@Autowired
public RabbitTemplate rabbitTemplate;
@GetMapping("/sendMsg/{message}")
public String sendMsg(@PathVariable String message){
log.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", new Date(), message);
rabbitTemplate.convertAndSend(NORMAL_EXCHANGE_NAME_X, "XA", "消息来自 ttl 为 10S 的队列: "+message);
rabbitTemplate.convertAndSend(NORMAL_EXCHANGE_NAME_X, "XB", "消息来自 ttl 为 40S 的队列: "+message);
return message;
}
消费者
/**
* Created by KingsLanding on 2022/9/21 2:33
*/
@Component
@Slf4j
public class DeadLetterQueueConsumer {
public static final String DEAD_QUEUE_NAME_QD="dead_qd_queue";
@RabbitListener(queues = DEAD_QUEUE_NAME_QD)
public void receiveD(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
}
}
第一条消息在 10S 后变成了死信消息,然后被消费者消费掉,第二条消息在 40S 之后变成了死信消息, 然后被消费掉,这样一个延时队列就打造完成了。
不过,如果这样使用的话,岂不是每增加一个新的时间需求,就要新增一个队列,这里只有 10S 和 40S 两个时间选项,如果需要一个小时后处理,那么就需要增加 TTL 为一个小时的队列,如果是预定会议室然 后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?
延时队列优化
/**
* Created by KingsLanding on 2022/9/20 22:11
*/
@Configuration
public class RabbitMQ_Config {
//正常交换机、队列名称
public static final String NORMAL_EXCHANGE_NAME_X="x_exchange";
public static final String NORMAL_QUEUE_NAME_QA="qa_queue";
public static final String NORMAL_QUEUE_NAME_QB="qb_queue";
//死信交换机、队列名称
public static final String DEAD_EXCHANGE_NAME_Y="dead_y_exchange";
public static final String DEAD_QUEUE_NAME_QD="dead_qd_queue";
//延迟队列优化 加入一个不设置条件的队列,条件由生产者携带
public static final String NORMAL_QUEUE_NAME_QC="qc_queue";
//优化队列声明
@Bean
public Queue qcQueue(){
HashMap<String, Object> args = new HashMap<>();
//存放队列相关参数
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME_Y);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key","YD");
return QueueBuilder.durable(NORMAL_QUEUE_NAME_QC).withArguments(args).build();
}
//绑定普通X交换机
@Bean
public Binding qcQueueBindingX(@Qualifier("qcQueue") Queue qcQueue,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(qcQueue).to(xExchange).with("XC");
}
//绑定死信Y交换机
@Bean
public Binding qcQueueBindingY(@Qualifier("qcQueue") Queue qcQueue,
@Qualifier("yExchange") DirectExchange yExchange){
return BindingBuilder.bind(qcQueue).to(yExchange).with("YD");
}
//组件声明
//x交换机
@Bean
public DirectExchange xExchange(){
return new DirectExchange(NORMAL_EXCHANGE_NAME_X);
}
//y交换机
@Bean
public DirectExchange yExchange(){
return new DirectExchange(DEAD_EXCHANGE_NAME_Y);
}
//qa队列
@Bean
public Queue qaQueue(){
HashMap<String, Object> args = new HashMap<>();
//存放队列相关参数
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME_Y);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key","YD");
//声明队列的 TTL
args.put("x-message-ttl",10000);
//队列声明
return QueueBuilder.durable(NORMAL_QUEUE_NAME_QA).withArguments(args).build();
}
//qb队列
@Bean
public Queue qbQueue(){
HashMap<String, Object> args = new HashMap<>();
//存放队列相关参数
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange",DEAD_EXCHANGE_NAME_Y);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key","YD");
//声明队列的 TTL
args.put("x-message-ttl",40000);
//队列声明
return QueueBuilder.durable(NORMAL_QUEUE_NAME_QB).withArguments(args).build();
}
//qd死信队列
@Bean
public Queue qdQueue(){
return new Queue(DEAD_QUEUE_NAME_QD);
}
//绑定
//qa与x绑定 qb与x绑定
@Bean
public Binding qaQueueBindingX(@Qualifier("qaQueue") Queue qaQueue,
@Qualifier("xExchange") DirectExchange xExchange){
//可直接调用方法
// Queue queue = qaQueue();
return BindingBuilder.bind(qaQueue).to(xExchange).with("XA");
}
@Bean
public Binding qbQueueBindingX(@Qualifier("qbQueue") Queue qbQueue,
@Qualifier("xExchange") DirectExchange xExchange){
//可直接调用方法
// Queue queue = qaQueue();
return BindingBuilder.bind(qbQueue).to(xExchange).with("XB");
}
//绑定qa与死信交换机Y qb-->Y
@Bean
public Binding qaQueueBindingY(@Qualifier("qaQueue") Queue qaQueue,
@Qualifier("yExchange") DirectExchange yExchange){
//可直接调用方法
// Queue queue = qaQueue();
return BindingBuilder.bind(qaQueue).to(yExchange).with("YD");
}
@Bean
public Binding qbQueueBindingY(@Qualifier("qbQueue") Queue qbQueue,
@Qualifier("yExchange") DirectExchange yExchange){
//可直接调用方法
// Queue queue = qaQueue();
return BindingBuilder.bind(qbQueue).to(yExchange).with("YD");
}
//绑定死信QD与死信交换机
@Bean
public Binding qdQueueBindingY(@Qualifier("qdQueue") Queue qdQueue,
@Qualifier("yExchange") DirectExchange yExchange){
//可直接调用方法
// Queue queue = qaQueue();
return BindingBuilder.bind(qdQueue).to(yExchange).with("YD");
}
}
生产者
@GetMapping("/sendMsg/{message}/{ttl}")
public String sendMsg(@PathVariable String message,@PathVariable String ttl){
log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(), message,ttl);
//将过期时间由生产端发送到队列
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration(ttl);//设置过期时间为50秒
return message;
}
};
rabbitTemplate.convertAndSend(NORMAL_EXCHANGE_NAME_X,"XC",message,messagePostProcessor);
return message+"***"+ttl;
}
存在的问题
看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用在消息属性上设置 TTL 的方式,消息可能并不会按时“死亡“,因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。
Rabbitmq 插件实现延迟队列
RabbitMQ插件的安装
rabbitmq_delayed_message_exchange-3.8.0.ez插件安装包:所在目录/usr/local/software
将其复制到RabbitMQ的插件专用目录
/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
复制:cp rabbitmq_delayed_message_exchange-3.8.0.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
安装
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
安装之后将RabbitMQ重启
自定义交换机
在我们自定义的交换机中,这是一种新的交换类型,该类型消息支持延迟投递机制 消息传递后并 不会立即投递到目标队列中,而是存储在 mnesia(一个分布式数据系统)表中,当达到投递时间时,才 投递到目标队列中。
因为队列中的消息都是要排队的,所以延时信息也会进入排队 所以如果第一个入队的延时信息A是10秒,第二入队B的是5秒,那么B尽管延迟是5秒但是仍然需要等待A完成之后才能被消费,或者进入死信
DelayedMessageExchange_Config
/**
* Created by KingsLanding on 2022/9/21 22:00
*
* 用于延时队列插件的配置类
* 因为队列中的消息都是要排队的,所以延时信息也会进入排队
* 所以如果第一个入队的延时信息A是10秒,第二入队B的是5秒,那么B需要等待A完成之后才能被消费,
* 或者进入死信
*/
@Configuration
public class DelayedMessageExchange_Config {
/*
在我们自定义的交换机中,这是一种新的交换类型,该类型消息支持延迟投递机制 消息传递后并
不会立即投递到目标队列中,而是存储在 mnesia(一个分布式数据系统)表中,当达到投递时间时,才
投递到目标队列中。
*/
//交换机名称
public static final String DELAYED_EXCHANGE_NAME="delayed.exchange";
//队列名称
public static final String DELAYED_QUEUE_NAME="delayed.queue";
//声明自定义的交换机 我们在这里定义的是一个延迟交换机
@Bean
public CustomExchange delayedExchange(){
HashMap<String, Object> args = new HashMap<>();
//自定义交换机的类型
args.put("x-delayed-type", "direct");
/*
参数一:交换机名称
参数二:交换机类型
参数三:是否持久化
参数四:是否自动删除
参数五:其他参数设置
*/
return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,args);
}
//声明队列
@Bean
public Queue delayedQueue(){
return new Queue(DELAYED_QUEUE_NAME);
}
//队列绑定交换机
@Bean
public Binding delayedQueueBindingDelayedExchange(@Qualifier("delayedExchange") CustomExchange delayedExchange,
@Qualifier("delayedQueue") Queue delayedQueue){
return BindingBuilder.bind(delayedQueue).to(delayedExchange).with("delayed.routingKey").noargs();
}
}
生产者
//利用插件解决延迟队列问题
@GetMapping("/sendDelayMsg/{message}/{ttl}")
public String sendMsg(@PathVariable String message,@PathVariable Integer ttl){
log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(), message,ttl);
//将过期时间由生产端发送到队列
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setDelay(ttl);//设置延迟时间为50秒
return message;
}
};
rabbitTemplate.convertAndSend(DelayedMessageExchange_Config.DELAYED_EXCHANGE_NAME,"delayed.routingKey",message,messagePostProcessor);
return message+"***"+ttl;
}
消费者
/**
* Created by KingsLanding on 2022/9/21 22:47
*/
@Slf4j
@Component
public class NormalDelayExchangeConsumer {
@RabbitListener(queues = DelayedMessageExchange_Config.DELAYED_QUEUE_NAME)
public void receiveD(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
}
}
延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用 RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。
当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis 的 zset,利用 Quartz 或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景
发布确认高级
在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败, 导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢? 特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢:
配置文件
在配置文件当中需要添加 spring.rabbitmq.publisher-confirm-type=correlated
NONE
禁用发布确认模式,是默认值
CORRELATED
发布消息成功到交换器后会触发回调方法
SIMPLE
经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法, 其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法 等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是 waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker
#配置RabbitMQ基本信息
spring:
rabbitmq:
host: 192.168.222.128
port: 5672
username: admin
password: 123123
#correlated:发布消息成功到交换器后会触发回调方法
#none:禁用发布确认模式,是默认值
#simple:经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法,
#其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法
#等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是
#waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker
publisher-confirm-type: correlated
ConfirmExchange_Config
//交换机名称
public static final String CONFIRM_EXCHANGE_NAME="confirm.exchange";
//队列名称
public static final String CONFIRM_QUEUE_NAME="confirm.queue";
//confirm交换机声明
@Bean
public DirectExchange confirmExchange(){
//声明确认交换机CONFIRM_EXCHANGE_NAME的备份交换机BACKUP_EXCHANGE_NAME
ExchangeBuilder exchangeBuilder = ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
.durable(true)//持久化
.withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);
return (DirectExchange)exchangeBuilder.build();
}
//confirm队列声明
@Bean
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
//绑定
@Bean
public Binding confirmQueueBindingConfirmExchange(@Qualifier("confirmQueue")Queue confirmQueue,
@Qualifier("confirmExchange") DirectExchange confirmExchange){
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with("key1");
}
回调接口
MyCallBack
/**
* Created by KingsLanding on 2022/9/22 17:54
*/
@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {
/*
交换机确认回调方法
1.交换机接收消息成功 回调消息
1、correlationData 保存回调消息的ID及其相关信息
2、ack=true 交换机是否收到消息
3、cause null
2.交换机接收消息失败 回调消息
1、correlationData 保存回调消息的ID及其相关信息
2、ack=false 交换机没有收到消息
3、cause 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String id = correlationData!=null ? correlationData.getId() : "";
if (ack){
log.info("交换机已经收到 id 为:{}的消息",id);
}else{
log.info("交换机还未收到 id 为:{}的消息,由于原因:{}",id,cause);
}
}
//消息无法路由,也就是交换机接收成功,但是路由到队列时出问题
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.error(" 消 息 {}, 被交换机 {} 退回,退回原因 :{}, 路 由 key:{}",new
String(message.getBody()),exchange,replyText,routingKey);
}
}
依赖注入 rabbitTemplate 之后再设置它的回调对象
@Autowired
MyCallBack myCallBack;
@Autowired
RabbitTemplate rabbitTemplate;
//依赖注入 rabbitTemplate 之后再设置它的回调对象
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(myCallBack);
/**
* true:
* 交换机无法将消息进行路由时,会将该消息返回给生产者
* false:
* 如果发现消息无法进行路由,则直接丢弃
*/
rabbitTemplate.setMandatory(true);//必须设置是否丢弃,否则无法回调
//设置回退消息交给谁处理
rabbitTemplate.setReturnCallback(myCallBack);
}
生产者
/**
* Created by KingsLanding on 2022/9/22 18:06
*/
@Slf4j
@RestController
public class ConfirmProducerController {
@Autowired
MyCallBack myCallBack;
@Autowired
RabbitTemplate rabbitTemplate;
//依赖注入 rabbitTemplate 之后再设置它的回调对象
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(myCallBack);
/**
* true:
* 交换机无法将消息进行路由时,会将该消息返回给生产者
* false:
* 如果发现消息无法进行路由,则直接丢弃
*/
rabbitTemplate.setMandatory(true);//必须设置是否丢弃,否则无法回调
//设置回退消息交给谁处理
rabbitTemplate.setReturnCallback(myCallBack);
}
//发布确认高级
@GetMapping("/ConfirmSendMsg/{message}")
public String ConfirmSendMsg(@PathVariable String message){
//指定消息 id 为 1
CorrelationData correlationData1=new CorrelationData("1");
CorrelationData correlationData2=new CorrelationData("2");
/*
交换机没收到信息
交换机还未收到 id 为:1的消息,由于原因:channel error; protocol method:
#method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'confirm.exchange123'
in vhost '/', class-id=60, method-id=40)
*/
rabbitTemplate.convertAndSend(ConfirmExchange_Config.CONFIRM_EXCHANGE_NAME+"123",
"key1",message,correlationData1);
/*
交换机收到消息,但队列错误的情况
消 息 张明均, 被交换机 confirm.exchange 退回,退回原因 :NO_ROUTE, 路 由 key:key2
交换机已经收到 id 为:2的消息 //路由失败转交备份交换机处理
报警:消息张明均路由不成功 //备份交换机转交给报警队列,消息被报警消费者消费
*/
rabbitTemplate.convertAndSend(ConfirmExchange_Config.CONFIRM_EXCHANGE_NAME,
"key2",message,correlationData2);
log.info("发送了一个消息:{}",message);
return message;
}
}
消费者
/**
* Created by KingsLanding on 2022/9/22 16:21
*/
@Component
@Slf4j
public class ConfirmConsumer {
@RabbitListener(queues=ConfirmExchange_Config.CONFIRM_QUEUE_NAME)
@RabbitListener(queues=ConfirmExchange_Config.BACKUP_QUEUE_NAME)//备份交换机与备份队列
public void confirmConsumer(Message message, Channel channel){
String strMessage = new String(message.getBody());
log.info("接受到消息{}",strMessage);
}
}
备份交换机
有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理
但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然 后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者 所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置 mandatory 参数会增 加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。
如果既不想丢失消息,又不想增加生产者的 复杂性,该怎么做呢?
可以为队列设置死信交换机来存储那些 处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息
#### 备份交换机的机制
备份 交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时, 就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由 备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑 定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都 进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
修改ConfirmExchange_Config配置类
/**
* Created by KingsLanding on 2022/9/22 16:04
*
* 发布确认高级:消息发送时Rabbit服务器宕机
* 备份交换机
*/
@Configuration
public class ConfirmExchange_Config {
//交换机名称
public static final String CONFIRM_EXCHANGE_NAME="confirm.exchange";
//队列名称
public static final String CONFIRM_QUEUE_NAME="confirm.queue";
//备份交换机
public static final String BACKUP_EXCHANGE_NAME="backup.exchange";
//备份队列
public static final String BACKUP_QUEUE_NAME="backup.queue";
//警告队列
public static final String WARNING_QUEUE_NAME="warning.queue";
//confirm交换机声明
@Bean
public DirectExchange confirmExchange(){
//声明确认本确认交换机CONFIRM_EXCHANGE_NAME的备份交换机BACKUP_EXCHANGE_NAME
ExchangeBuilder exchangeBuilder = ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
.durable(true)//持久化
.withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);
return (DirectExchange)exchangeBuilder.build();
}
//confirm队列声明
@Bean
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
//绑定
@Bean
public Binding confirmQueueBindingConfirmExchange(@Qualifier("confirmQueue")Queue confirmQueue,
@Qualifier("confirmExchange") DirectExchange confirmExchange){
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with("key1");
}
//声明备份交换机
@Bean
public FanoutExchange backupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
//声明备份队列
@Bean
public Queue backupQueue(){
return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
}
//绑定备份交换机和备份队列
@Bean
public Binding backupQueueBindingBackupExchange(@Qualifier("backupExchange")FanoutExchange backupExchange,
@Qualifier("backupQueue")Queue backupQueue){
return BindingBuilder.bind(backupQueue).to(backupExchange);
}
//声明警告队列
@Bean
public Queue warningQueue(){
return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
}
//备份交换机和警告队列的绑定
@Bean
public Binding warningQueueBindingBackupExchange(@Qualifier("backupExchange")FanoutExchange backupExchange,
@Qualifier("warningQueue")Queue warningQueue){
return BindingBuilder.bind(warningQueue).to(backupExchange);
}
}
消费者
/**
* Created by KingsLanding on 2022/9/22 16:21
*/
@Component
@Slf4j
public class ConfirmConsumer {
@RabbitListener(queues=ConfirmExchange_Config.CONFIRM_QUEUE_NAME)
@RabbitListener(queues=ConfirmExchange_Config.BACKUP_QUEUE_NAME)//备份交换机与备份队列
public void confirmConsumer(Message message, Channel channel){
String strMessage = new String(message.getBody());
log.info("接受到消息{}",strMessage);
}
}
警告队列消费
/**
* Created by KingsLanding on 2022/9/22 21:41
*/
@Slf4j
@Component
public class WarningConsumer {
@RabbitListener(queues = ConfirmExchange_Config.WARNING_QUEUE_NAME)
public void warningConsumer(Message message, Channel channel){
String strMessage = new String(message.getBody());
log.error("报警:消息{}路由不成功",strMessage);
}
}
mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先 级高,经过上面结果显示答案是备份交换机优先级高。
优先级队列
使用场景
在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如 果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,tmall 商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创 造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用 redis 来存 放的定时轮询,大家都知道 redis 只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景,所以订单量大了后采用 RabbitMQ 进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级, 否则就是默认优先级。
- 要让队列实现优先级需要做的事情有如下事情:队列需要设置为优先级队列,消息需要设置消息的优先 级,消费者需要等待消息已经发送到队列中才去消费因为,这样才有机会对消息进行排序
- 在控制台创建优先级队列
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
生产者
public class Producer {
private static final String QUEUE_NAME="hello";
public static void main(String[] args) throws Exception {
try (Channel channel = RabbitMqUtils.getChannel();) {
//给消息赋予一个 priority 属性
AMQP.BasicProperties properties = new
AMQP.BasicProperties().builder().priority(5).build();
for (int i = 1; i <11; i++) {
String message = "info"+i;
if(i==5){
//设置优先级最高
channel.basicPublish("", QUEUE_NAME, properties, message.getBytes());
}else{
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
}
System.out.println("发送消息完成:" + message);
}
}
}
}
消费者
public class Consumer {
private static final String QUEUE_NAME="hello";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
//设置队列的最大优先级 最大可以设置到 255 官网推荐 1-10 如果设置太高比较吃内存和 CPU
Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);
channel.queueDeclare(QUEUE_NAME, true, false, false, params);
System.out.println("消费者启动等待消费......");
DeliverCallback deliverCallback=(consumerTag, delivery)->{
String receivedMessage = new String(delivery.getBody());
System.out.println("接收到消息:"+receivedMessage);
};
channel.basicConsume(QUEUE_NAME,true,deliverCallback,(consumerTag)->{
System.out.println("消费者无法消费消息时调用,如队列被删除");
});
}
}