目录
一、安装RabbitMQ
1.yum安装
Ⅰ.安装或更新yum插件
Ⅱ.安装 Erlang
Ⅲ、创建yum的RabbitMQ官方存储库
Ⅳ、安装RabbitMQ
Ⅴ、启动RabbitMQ
Ⅵ 、登录RabbitMQ的后台
Ⅶ、让guest可以远程登录
二、java代码实战RabbitMQ的五种消息模型
前置准备
Ⅰ.新建host
Ⅱ.新建队列
Ⅲ、新建项目
1.简单模式(Simple Mode) / 一对一模式
2.工作队列模式(Work Queue Mode)
Ⅰ、公平的工厂模式
Ⅱ、按能分配
3.发布/订阅模式(Publish/Subscribe Mode)
4.路由模式(Routing Mode)
5.主题模式(Topic Mode)
三、补充内容
1.可能因为版本原因不能够旧方法设置头部取头部
Ⅰ、背景描述
Ⅱ、方法的代码
sudo yum update
我这个是安装过的样子
RabbitMQ基于Erlang语法,高效但是需要Erlang支持
我们之后要下载的是rabbitMQ 3.10 需要
首先安装存储库
sudo yum install epel-release
中间会询问是否安装,输入y就行了
创建yum的erlang数据库(直接下载版本太低了)
cd /etc/yum.repos.d
sudo touch /etc/yum.repos.d/erlang.repo
vi erlang.repo
编写文件为
[erlang-solutions]
name=CentOS $releasever - $basearch - Erlang Solutions
baseurl=https://packages.erlang-solutions.com/rpm/centos/$releasever/$basearch
gpgcheck=1
gpgkey=https://packages.erlang-solutions.com/rpm/erlang_solutions.asc
enabled=1
如果之前安过低版本要先删除
后面改成你的版本
sudo yum remove erlang-erts-R16B-03.18.el7.x86_64
然后就可以安装
sudo yum install erlang
然后输入y就行了
添加 RabbitMQ 的官方存储库密钥到系统
sudo rpm --import https://github.com/rabbitmq/signing-keys/releases/download/2.0/rabbitmq-release-signing-key.asc
添加 RabbitMQ 的官方存储库到 yum:
sudo touch /etc/yum.repos.d/rabbitmq.repo
然后修改这个文件
cd /etc/yum.repos.d
vi rabbitmq.repo
[rabbitmq]
name=rabbitmq
baseurl=https://packagecloud.io/rabbitmq/rabbitmq-server/el/7/$basearch
enabled=1
gpgcheck=0
repo_gpgcheck=0
查看可用版本
sudo yum --disablerepo="*" --enablerepo="rabbitmq" list available
可安装版本是3.10.0-1 官网最新版本是3.12.2 如果想要安装最新版本可以先使用其他方法
我们选择安装
sudo yum install rabbitmq-server
如果提示版本不足,请记住版本号之后,回到上面安装Erlang的步骤删除旧版本,重新安装erlang到正确版本
这样就安装好了
安装之后的路径在
/usr/lib/rabbitmq
和其他的软件一样,如果需要外部服务器访问,需要更改防火墙
检查防火墙状态
sudo systemctl status firewalld
如果防火墙未启用(启用了可以跳过,虚拟机也可以不开防火墙,但养成习惯服务器不会忘记)
便输入下面指令开启防火墙
sudo systemctl start firewalld
开启5672和15672 (后者是控制台)
firewall-cmd --add-port=5672/tcp --permanent
firewall-cmd --add-port=15672/tcp --permanent
提示success就对了(我这个是重复添加)
重新加载防火墙
firewall-cmd --reload
验证端口是否打开(这样出现了连接不上可以排除这里)
firewall-cmd --query-port=15672/tcp
firewall-cmd --query-port=5672/tcp
出现yes就是对的了
启动管理工具
rabbitmq-plugins enable rabbitmq_management
如果报下面的错就是15672被占用,可能是重复开启(如果提前改了配置文件再重启服务也能达到上面那个指令效果)
我们可以查看下端口占用
sudo ss -tlnp | grep 15672
选一个允许 如果不是centos 7 还有其他的指令
sudo netstat -tlnp | grep 15672
占用端口15672的进程是beam.smp
,它是RabbitMQ服务器的核心进程。这意味着RabbitMQ管理插件已经在监听15672端口,并且正常工作。
如果不是beam.smp我们可以杀死之后,再启动管理工具
sudo kill -- 实例代码 换成想杀死的队列
sudo kill 15672
我们新建一个用户,并赋予管理员权限,来远程访问我们控制台
rabbitmqctl add_user admin admin
前面是用户名,后面是密码 密码还是改复杂点
新建了一个简单用户怕不安全,可以用下面指令改密码
sudo rabbitmqctl change_password admin <新密码>
赋予管理员权限
rabbitmqctl set_user_tags admin administrator
然后我们可以去主机访问下虚拟机或者服务器(这次没有用虚拟机,没有安装图形化和浏览器就只演示主机连接虚拟机,平时在本地访问对应端口是确保真的启动了软件)
你的虚拟机ip或者服务器公网ip:15672
然后输入刚才新建的用户就登录上去了
guest拥有最高的权限但是不能在非本机登录(比如服务器就得登服务器,我用主机连接服务器的rabbit并登录guest,就会被拒绝)
可是需要guest的权限来做事情,服务器又没有图形化,直接敲命令又不熟练差点把人急死
我们需要改配置
/etc/rabbitmq
如果有rabbitmq.config就修改,没有就新建
touch rabbitmq.config
vi rabbitmq.config
确保里面有
[{rabbit, [{loopback_users, []}]}].
这个时候重启就行了
sudo systemctl restart rabbitmq-server
重启访问
http://rabbitmq的主机地址:15672/
就能用guest登录了
一些需要高权限的事情,就用guest做,平时还是用admin
首先在后台新建host并创建响应的队列
可以只输入一个名字
然后 Add virtual host
然后我们还要创建队列
点击add a newqueue之后
这里就有我们刚新建的host了
我们看见了队列的三种类型,分别是下面三种,我们只要经典队列就行了
经典队列(Classic Queue): 经典队列(也称为普通队列)是 RabbitMQ 最常见和默认的队列类型。它支持多个消费者并且提供了传统的消息传递模式。
Quorum 队列(Quorum Queue): Quorum 队列是 RabbitMQ 3.8 版本引入的一种新的队列类型。它基于 Raft 一致性协议,提供更高的可靠性和良好的扩展性。Quorum 队列适用于高吞吐量和高可靠性的应用场景。
流队列(Stream Queue): 流队列是 RabbitMQ 3.9 版本中引入的新队列类型。它是为了解决大规模事件流处理而设计的。流队列支持发布/订阅模式,具备持久化和分区等特性,能够处理大量的事件流数据。
这里我们就建好了
新建一个maven项目(基于IDEA 2023.2 演示 如果版本相同,缺很多东西一般是教育免费版)
在pom.xml中添加依赖
com.rabbitmq
amqp-client
3.4.1
再编写一下工具来进行RabbitMQ的连接
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
public class MQUtils {
public static final String QUEUE_NAME = "myqueue01"; //队列1 我们刚才新建了
public static final String QUEUE_NAME2 = "myqueue02"; //队列2 我们没有新建 (也可以在使用的时候直接写名字)
public static final String EXCHANGE_NAME = "myexchange01"; //路由名字 后面要定义
public static final String EXCHANGE_NAME2 = "myexchange02";
public static final String EXCHANGE_NAME3 = "myexchange03";
/**
* 获得MQ的连接
* @return
* @throws IOException
*/
public static Connection getConnection() throws IOException {
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost"); // 服务器的主机名
connectionFactory.setPort(5672); // 服务器的主机名
connectionFactory.setVirtualHost("myhost"); //我们虚拟出来的主机名(刚才新建的主机名)
connectionFactory.setUsername("admin"); //我们新建的用户
connectionFactory.setPassword("123456"); //以及我们新建用户的密码
return connectionFactory.newConnection(); // 返回Connection用于后续操作
}
}
首先来熟悉一下简单模式的流程
发布者(Producer) --> 队列(Queue) --> 消费者(Consumer)
①.生产者将消息直接发送到一个队列。
②.消费者从队列中接收消息并进行处理。
明确了需要的东西之后我们开始建包(不理解生产者消费者的,可以去我的主页的堵塞队列里面看一下,讲的 比较俗 易懂)
首先是生产者
import com.cece.utils.MQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
import com.cece.utils.MQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
/*
* 简单模式的生成者 将消息直接发送到一个队列
*/
public class SimpleProducer {
public static void main(String[] args) throws IOException {
// 获取与 RabbitMQ 的连接
Connection connection = MQUtils.getConnection();
// 创建通道
Channel channel = connection.createChannel();
// 定义队列
// 参数说明:
// - MQUtils.QUEUE_NAME: 队列名称
// - false: 指定队列是否持久化,false 表示不持久化
// - false: 指定当所有消费者客户端连接断开时,是否删除队列。false 表示不删除
// - false: 指定是否独占队列。false 表示队列可以被多个消费者监听
// - null: 其他属性,比如队列的消息过期时间等。这里为 null
channel.queueDeclare(MQUtils.QUEUE_NAME, false, false, false, null);
// 要发送的消息内容
String msg = "Hello World!";
// 发布消息到队列
// 参数说明:
// - "": 交换机名称。空字符串表示直接发送到队列,不经过交换机
// - MQUtils.QUEUE_NAME: 队列名称
// - null: 其他属性,比如消息的路由键等。这里为 null
// - msg.getBytes(): 要发送的消息的字节数组形式
channel.basicPublish("", MQUtils.QUEUE_NAME, null, msg.getBytes());
// 关闭通道
channel.close();
// 关闭连接
connection.close();
}
}
然后是消费者
import com.cece.utils.MQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import java.io.IOException;
/*
* 简单模式的消费者 从队列中接收消息并进行处理
*/
public class SimpleConsumer {
public static void main(String[] args) throws IOException, InterruptedException {
Connection connection = MQUtils.getConnection();
Channel channel = connection.createChannel();
//定义队列
channel.queueDeclare(MQUtils.QUEUE_NAME,false,false,false,null);
//创建消费者
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
//消费者消费通道中的消息
channel.basicConsume(MQUtils.QUEUE_NAME,true,queueingConsumer);
//读取消息
while(true){
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
System.out.println(new String(delivery.getBody()));
}
}
}
第二个参数是否持久化是我们之前就设置好的
已经存在的队列不可更改 durable,直接更改会报错
如果写的和我们配置的不一样就会报错
Exception in thread "main" java.io.IOException
at com.rabbitmq.client.impl.AMQChannel.wrap(AMQChannel.java:106)
at com.rabbitmq.client.impl.AMQChannel.wrap(AMQChannel.java:102)
at com.rabbitmq.client.impl.AMQChannel.exnWrappingRpc(AMQChannel.java:124)
at com.rabbitmq.client.impl.ChannelN.queueDeclare(ChannelN.java:833)
at com.rabbitmq.client.impl.ChannelN.queueDeclare(ChannelN.java:61)
at com.cece.simplemode.SimpleProducer.main(SimpleProducer.java:28)
Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'durable' for queue 'myqueue01' in vhost 'myhost': received 'false' but current is 'true', class-id=50, method-id=10)
at com.rabbitmq.utility.ValueOrException.getValue(ValueOrException.java:67)
at com.rabbitmq.utility.BlockingValueOrException.uninterruptibleGetValue(BlockingValueOrException.java:33)
at com.rabbitmq.client.impl.AMQChannel$BlockingRpcContinuation.getReply(AMQChannel.java:343)
at com.rabbitmq.client.impl.AMQChannel.privateRpc(AMQChannel.java:216)
at com.rabbitmq.client.impl.AMQChannel.exnWrappingRpc(AMQChannel.java:118)
... 3 more
Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'durable' for queue 'myqueue01' in vhost 'myhost': received 'false' but current is 'true', class-id=50, method-id=10)
at com.rabbitmq.client.impl.ChannelN.asyncShutdown(ChannelN.java:478)
at com.rabbitmq.client.impl.ChannelN.processAsync(ChannelN.java:315)
at com.rabbitmq.client.impl.AMQChannel.handleCompleteInboundCommand(AMQChannel.java:144)
at com.rabbitmq.client.impl.AMQChannel.handleFrame(AMQChannel.java:91)
at com.rabbitmq.client.impl.AMQConnection$MainLoop.run(AMQConnection.java:552)
at java.lang.Thread.run(Thread.java:750)
多次运行生产者,消费者就会得到多次消息
而且改变消费者方法,也会接收到不同的消息
这就是简单模式 他们用队列一对一来传递东西
我们发现生产者传递一次,只有一个消费者能够接收到消息,效率很低下
所以我们使用工作队列模式:
生产者将消息发送到一个队列。
多个消费者同时监听同一个队列,竞争接收消息。
每个消息只会被一个消费者接收和处理。
我们新建包workqueuemode,把刚才的生产者消费者类都复制过去,消费者复制两个,并且重命名
首先是生产类
import com.cece.utils.MQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import java.io.IOException;
/*
* 模式的生产者 经过一个队列 发生多条消息 给消费者
* 是分发,两个人得到的是总量
*/
public class WorkProductor {
public static void main(String[] args) throws IOException, InterruptedException {
// 获取与MQ服务器的连接
Connection connection = MQUtils.getConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 声明队列
// 参数说明:
// - queue: 队列名称
// - durable: 队列是否持久化,false表示非持久化,消息不会保存到磁盘
// - exclusive: 队列是否为专用队列,true表示只能被当前连接使用,连接关闭后队列会被删除
// - autoDelete: 队列是否自动删除,true表示当没有消费者订阅该队列时自动删除队列
// - arguments: 额外的参数,例如消息存活时间、最大长度等
channel.queueDeclare(MQUtils.QUEUE_NAME, true, false, false, null);
// 循环发送消息到队列
for (int i = 0; i < 100; i++) {
String msg = "Hello-->" + i;
// 发布消息到队列
// 参数说明:
// - exchange: 交换机名称,空字符串表示默认的直连交换机
// - routingKey: 路由键,用于将消息路由到指定的队列
// - props: 消息的属性,例如消息持久化标记、优先级等
// - body: 消息体,即要发送的消息内容
channel.basicPublish("", MQUtils.QUEUE_NAME, null, msg.getBytes());
System.out.println("发生了:" + msg);
Thread.sleep(10); // 睡眠一会儿
}
// 关闭通道和连接
channel.close();
connection.close();
}
}
然后是消费类1
import com.cece.utils.MQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import java.io.IOException;
/*
* 工厂模式的消费者 从队列中接收消息并进行处理 休眠时间很短
*/
public class WorkConsumer01 {
public static void main(String[] args) throws IOException, InterruptedException {
// 获取与MQ服务器的连接
Connection connection = MQUtils.getConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 声明队列
// 参数说明:
// - queue: 队列名称
// - durable: 队列是否持久化,false表示非持久化,消息不会保存到磁盘
// - exclusive: 队列是否为专用队列,true表示只能被当前连接使用,连接关闭后队列会被删除
// - autoDelete: 队列是否自动删除,true表示当没有消费者订阅该队列时自动删除队列
// - arguments: 额外的参数,例如消息存活时间、最大长度等
channel.queueDeclare(MQUtils.QUEUE_NAME, true, false, false, null);
// 创建一个 QueueingConsumer 对象,用于消费通道中的消息
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
// 消费者订阅队列中的消息
// 参数说明:
// - queue: 要订阅的队列名称
// - autoAck: 是否自动确认消息消费完成,true表示自动确认,消息一旦被接收就会从队列中删除
// - consumer: 消息的消费者
channel.basicConsume(MQUtils.QUEUE_NAME, true, queueingConsumer);
// 循环接收并处理消息
while (true) {
// 等待下一条消息的到达
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
// 获取消息内容并进行处理
String message = new String(delivery.getBody());
System.out.println("工厂消费者1接收到: " + message);
// 模拟处理消息的耗时操作
Thread.sleep(10);
}
}
}
消费类2
import com.cece.utils.MQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import java.io.IOException;
/*
* 工厂模式的消费者 从队列中接收消息并进行处理 休眠时间很长
*/
public class WorkConsumer02 {
public static void main(String[] args) throws IOException, InterruptedException {
// 获取与MQ服务器的连接
Connection connection = MQUtils.getConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 声明队列
// 参数说明:
// - queue: 队列名称
// - durable: 队列是否持久化,false表示非持久化,消息不会保存到磁盘
// - exclusive: 队列是否为专用队列,true表示只能被当前连接使用,连接关闭后队列会被删除
// - autoDelete: 队列是否自动删除,true表示当没有消费者订阅该队列时自动删除队列
// - arguments: 额外的参数,例如消息存活时间、最大长度等
channel.queueDeclare(MQUtils.QUEUE_NAME, true, false, false, null);
// 创建一个 QueueingConsumer 对象,用于消费通道中的消息
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
// 消费者订阅队列中的消息
// 参数说明:
// - queue: 要订阅的队列名称
// - autoAck: 是否自动确认消息消费完成,true表示自动确认,消息一旦被接收就会从队列中删除
// - consumer: 消息的消费者
channel.basicConsume(MQUtils.QUEUE_NAME, true, queueingConsumer);
// 循环接收并处理消息
while (true) {
// 等待下一条消息的到达
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
// 获取消息内容并进行处理
String message = new String(delivery.getBody());
System.out.println("工厂消费者2接收到: " + message);
// 模拟处理消息的耗时操作
Thread.sleep(1000);
}
}
}
先启动两个消费者
我们发现消费者1收到的是0,2,4这样的,消费者2是1,3,5这样
说明他们是一人获得一个,这个人获得了,下个人再获得一个
可是消费者1很快就运行完了,消费2却还要很久
我们可以这样理解,现在有两个小朋友,一个阿姨给他们分糖,分的很快,左边甩一个,右边甩一个,小朋友甲吃的也很快,恨不得甩一个吃一个,小朋友乙却是吃的很慢。发完之后,甲和阿姨就目瞪口呆看着它吃。
这是因为两个消费者都订阅了同一个队列,并且队列默认采用是自动确认机制,消息发过去后就自动确认,队列不清楚每个消息具体什么时间处理完,所以平均分配消息数量。
好像分吃的时候很公平,可是平时处理业务的时候,我们应该让做的快的消费者,多接收些消息来消费。
所以我们要修改原来的工厂代码
新建我们新工厂类软件包
直接复制之前工厂的软件包
①channel.basicQos(1);限制队列一次发一个消息给消费者,等消费者有了反馈,再发下一条
②channel.basicAck 消费完消息后手动反馈,处理快的消费者就能处理更多消息
③basicConsume 中的参数改为false
消费者1和2都如下增加
package com.cece.workqueuemode2;
import com.cece.utils.MQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import java.io.IOException;
/*
* 工厂模式的消费者 从队列中接收消息并进行处理 休眠时间很短
*/
public class WorkConsumer01 {
public static void main(String[] args) throws IOException, InterruptedException {
// 获取与MQ服务器的连接
Connection connection = MQUtils.getConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 声明队列
// 参数说明:
// - queue: 队列名称
// - durable: 队列是否持久化,false表示非持久化,消息不会保存到磁盘
// - exclusive: 队列是否为专用队列,true表示只能被当前连接使用,连接关闭后队列会被删除
// - autoDelete: 队列是否自动删除,true表示当没有消费者订阅该队列时自动删除队列
// - arguments: 额外的参数,例如消息存活时间、最大长度等
channel.queueDeclare(MQUtils.QUEUE_NAME, true, false, false, null);
//同一时刻服务器只发送一条消息给消费者
channel.basicQos(1);
// 创建一个 QueueingConsumer 对象,用于消费通道中的消息
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
// 消费者订阅队列中的消息
// 参数说明:
// - queue: 要订阅的队列名称
// - autoAck: 是否自动确认消息消费完成,true表示自动确认,消息一旦被接收就会从队列中删除
// - consumer: 消息的消费者
channel.basicConsume(MQUtils.QUEUE_NAME, false, queueingConsumer);
// 循环接收并处理消息
while (true) {
// 等待下一条消息的到达
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
// 获取消息内容并进行处理
String message = new String(delivery.getBody());
System.out.println("工厂消费者1接收到: " + message);
// 模拟处理消息的耗时操作
Thread.sleep(10);
//手动确定返回状态,不写就是自动确认
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
}
}
}
package com.cece.workqueuemode2;
import com.cece.utils.MQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import java.io.IOException;
/*
* 工厂模式的消费者 从队列中接收消息并进行处理 休眠时间很长
*/
public class WorkConsumer02 {
public static void main(String[] args) throws IOException, InterruptedException {
// 获取与MQ服务器的连接
Connection connection = MQUtils.getConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 声明队列
// 参数说明:
// - queue: 队列名称
// - durable: 队列是否持久化,false表示非持久化,消息不会保存到磁盘
// - exclusive: 队列是否为专用队列,true表示只能被当前连接使用,连接关闭后队列会被删除
// - autoDelete: 队列是否自动删除,true表示当没有消费者订阅该队列时自动删除队列
// - arguments: 额外的参数,例如消息存活时间、最大长度等
channel.queueDeclare(MQUtils.QUEUE_NAME, true, false, false, null);
//同一时刻服务器只发送一条消息给消费者
channel.basicQos(1);
// 创建一个 QueueingConsumer 对象,用于消费通道中的消息
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
// 消费者订阅队列中的消息
// 参数说明:
// - queue: 要订阅的队列名称
// - autoAck: 是否自动确认消息消费完成,true表示自动确认,消息一旦被接收就会从队列中删除
// - consumer: 消息的消费者
channel.basicConsume(MQUtils.QUEUE_NAME, false, queueingConsumer);
// 循环接收并处理消息
while (true) {
// 等待下一条消息的到达
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
// 获取消息内容并进行处理
String message = new String(delivery.getBody());
System.out.println("工厂消费者2接收到: " + message);
// 模拟处理消息的耗时操作
Thread.sleep(1000);
//手动确定返回状态,不写就是自动确认
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
}
}
}
消费者1(休息时间短的)几乎接收了所有的消息
消费者2(休息时间短的)就寥寥几条消息
我们就实现了按能分配
发布/订阅模式也被称为广播模式(Broadcast Mode)。
在发布/订阅模式中,消息会被广播到所有订阅了相应交换机的队列。
生产者发送消息到交换机,而不直接发送到队列。
多个消费者可以创建各自的队列并绑定到交换机上,从而接收消息。
这种模式可用于实现广播通知、日志分发等场景。
一个生产者将消息发布到交换机。
多个队列通过绑定到该交换机来接收消息。
每个队列都有自己的消费者。
发布/订阅 与工厂类的区别就是
工厂类是共用一个队列 多个消费者争夺有限的消息
,而 发布/订阅 是消费者一人一个队列,可以接收到相同的消息
综上所述我们还要新建一个队列,以及一个交换机
新建队列(是否持久化看自己选择,使用的时候不能搞错)
订阅是扇形交换机,我们type选择Fanout
直连交换机(Direct Exchange): 直连交换机将消息通过路由键与绑定键进行精确匹配,并将消息发送到匹配的队列中。该类型的交换机通常用于点对点的消息传递。
主题交换机(Topic Exchange): 主题交换机通过使用通配符匹配路由键与绑定键,将消息发送到符合匹配规则的队列中。通配符可以使用 "*" 表示单个词,"#" 表示零个或多个词。该类型的交换机广泛应用于灵活的发布/订阅模式。
扇形交换机(Fanout Exchange): 扇形交换机将消息广播给所有绑定到它的队列,忽略路由键。该类型的交换机适用于一对多的消息广播场景,消息发送给所有订阅者。
头交换机(Headers Exchange): 头交换机根据消息的头部信息(headers)进行匹配,并将消息发送到符合匹配规则的队列中。相比于其他交换机类型,头交换机的匹配是更复杂的。
这样就新建好了
我们接下来要把两个队列绑在交换机上
虽然队列和交换机都是一个host衍生出来的,但是他们又不会互相认识。
Ⅰ、直接控制台绑定
我们点击刚才建好的交换机
点开bindings属性就可以绑定,队列
(这里绑定了,代码就不要写绑定代码了)
Ⅱ、Java代码去绑定
首先新建包subscribemode,复制工厂工厂模式的三个类,并重命名
首先是生产者的代码
我们和之前的不同点就是 没有和队列绑定,绑定的是交换机
import com.cece.utils.MQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
/*
* 发布和订阅模式的生产者,消息会通过交换机发到队列
*/
public class PublishProductor {
public static void main(String[] args) throws IOException, InterruptedException {
// 获取与MQ服务器的连接
Connection connection = MQUtils.getConnection();
// 创建一个通道,用于进行消息的发布和接收等操作。
Channel channel = connection.createChannel();
//声明一个交换机, fanout表示使用发布/订阅模式(广播模式)。这是我们刚才定义的类型
// 交换机名字
// type
// 是否持久化 如果不是持久化,可以不要这个参数
channel.exchangeDeclare(MQUtils.EXCHANGE_NAME,"fanout",true);
//设置要发送的消息内容。
String msg = "Hello Fanout";
// 发布消息到队列
// 参数说明:
// - exchange: 交换机名称,空字符串表示默认的直连交换机
// - routingKey: 路由键,根据实际需求将消息路由到指定的队列
// ,这里为空字符串表示消息将被广播给所有绑定到交换机的队列。
// - props: 消息的属性,例如消息持久化标记、优先级等消息的属性,这里设置为null表示使用默认属性。
// - body: 消息体,即要发送的消息内容
channel.basicPublish(MQUtils.EXCHANGE_NAME,"",null,msg.getBytes());
System.out.println("send:" + msg);
// 关闭通道和MQ连接
channel.close();
connection.close();
}
}
消费者1
import com.cece.utils.MQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import java.io.IOException;
/*
* 发布/订阅模式 的消费者1
* 两个消费者绑定的消息队列不同
* 通过交换机一个消息能被 不同队列的 两个消费者同时获取
* 一个队列可以有多个消费者,队列中的消息 只能被一个 消费者获取
*/
public class SubscribeConsumer1 {
public static void main(String[] args) throws IOException, InterruptedException {
// 获取与MQ服务器的连接
Connection connection = MQUtils.getConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 声明队列
// 参数说明:
// - queue: 队列名称
// - durable: 队列是否持久化,false表示非持久化,消息不会保存到磁盘
// - exclusive: 队列是否为专用队列,true表示只能被当前连接使用,连接关闭后队列会被删除
// - autoDelete: 队列是否自动删除,true表示当没有消费者订阅该队列时自动删除队列
// - arguments: 额外的参数,例如消息存活时间、最大长度等
//绑定队列1
channel.queueDeclare(MQUtils.QUEUE_NAME, true, false, false, null);
//绑定队列1到交换机
channel.queueBind(MQUtils.QUEUE_NAME,MQUtils.EXCHANGE_NAME,"");
// 创建一个 QueueingConsumer 对象,用于消费通道中的消息
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
// 消费者订阅队列中的消息
// 参数说明:
// - queue: 要订阅的队列名称
// - autoAck: 是否自动确认消息消费完成,true表示自动确认,消息一旦被接收就会从队列中删除
// - consumer: 消息的消费者
//订阅队列1
channel.basicConsume(MQUtils.QUEUE_NAME, true, queueingConsumer);
// 循环接收并处理消息
while (true) {
// 等待下一条消息的到达
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
// 获取消息内容并进行处理
String message = new String(delivery.getBody());
//输出信息
System.out.println("工厂消费者1接收到: " + message);
}
}
}
消费者2
import com.cece.utils.MQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import java.io.IOException;
/*
* 发布/订阅模式 的消费者2
* 两个消费者绑定的消息队列不同
* 通过交换机一个消息能被 不同队列的 两个消费者同时获取
* 一个队列可以有多个消费者,队列中的消息 只能被一个 消费者获取
*/
public class SubscribeConsumer2 {
public static void main(String[] args) throws IOException, InterruptedException {
// 获取与MQ服务器的连接
Connection connection = MQUtils.getConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 声明队列
// 参数说明:
// - queue: 队列名称
// - durable: 队列是否持久化,false表示非持久化,消息不会保存到磁盘
// - exclusive: 队列是否为专用队列,true表示只能被当前连接使用,连接关闭后队列会被删除
// - autoDelete: 队列是否自动删除,true表示当没有消费者订阅该队列时自动删除队列
// - arguments: 额外的参数,例如消息存活时间、最大长度等
//声明队列2
channel.queueDeclare(MQUtils.QUEUE_NAME2, true, false, false, null);
//绑定队列2到交换机
channel.queueBind(MQUtils.QUEUE_NAME2,MQUtils.EXCHANGE_NAME,"");
// 创建一个 QueueingConsumer 对象,用于消费通道中的消息
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
// 消费者订阅队列中的消息
// 参数说明:
// - queue: 要订阅的队列名称
// - autoAck: 是否自动确认消息消费完成,true表示自动确认,消息一旦被接收就会从队列中删除
// - consumer: 消息的消费者
//订阅队列2
channel.basicConsume(MQUtils.QUEUE_NAME2, true, queueingConsumer);
// 循环接收并处理消息
while (true) {
// 等待下一条消息的到达
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
// 获取消息内容并进行处理
String message = new String(delivery.getBody());
System.out.println("工厂消费者2接收到: " + message);
}
}
}
我们开始测试,先启动两个消费者
我们就完成了,一个信息让多个消费者同时接收到
其实和之前那个的表面流程差不多,不过这次消费者不是用队列绑定了交换机就能取得消息。
之前那个交换机就相当于一个放补贴的地方,补贴到了之后,每个绑定了这个补贴站的都能获得一份补贴。
现在就相当于你你去取快递,你和另一个消费者都知道快递站在哪,但是快递被标记了记号,只有消费者手上的记号,和快递对的上,才能取得。总不可能你手机绑定了一个快递驿站,里面的东西你就都能随便取了吧。
路由模式使用一个直连交换机(Direct Exchange)进行消息路由。
在路由模式中,生产者发送消息到交换机,并指定一个路由键(Routing Key)。
消费者创建队列,并将队列与交换机进行绑定,同时指定一个绑定键(Binding Key)。
只有指定路由键和绑定键相匹配的消息,才会被分发到相应的队列。
这种模式可用于实现按需发布消息的场景。
综上所述,我们需要新建一个 直连的交换机
这样我们就得到了我们需要的交换机
我们可以和上面一样用代码绑定,或者控制台手动绑定,
后面为了方便,我都用代码绑定
我们需要在生产者里面 ,为队列指定键值(比发布/订阅多了的是,在声明交换机之后,要将队列绑定并且指定键值给交换机),然后更改发布消息经过的交换机
消费者需要 绑定并订阅 队列,不需要再 绑定队列到交换机(生产者做了)
生产者
import com.cece.utils.MQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
/*
路由模式的生产者,发布消息会有特定的Key,消息会被绑定特定Key的消费者获取
*/
public class RouteProductor {
public static void main(String[] args) throws IOException, InterruptedException {
// 获取与MQ服务器的连接
Connection connection = MQUtils.getConnection();
// 创建一个通道,用于进行消息的发布和接收等操作。
Channel channel = connection.createChannel();
//声明一个交换机, direct表示直连交换机,将消息通过路由键与绑定键进行精确匹配
// 交换机名字
// type
// 是否持久化 如果不是持久化,可以不要这个参数
channel.exchangeDeclare(MQUtils.EXCHANGE_NAME2,"direct",true);
//设置要发送的消息内容。
String msg = "error";
// 队列名 交换机名 "绑定的键"
//绑定队列1到交换机,指定了Key为error
channel.queueBind(MQUtils.QUEUE_NAME,MQUtils.EXCHANGE_NAME2,"error");
//绑定队列2到交换机,指定了Key为debug
channel.queueBind(MQUtils.QUEUE_NAME2,MQUtils.EXCHANGE_NAME2,"debug");
// 发布消息到队列
// 参数说明:
// - exchange: 交换机名称,空字符串表示默认的直连交换机
// - routingKey: 路由键,根据实际需求将消息路由到指定的队列
// -"":路由键,根据实际需求将消息路由到指定的队列。,这里为空字符串表示消息将被广播给所有绑定到交换机的队列。
//,这里指定为"error" 对应队列1
// - body: 消息体,即要发送的消息内容
channel.basicPublish(MQUtils.EXCHANGE_NAME2,"debug",null,msg.getBytes());
System.out.println("send:" + msg);
// 关闭通道和MQ连接
channel.close();
connection.close();
}
}
消费者1
import com.cece.utils.MQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import java.io.IOException;
/*
* 路由模式的消费者1
* 可以指定Key,消费特定的消息
*/
public class RouteConsumer01 {
public static void main(String[] args) throws IOException, InterruptedException {
// 获取与MQ服务器的连接
Connection connection = MQUtils.getConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 声明队列
// 参数说明:
// - queue: 队列名称
// - durable: 队列是否持久化,false表示非持久化,消息不会保存到磁盘
// - exclusive: 队列是否为专用队列,true表示只能被当前连接使用,连接关闭后队列会被删除
// - autoDelete: 队列是否自动删除,true表示当没有消费者订阅该队列时自动删除队列
// - arguments: 额外的参数,例如消息存活时间、最大长度等
//绑定队列1
channel.queueDeclare(MQUtils.QUEUE_NAME, true, false, false, null);
// 创建一个 QueueingConsumer 对象,用于消费通道中的消息
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
// 消费者订阅队列中的消息
// 参数说明:
// - queue: 要订阅的队列名称
// - autoAck: 是否自动确认消息消费完成,true表示自动确认,消息一旦被接收就会从队列中删除
// - consumer: 消息的消费者
//订阅队列1
channel.basicConsume(MQUtils.QUEUE_NAME, true, queueingConsumer);
// 循环接收并处理消息
while (true) {
// 等待下一条消息的到达
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
// 获取消息内容并进行处理
String message = new String(delivery.getBody());
//输出信息
System.out.println("工厂消费者1接收到: " + message);
}
}
}
消费者2
import com.cece.utils.MQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import java.io.IOException;
/*
* 路由模式的消费者2
* 可以指定Key,消费特定的消息
*/
public class RouteConsumer02 {
public static void main(String[] args) throws IOException, InterruptedException {
// 获取与MQ服务器的连接
Connection connection = MQUtils.getConnection();
// 创建一个通道
Channel channel = connection.createChannel();
// 声明队列
// 参数说明:
// - queue: 队列名称
// - durable: 队列是否持久化,false表示非持久化,消息不会保存到磁盘
// - exclusive: 队列是否为专用队列,true表示只能被当前连接使用,连接关闭后队列会被删除
// - autoDelete: 队列是否自动删除,true表示当没有消费者订阅该队列时自动删除队列
// - arguments: 额外的参数,例如消息存活时间、最大长度等
channel.queueDeclare(MQUtils.QUEUE_NAME2, true, false, false, null);
// 创建一个 QueueingConsumer 对象,用于消费通道中的消息
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
// 消费者订阅队列中的消息
// 参数说明:
// - queue: 要订阅的队列名称
// - autoAck: 是否自动确认消息消费完成,true表示自动确认,消息一旦被接收就会从队列中删除
// - consumer: 消息的消费者
channel.basicConsume(MQUtils.QUEUE_NAME2, true, queueingConsumer);
// 循环接收并处理消息
while (true) {
// 等待下一条消息的到达
QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
// 获取消息内容并进行处理
String message = new String(delivery.getBody());
System.out.println("工厂消费者2接收到: " + message);
}
}
}
发现刚好是键值为debug的队列2的消费者2得到了消息,发消息的键值改成error
就成消费者1得到了
如果发信息改成键值改成123,就没有一个得到了
这样我们就完成了路由模式,经过直连交换机,只有键值对应才能接收到消息
主题模式使用一个主题交换机(Topic Exchange)进行消息路由。
在主题模式中,生产者发送消息到交换机,并指定一个主题(Topic)。
消费者创建队列,并将队列与交换机进行绑定,同时指定一个绑定键(Binding Key)。
主题由一个或多个单词组成,单词间使用点号(.)分隔。
消息的主题与绑定键进行匹配,可以使用星号(*)和井号(#)进行通配符匹配。
这种模式可用于实现灵活的消息路由和订阅过滤。
发布者将消息发送到topic交换机,并指定一个主题,消费者可以根据通配符规则绑定队列来接收符合特定主题的消息。
其实就是订阅模式太过针对性了,实际使用中,更多的是处理某些特征的,而不是用某个队列单单处理某个
综上我们新建一个topic的交换机
我们依旧还是用代码绑定,新建包topicmode,移动之前的并重命名
生产者 我们修改声明的主机,绑定队列的主机,消息的类型
import com.cece.utils.MQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.io.IOException;
/*
主题模式的生产者,发布消息会有特定的Key,消息会被绑定区配key值规则的消费者获取
*/
public class TopicProductor {
public static void main(String[] args) throws IOException, InterruptedException {
// 获取与MQ服务器的连接
Connection connection = MQUtils.getConnection();
// 创建一个通道,用于进行消息的发布和接收等操作。
Channel channel = connection.createChannel();
//声明一个交换机, direct表示直连交换机,将消息通过路由键与绑定键进行精确匹配
// 交换机名字
// type
// 是否持久化 如果不是持久化,可以不要这个参数
channel.exchangeDeclare(MQUtils.EXCHANGE_NAME3,"topic",true);
//设置要发送的消息内容。
String msg = "com.a.b";
// 队列名 交换机名 "绑定的键"
//绑定队列1到交换机,指定了Key为error
channel.queueBind(MQUtils.QUEUE_NAME,MQUtils.EXCHANGE_NAME3,"#.medo");
//绑定队列2到交换机,指定了Key为debug
channel.queueBind(MQUtils.QUEUE_NAME2,MQUtils.EXCHANGE_NAME3,"com.*.*");
// 发布消息到队列
// 参数说明:
// - exchange: 交换机名称,空字符串表示默认的直连交换机
// - routingKey: 路由键,根据实际需求将消息路由到指定的队列
// -"":路由键,根据实际需求将消息路由到指定的队列。,这里为空字符串表示消息将被广播给所有绑定到交换机的队列。
//,这里指定为"error" 对应队列1
// - body: 消息体,即要发送的消息内容
channel.basicPublish(MQUtils.EXCHANGE_NAME3,"error",null,msg.getBytes());
System.out.println("send:" + msg);
// 关闭通道和MQ连接
channel.close();
connection.close();
}
}
消费者1,消费者2都不用变
先启动消费者1,消费者2然后启动生产者
这个时候我们把发消息的键值改成
channel.basicPublish(MQUtils.EXCHANGE_NAME3,"com.a.medo",null,msg.getBytes());
至此我们就我们了五种消息模型的实战,熟练掌握之后就可以用于项目上
我们定义了队列来执行canal监听之后应该做的操作,为了避免重复消息多次处理数据库操作,于是使用了设置头部识别码,但是好像因为RabbitMQ的版本不对,无法用旧方法设置头部并获取,固然探寻解决方法。
(已经完成了主题交换机的定义和队列的绑定,关闭自动确认)
消息的发送者
import com.alibaba.fastjson.JSON;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.cece.common.config.RabbitMQConfig;
import com.cece.common.entity.Course;
import com.cece.educoureservice.service.CourseService;
import com.xpand.starter.canal.annotation.CanalEventListener;
import com.xpand.starter.canal.annotation.ListenPoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.UUID;
/**
* 事件监听器
*/
@Slf4j
@CanalEventListener
public class CourseCanalListener {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private CourseService courseService;
/**
* 监听 edu_course数据库的course表
*/
@ListenPoint(schema = "edu_course",table = "course")
public void handleCourseUpdate(CanalEntry.EventType eventType, CanalEntry.RowData rowData){
//判断操作的类型
if("DELETE".equals(eventType.name())){
//遍历删除前数据行的每一列
rowData.getBeforeColumnsList().forEach(column -> {
//获得删除前的ID
if("id".equals(column.getName())){
//发删除消息给处理删除的队列 CorrelationData可以消息的id
rabbitTemplate.convertAndSend(RabbitMQConfig.COURSE_EXCHANGE,
RabbitMQConfig.KEY_COURSE_REMOVE,
Long.valueOf(column.getValue()),
new CorrelationData(UUID.randomUUID().toString()));
log.debug("发送删除课程{}消息",column.getValue());
}
});
}else if("INSERT".equals(eventType.name()) || "UPDATE".equals(eventType.name())){
//获得插入或更新后的数据
rowData.getAfterColumnsList().forEach(column -> {
if("id".equals(column.getName())){
//通过id查询课程的完整信息
System.out.println("保存的id是"+Long.valueOf(column.getValue()));
Course course =
courseService.getCourseById(Long.valueOf(column.getValue()));
//保存时查到的数据
System.out.println("保存时查到的"+course);
//包装到Course对象中,转换为JSON
String json = JSON.toJSONString(course);
System.out.println("要传的json为"+json);
//发送给添加或更新队列
rabbitTemplate.convertAndSend(RabbitMQConfig.COURSE_EXCHANGE,
RabbitMQConfig.KEY_COURSE_SAVE,
json,
new CorrelationData(UUID.randomUUID().toString()));
log.debug("发送保存课程消息{}",json);
}
});
}
}
}
消息的接收者
(删除的接收没有写,因为删除不要判定头部来确定是不是重复消息)
@Slf4j
@Component
public class CourseMQListener {
@Autowired
private CourseIndexService courseIndexService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 处理保存课程消息
* @param data
* @param channel
* @param message
*/
@RabbitListener(queues = {RabbitMQConfig.QUEUE_COURSE_SAVE})
public void handleSaveCourse(String data, Channel channel, Message message){
System.out.println("收到保存课程:" + data);
System.out.println("收到的message:"+message);
//获得消息的id
String id = message.getMessageProperties().getHeader("spring_returned_message_correlation");
System.out.println("消息的id:"+id);
//使用setNx名保存键,不存在就消费,存在就不消费
ValueOperations ops = redisTemplate.opsForValue();
try {
if(ops.setIfAbsent(id,"0",100L, TimeUnit.SECONDS)){
log.info("id{}不存在,执行业务",id);
//如果为true,代表键不存在,设置成功
//将json转换为课程对象
Course course = JSON.parseObject(data, Course.class);
courseIndexService.saveCourse(course);
//处理完后,设置为1代表业务完成
ops.set(id,"1",100L,TimeUnit.SECONDS);
//手动确认消息处理完成 参数1 消息的标识 参数2 是否批量确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
log.info("课程保存完成:{}",course);
}else{
//如果为false,代表键存在,不消费
log.info("id{}存在,不执行业务",id);
if("1".equals(ops.get(id))){
log.info("业务已经执行完");
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
} catch (IOException e) {
log.error("保存课程出现异常",e);
}
}
}
发现
//获得消息的id
String id = message.getMessageProperties().getHeader("spring_returned_message_correlation");
结果为null,但是我们打印
UUID.randomUUID().toString()
是没有问题的,按理说发送消息的时候应该被携带上去了
继续检查 Message
结果发现头部为空,找了半天也没有找到解决方案,
可能是版本的原因,我搜到了可以手动设置请求头
import org.springframework.amqp.core.*;
String id =UUID.randomUUID().toString();
System.out.println("生成的表示符是"+id);
MessageProperties properties = new MessageProperties();
properties.getHeaders().put("id", id);
Message message = new Message(json.getBytes(), properties);
发送的时候我们发message,不是发json
我本来想把json直接放前面
Message message = new Message(json, properties);
message的确是没有内容(毕竟打印的是数组),但有了头部
而且从,data获得了数据
String id = message.getMessageProperties().getHeader("spring_returned_message_correlation");
这个语句没有获得头部,因为头部是我们自定义的
String id = message.getMessageProperties().getHeaders().get("id").toString();
System.out.println("自定义头部字段 id 的值为:" + id);
这样就能正常处理信息了
方法修改后的代码
@Slf4j
@CanalEventListener
public class CourseCanalListener {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private CourseService courseService;
/**
* 监听 edu_course数据库的course表
*/
@ListenPoint(schema = "edu_course",table = "course")
public void handleCourseUpdate(CanalEntry.EventType eventType, CanalEntry.RowData rowData){
//判断操作的类型
if("DELETE".equals(eventType.name())){
//遍历删除前数据行的每一列
rowData.getBeforeColumnsList().forEach(column -> {
//获得删除前的ID
if("id".equals(column.getName())){
//发删除消息给处理删除的队列 CorrelationData可以消息的id
rabbitTemplate.convertAndSend(RabbitMQConfig.COURSE_EXCHANGE,
RabbitMQConfig.KEY_COURSE_REMOVE,
Long.valueOf(column.getValue()),
new CorrelationData(UUID.randomUUID().toString()));
log.debug("发送删除课程{}消息",column.getValue());
}
});
}else if("INSERT".equals(eventType.name()) || "UPDATE".equals(eventType.name())){
//获得插入或更新后的数据
rowData.getAfterColumnsList().forEach(column -> {
if("id".equals(column.getName())){
//通过id查询课程的完整信息
System.out.println("保存的id是"+Long.valueOf(column.getValue()));
Course course = courseService.getCourseById(Long.valueOf(column.getValue()));
//保存时查到的数据
System.out.println("保存时查到的"+course);
//包装到Course对象中,转换为JSON
String json = JSON.toJSONString(course);
System.out.println("要传的json为"+json);
String id =UUID.randomUUID().toString();
System.out.println("生成的表示符是"+id);
MessageProperties properties = new MessageProperties();
properties.getHeaders().put("id", id);
Message message = new Message(json.getBytes(), properties);
// rabbitTemplate.convertAndSend(RabbitMQConfig.COURSE_EXCHANGE,
// RabbitMQConfig.KEY_COURSE_SAVE,
// message,
// new CorrelationData(UUID.randomUUID().toString()));
//发送给添加或更新队列
rabbitTemplate.convertAndSend(RabbitMQConfig.COURSE_EXCHANGE,
RabbitMQConfig.KEY_COURSE_SAVE,
message,
new CorrelationData(id)
);
log.debug("发送保存课程消息{}",message);
}
});
}
}
}
@RabbitListener(queues = {RabbitMQConfig.QUEUE_COURSE_SAVE})
public void handleSaveCourse(String data, Channel channel, Message message){
System.out.println("收到保存课程:" + data);
System.out.println("收到的message:"+message);
//获得消息的id
// String id = message.getMessageProperties().getHeader("spring_returned_message_correlation");
// System.out.println("消息的id:"+id);
String id = message.getMessageProperties().getHeaders().get("id").toString();
System.out.println("自定义头部字段 id 的值为:" + id);
//使用setNx名保存键,不存在就消费,存在就不消费
ValueOperations ops = redisTemplate.opsForValue();
try {
if(ops.setIfAbsent(id,"0",100L, TimeUnit.SECONDS)){
log.info("id{}不存在,执行业务",id);
//如果为true,代表键不存在,设置成功
//将json转换为课程对象
Course course = JSON.parseObject(data, Course.class);
courseIndexService.saveCourse(course);
//处理完后,设置为1代表业务完成
ops.set(id,"1",100L,TimeUnit.SECONDS);
//手动确认消息处理完成 参数1 消息的标识 参数2 是否批量确认
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
log.info("课程保存完成:{}",course);
}else{
//如果为false,代表键存在,不消费
log.info("id{}存在,不执行业务",id);
if("1".equals(ops.get(id))){
log.info("业务已经执行完");
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
} catch (IOException e) {
log.error("保存课程出现异常",e);
}
}