简单的消息交互案例
1 依赖引入
<!-- 引入RabbitMQ客户端依赖 -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
</dependency>
2 设置连接MQ的工具类
public class RabbitConnectionUtil {
public static Connection getConnection() throws IOException, TimeoutException {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 工厂IP,连接RabbitMQ的队列
factory.setHost("192.168.190.129");
// 注意,连接端口是5672,Linux端的防火墙需要开放该端口
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin");
// 创建连接
return factory.newConnection();
}
}
3 创建消息发送者
实例代码:
public class RabbitMQProducer {
public static final String QUEUE_NAME = "RabbitName";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitConnectionUtil.getConnection();
// 获取信道
Channel channel = connection.createChannel();
/**
* 生成队列
* queue 队列名称
* durable 是否持久化
* exclusive 是否独占队列
* autoDelete 是否自动删除队列(服务器不再使用时自动删除队列)
* arguments 队列参数
*/
HashMap<String, Object> param = new HashMap<>();
param.put("name", "梦尘");
param.put("action", "添加了一个消息");
channel.queueDeclare(QUEUE_NAME, false, false, false, param);
/**
* exchange 交换机
* 路由的key值
* 其他参数信息
* 发送消息的消息体
*/
String message = "给您发了一条消息";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println("消息发送完毕!");
}
}
运行成功:
由运行可以看出,队列中接收到一条为未被消费的消息。
4 创建消息接受者
实例代码:
public class RabbitMQConsumer {
public static String QUEUE_NAME = "RabbitName";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 接收消息回调
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println(new String(message.getBody()));
};
// 取消消息回调
CancelCallback cancelCallback = consumerTag -> {
System.out.println("消息消费中断!");
};
/**
* queue 消费队列名称
* autoOk 消费成功后是否自动应答
* consumer 消费者未成功消费回调
* 消费者取消回调
*/
channel.basicConsume(String.valueOf(QUEUE_NAME), true, deliverCallback, cancelCallback);
}
}
运行结果:
5 常见错误解决
connection error; protocol method: #method<connection.close>(reply-code=530, reply-text=N
因为没有设置系统默认的Virtua lHost,如下图
解决:为用户设置permission
RabbitMQ工作队列
工作队列(又称任务队列)的主要思想是为了避免立即执行资源密集型任务,而不得不等待他完成。其作用就是将需要处理的任务放在队列(先进先出)中,一个工作进程可以取出任务并完成工作。如果启动了多个工作进程,那么这些工作线程会一起处理这些任务。
好比工厂的流水线,如果流水线上有300个产品,而工位上只有一位工人在给产品加工,可想而知其效率低下。如果在流水线后多安排几个工位,那么就可以提高产品加工速度(注意:一个产品只能被加工一次,不能被多次处理,要防止重复“加工”)。
轮询发消息
概念
rabbitmq队列处理机制默认是轮询的,即轮流按工作顺序将任务分配给工作线程(循环分配任务)。
实例代码
生产者首先往队列中加入15条消息,成功加入队列后等待消费者消费。生产者代码如下:
public static final String QUEUE_NAME = "rabbit_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitConnectionUtil.getConnection();
// 获取信道
Channel channel = connection.createChannel();
for (int i = 0; i < 15; i++) {
/**
* 生成队列
* queue 队列名称
* durable 是否持久化
* exclusive 是否独占队列
* autoDelete 是否自动删除队列(服务器不再使用时自动删除队列)
* arguments 队列参数
*/
HashMap<String, Object> param = new HashMap<>();
param.put("name", "梦尘");
param.put("action", "添加了一个消息");
channel.queueDeclare(QUEUE_NAME, false, false, false, param);
/**
* exchange 交换机
* 路由的key值
* 其他参数信息
* 发送消息的消息体
*/
String message = "给您发了一条消息";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println("消息发送完毕!");
}
}
创建5个消费者,监听到队列中有任务时进行工作。消费者代码如下:
public class Work01 implements Runnable {
public static final String QUEUE_NAME = "rabbit_queue";
public static void main(String[] args) {
Runnable work01 = new Work01();
new Thread(work01, "A").start();
new Thread(work01, "B").start();
new Thread(work01, "C").start();
new Thread(work01, "D").start();
new Thread(work01, "E").start();
}
@Override
public void run() {
try {
String threadName = Thread.currentThread().getName();
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 消息接收
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("Work01 => " + threadName + "接收到消息:" + new String(message.getBody()));
};
// 消息取消监听
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag + "消息者取消消息发送!");
};
// 消息发送
try {
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
} catch (IOException e) {
throw new RuntimeException(e);
}
} catch (IOException | TimeoutException e) {
throw new RuntimeException(e);
}
}
}
运行结果:
可以看到里面成功添加了15条消息,接下来运行消费者查询观察消费过程。
消费者运行结果:
总结
工作队列是学习RabbitMQ的一个非常重要知识点,通过利用队列的先进先出的特点,将工作顺序分配给工作线程执行。
思考
如果D任务在被消费者处理过程中因某些原因出现bug导致任务中断,而这时候队列已经将任务分配出去并清除了队列里的任务D,这就意味着我们丢失了这条未处理完毕的任务,那么我们该怎么防止任务丢失的情况?
消息应答
概念
根据上一篇文章的思考中得知,如果任务在执行过程中发生故障导致任务中断,并且这条任务也被队列清除掉了,也就是意味着我们就丢失了一条未被执行完的任务。
消息应答的出现可以保证消息在发送过程中不丢失。其工作原理就是,消费者在接收到了队列分配的任务并且处理完这条任务后,告诉RabbitMQ它已经处理完这条任务,可以从队列中清除掉这条任务了。
自动应答
Rabbit MQ默认的应答机制就是自动应答。消息发送后立即被认为已经传送成功。即当消费者接收到消息后立即通知RabbitMQ,Rabbit MQ此时就可以直接从队列中移除该消息。
缺点:很可能会造成消息没有被完成处理就丢失消息,如:
-
接收过多,来不及被处理的消息,导致大量消息积压而耗尽内存,最终被操作系统强行终止(线程/进程终止任务)。
-
消息处理过程出现问题(Bug),导致程序抛出异常造成消息未处理完毕,而这条消息因为已经被自动应答默认从队列中移除了。
手动应答
手动应答的出现可以很好的处理自动应答的缺点,它可以通过手动控制应答机制,将当前工作线程处理失败的消息通知Rabbit MQ,转交给其他工作线程处理,减低消息丢失的机率。手动应答还可以进行批量应答,减少了网络拥堵的情况,但可能会造成消息不会被完全处理。
生产者:
/**
*
* @author xgs87762
*/
public class RabbitMQAskProducer {
private static final String TAG = "RabbitMQAsk";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
Map<String, Object> params = new HashMap<>();
params.put("author", "代码の诱惑");
params.put("age", "22");
for (int i = 1; i <= 50; i++) {
/**
* 生成队列
* queue 队列名称
* durable 是否持久化
* exclusive 是否独占队列
* autoDelete 是否自动删除队列(服务器不再使用时自动删除队列)
* arguments 队列参数
*/
channel.queueDeclare(TAG, false, false, false, params);
String message = "发布消息";
channel.basicPublish("", TAG, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println("发送了" + i + "条消息");
}
}
}
消费者
/**
* RabbitMQAskCustomer.java
* TODO
* Created on 2023/2/26 18:22
* 1、测试工作线程消费情况
* 2、测试工作线程出现异常,任务自动分配给其他正常工作线程消费
* @author xgs87762
* @version V1.0
*/
public class RabbitMQAskCustomer implements Runnable {
// 队列名称
private static final String TAG = "RabbitMQAsk";
// 计算已执行成功任务数
private static Integer counter = 0;
public static void main(String[] args) {
Runnable askCustomer = new RabbitMQAskCustomer();
for (int i = 1; i <= 10; i++) {
new Thread(askCustomer, "线程" + i).start();
}
}
@Override
public void run() {
try {
String threadName = Thread.currentThread().getName();
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
DeliverCallback deliverCallback = (consumerTag, message) -> {
// 消息接收监听
System.out.println(threadName + " ==> 准备开展工作...");
RabbitMQAskCustomer.thread();
// 测试:测试线程执行失败,观察工作任务情况
// 结论:线程8出现问题后,队列自动将工作任务分配给了其他线程消费。
if ("线程8".equals(threadName)) {
throw new IOException("Thread " + threadName + "异常");
}
System.out.println(threadName + " ==> 工作执行完毕!");
/**
* 消息应答
* deliverTag 消息的标记 tag,表明消息的唯一标识
* multiple 是否批量应答(一般不允许应答,防止消息丢失)
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
System.out.println(threadName + " ==> 消息应答提交完毕!");
counter++;
System.out.println("已执行 》" + counter + "《 条任务!");
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(threadName + " ==> 消息取消");
};
channel.basicConsume(TAG, false, deliverCallback, cancelCallback);
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
public static void thread() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
消息持久化
概念
RabbitMQ在新增队列后,MQ在正常运行状态下队列是保持运行的,但是,当RabbitMQ出现某种原因崩溃后出现重启或关闭后,队列及消息就会被清除。为防止队列和消息丢失,因而需要将队列和消息标记为持久化。
实例代码
消息提供者:
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
public class DurableProducer {
public static final String QUEUE_NAME = "DURABLE_QUEUE";
public static void main(String[] args) {
try {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
/**
* @param queue Queue队列的名称
* @param durable 持久化,如果我们声明的是持久队列(该队列将在服务器重启后存活)。
* @param exclusive 声明独占队列(仅限于此连接)。
* @param autoDelete 如果我们声明一个自动删除队列,则为True(服务器将在不再使用时将其删除)。
* @param arguments 参数队列的其他属性(构造参数)
*/
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 动态发送消息
Scanner scanner = new Scanner(System.in);
String message = "";
while (scanner.hasNext()) {
message = scanner.next();
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println("[任务提供者]:" + message);
}
} catch (IOException e) {
throw new RuntimeException(e);
} catch (TimeoutException e) {
throw new RuntimeException(e);
}
}
}
运行结果
重启前:
注:重启前把java程序停掉,队列显示结果更明显
重启后:
运行命令重启MQ
- systemctl restart rabbitmq-server
不公平分发
不公平分发可以理解为能者多劳,按照消费者的处理能力进行消息分配(处理快,消息分配多),以此提高队列消息处理速度,充分利用了消费者的性能。一般情况下会使用不公平分发,而不使用轮询。
-
// 消费方设置prefetch = 1,即开启不公平分发
-
channel.basicQos(1);
预取值
可以为消费者指定分配固定任务数量,假设有七个任务,可以给A分配2个任务,给B分配4个任务。
// 消费者A:设置prefetch = 2
channel.basicQos(2);
// 消费者B:设置 prefetch=4
channel.basicQos(4);
发布确认
消费者能消费消息的前提是,提供者能正确推送消息到队列中。然而,如果在提供者往队列中推送消息时,RabbitMQ出现某些突发意外,比如重启,导致消息推送时丢失,这时就需要手动处理和恢复推送失败的消息。
三种发布确认方式:
- 单个确认
问题排查相对简单,但效率低。
- 批量确认
出现问题不易定位具体问题。执行效率高。
- 异步确认
发布消息的同时,采用异步确认方式提高执行效率。
实例代码:
单次发布确认
public static void producerSingleton() throws Exception {
Connection connection = RabbitConnectionUtil.getConnection();
// 队列声明
Channel channel = connection.createChannel();
channel.queueDeclare(queenName, true, false, false, new HashMap<>());
// 开启发布确认
channel.confirmSelect();
long startTime = System.currentTimeMillis();
for (int i = 0; i < SEND_COUNT; i++) {
String sendMessage = "第<" + i + ">消息 ";
channel.basicPublish("", queenName, null, sendMessage.getBytes(StandardCharsets.UTF_8));
// 单个消息马上进行发布确认
boolean flag = channel.waitForConfirms();
if (flag) {
System.out.println("消息发送成功!");
}
}
long endTime = System.currentTimeMillis();
System.out.println("单个发布确认:耗时:" + (endTime - startTime));
}
批量确认
public static void producerBatch() {
try {
Connection connection = RabbitConnectionUtil.getConnection();
// 队列声明
Channel channel = connection.createChannel();
channel.queueDeclare(queenName, true, false, false, new HashMap<>());
// 开启发布确认
channel.confirmSelect();
long startTime = System.currentTimeMillis();
// 50条确认一次
int confirmPoint = 50;
for (int i = 0; i < SEND_COUNT; i++) {
String sendMessage = "第<" + i + ">消息 ";
channel.basicPublish("", queenName, null, sendMessage.getBytes(StandardCharsets.UTF_8));
// 批量确认
if (i % confirmPoint == 0) {
channel.waitForConfirms();
// System.out.println("消息发送成功!");
}
}
long endTime = System.currentTimeMillis();
System.out.println("批量发布确认:耗时:" + (endTime - startTime));
} catch (IOException | TimeoutException | InterruptedException e) {
e.printStackTrace();
}
}
异步确认
public static void producerSync() {
try {
Connection connection = RabbitConnectionUtil.getConnection();
// 队列声明
Channel channel = connection.createChannel();
channel.queueDeclare(queenName, true, false, false, new HashMap<>());
// 开启发布确认
channel.confirmSelect();
long startTime = System.currentTimeMillis();
// 消息确认成功回调 (消息标记,是否批量确认)
ConfirmCallback ackCallback = (deliverTag, multiple) -> {
System.out.println("消息监听成功" + deliverTag);
};
// 消息确认失败回调 (消息标记,是否批量确认)
ConfirmCallback nackCallback = (deliverTag, multiple) -> {
System.out.println("监听失败 ==> " + deliverTag);
};
// 准备消息监听器,监听哪些消息状态
channel.addConfirmListener(ackCallback, nackCallback);
for (int i = 0; i < SEND_COUNT; i++) {
String sendMessage = "第<" + i + ">消息 ";
channel.basicPublish("", queenName, null, sendMessage.getBytes(StandardCharsets.UTF_8));
}
long endTime = System.currentTimeMillis();
System.out.println("批量发布确认:耗时:" + (endTime - startTime));
} catch (IOException | TimeoutException e) {
e.printStackTrace();
}
}
测试:
public static void main(String[] args) throws Exception {
// 1、单个确认: 可以快速定位确认失败位置,效率低
producerSingleton();
// 2、批量确认:不好定位确认位置,效率高
producerBatch();
// 3、异步批量确认: 兼容二者,效率高,可监听消息发送状态
producerSync();
}
交换机(Exchanges)
在RabbitMQ中,生产者发送消息是不会直接将消息直接推送到消息推送到队列中的,实际上推送的过程是交由交换机推送到队列中(如果没有定义交换机会使用MQ默认的交换机推送),生产者只能推送消息到交换机中。其中,在之前的推送中,队列中的每条消息只能被消费者消费一次,通过交换机我们就可以实现把消息推到不同的队列,进而实现多个消费者消相同的消息。
注:交换机可以在提供者/消费者任意一端声明。
以下案例建议先启动消费者开启监听,后启动消息提供者,方便测试结果。
交换机类型
直接交换机(Direct Exchange)
通过routingKey和bindingKey将交换机和路由绑定,交换机通过对比Key值将消息推送到指定的队列中。
消息提供者
public class DirectProduct {
private static final String EXCHANGE_NAME = "DIRECT_EXCHANGE";
private static final String routingKey = "direct_key";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
String message = "测试一条消息";
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println("[提供者] ==> 推送成功!");
}
}
消费者
public class DirectCustomer {
private static final String EXCHANGE_NAME = "DIRECT_EXCHANGE";
private static final String routingKey = "direct_key";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 获取临时队列,队列消息消费完后自动删除
String queueName = channel.queueDeclare().getQueue();
// 绑定队列
channel.queueBind(queueName, EXCHANGE_NAME, routingKey);
DeliverCallback deliverCallback = (tag, message) -> {
System.out.println("[消费者] ==> " + new String(message.getBody()));
};
CancelCallback cancelCallback = (tag) -> {
System.out.println("消息接收失败");
};
// 绑定交换机和队列
channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
}
}
测试
消费者启动(先启动,方便监听)
提供者启动
主题交换机(Topic Exchange)
将路由按模式匹配,可以通过设置 # 或 ***** 对队列中定义的routingKey模糊匹配,匹配成功后将消息转入队列
**#:**匹配一个或多个词,如:ta.#.te == ta.tb.tc.td.te
**:**匹配一个词,如:td..te == td.tf.te
消息提供者
public class TopicProducer {
private static final String EXCHANGE_NAME = "EXCHANGE_NAME_TOPIC";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
// 输入
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("请输入发送消息:");
String message = scanner.nextLine();
if (Objects.equals(message, "exit")){
break;
}
System.out.println("请输入发送路由:");
String routerKey = scanner.nextLine();
channel.basicPublish(EXCHANGE_NAME, routerKey, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println("[主题交换机<" + routerKey + ">]: " + message);
}
}
}
消费者A
public class TopicCustomer_A {
private static final String EXCHANGE_NAME = "EXCHANGE_NAME_TOPIC";
public static final String CODER_LURE = "coder.#.lure";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明一个临时队列,队列名称随机,当消费者端口与队列的连接时,队列自动删除
String queueName = channel.queueDeclare().getQueue();
// 绑定交换机和队列
channel.queueBind(queueName, EXCHANGE_NAME, CODER_LURE);
System.out.println("[主题交换机<消费者A>] ==<接收成功,routingKey:[" + CODER_LURE + "]>==> 监听开始!");
DeliverCallback deliverCallback = (tag, message) -> {
String messageContent = "[主题交换机<消费者A>] ==<接收成功,routingKey:[" + CODER_LURE + "]>==> " + new String(message.getBody());
System.out.println(messageContent);
};
CancelCallback cancelCallback = (tag) -> {
System.out.println("[主题交换机<消费者A>] ==<接收取消,routingKey:[" + CODER_LURE + "]>==> 失败!");
};
// 接收消息
channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
}
}
消费者B
public class TopicCustomer_B {
private static final String EXCHANGE_NAME = "EXCHANGE_NAME_TOPIC";
public static final String CODER_LURE = "*.coder.lure";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明一个临时队列,队列名称随机,当消费者端口与队列的连接时,队列自动删除
String queueName = channel.queueDeclare().getQueue();
// 绑定交换机和队列
channel.queueBind(queueName, EXCHANGE_NAME, CODER_LURE);
System.out.println("[主题交换机<消费者B>] ==<接收成功,routingKey:[" + CODER_LURE + "]>==> 监听开始!");
DeliverCallback deliverCallback = (tag, message) -> {
String messageContent = "[主题交换机<消费者B>] ==<接收成功,routingKey:[" + CODER_LURE + "]>==> " + new String(message.getBody());
System.out.println(messageContent);
};
CancelCallback cancelCallback = (tag) -> {
System.out.println("[主题交换机<消费者B>] ==<接收取消,routingKey:[" + CODER_LURE + "]>==> 失败!");
};
// 接收消息
channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
}
}
测试:
启动提供者方法,控制台信息如下:
消费者A接收
消费者B接收
首部交换机(Headers Exchange)
首部交换机和扇出交换机都不需要路由键(routingKey),交换机时通过headers头部来将消息映射到队列中。其中Hash结构中要求携带一个键 ‘x-match’ ,这个键值可以是any或者all。
any:只要在发布消息是携带有一对键值对headers满足队列定义的多个参数arguments中其中一个,就能匹配上。(需要key:value完全匹配才行)
all:需要所有entry和绑定在队列上的所有entry完全匹配
消息提供者
/**
* HeadersProducer
*
* @author codeの诱惑
* @version 1.0.0
* @Descript 首部交换机:
* 首部交换机和扇出交换机都不需要路由键(routingKey),交换机时通过headers头部来将消息映射到队列中
* 其中Hash结构中要求携带一个键 ‘x-match’ ,这个键值可以是any或者all
* any:只要在发布消息是携带有一对键值对headers满足队列定义的多个参数arguments中其中一个,就能匹配上。(需要key:value完全匹配才行)
* all:需要所有entry和绑定在队列上的所有entry完全匹配
* Created on 2023/5/2 17:42
*/
public class HeadersProducer {
private static final String EXCHANGE_NAME = "HEADERS_EXCHANGE";
public static void main(String[] args) throws IOException, TimeoutException {
Connection conn = RabbitConnectionUtil.getConnection();
Channel channel = conn.createChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.HEADERS);
Map<String, Object> headers = new HashMap<String, Object>();
headers.put("author", "coderの诱惑");
headers.put("exchangeName", "Headers Exchange");
headers.put("version", "v1.0");
System.out.println("发送一条消息");
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().headers(headers).build();
String message = "hello world!";
channel.basicPublish(EXCHANGE_NAME, "", properties, message.getBytes(StandardCharsets.UTF_8));
}
}
消费者
/**
* HeaderExchangeCustomer
*
* @author codeの诱惑
* @version 1.0.0
* @Descript 首部交换机
* Created on 2023/5/3 0:11
*/
public class HeaderExchangeCustomer {
private static final String EXCHANGE_NAME = "HEADERS_EXCHANGE";
public static void main(String[] args) throws IOException, TimeoutException {
Connection conn = RabbitConnectionUtil.getConnection();
Channel channel = conn.createChannel();
Map<String, Object> arguments = new HashMap<String, Object>();
// arguments.put("x-match", "all");
// 测试不匹配
arguments.put("all", "测试不同headers,不匹配");
arguments.put("x-match", "any");
arguments.put("author", "coderの诱惑");
arguments.put("exchangeName", "Headers Exchange");
arguments.put("version", "v1.0");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "", arguments);
System.out.println("[首部交换机]:消费者等待消息中...");
DeliverCallback deliverCallback = (tag, message) -> {
byte[] body = message.getBody();
String string = new String(body);
System.out.println(string);
};
CancelCallback cancelCallback = (tag) -> {
System.out.println("取消");
};
channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
}
}
测试
执行提供者后,消费者监听结果如下:
扇出交换机(Fanount Exchange)
以广播的形式向队列推送消息,即类比通过广播发布通知,所有人接收通知内容。
以下以日志输出为案例,通过推送日志内容,一个负责将日志打印控制台,一个通过负责将日志写入日志文件当中。
消息提供者
- public class ExProducer {
private static final String EXCHANGE_NAME = "EXCHANGE_NAME_LOG";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String inputText = scanner.next();
channel.basicPublish(EXCHANGE_NAME, "", null, inputText.getBytes(StandardCharsets.UTF_8));
System.out.println("[生产者输出日志]: " + inputText);
}
}
}
消费者(控制台输出)
public class ExConsumerForConsoleLog {
private static final String EXCHANGE_NAME = "EXCHANGE_NAME_LOG";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
/**
* @param exchange 交换机名称
* @param type 交换机类型
*/
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
// 声明一个临时队列,队列名称随机,当消费者端口与队列的连接时,队列自动删除
String queueName = channel.queueDeclare().getQueue();
// 绑定交换机和队列
/**
* @param queueName 队列名称
* @param exchangeName 交换机名称
* @param routingKey 路由key(由于扇出交换机是以广播形式推送消息到队列,routingKey可以为空
* ,即可以忽略routingKey值
* ,忽略后会向所有队列推送,如果指定了routingKey,那就和 ‘直接交换机’ 差不多了)
*/
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("等待接收队列消息, 打印控制台...");
DeliverCallback deliverCallback = (tag, message) -> {
System.out.println("[控制台<消费者A>] ==<接收成功>==> " + new String(message.getBody()));
};
CancelCallback cancelCallback = (tag) -> {
System.out.println("[控制台<消费者A>] ==<接收取消>==> 失败!");
};
// 接收消息
channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
}
}
消费者(日志文件输出)
public class ExConsumerForFileLog {
private static final String EXCHANGE_NAME = "EXCHANGE_NAME_LOG";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
/**
* @param exchange 交换机名称
* @param type 交换机类型
*/
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
// 声明一个临时队列,队列名称随机,当消费者端口与队列的连接时,队列自动删除
String queueName = channel.queueDeclare().getQueue();
// 绑定交换机和队列
/**
* @param queueName 队列名称
* @param exchangeName 交换机名称
* @param routingKey 路由key(由于扇出交换机是以广播形式推送消息到队列,routingKey可以为空
* ,即可以忽略routingKey值
* ,忽略后会向所有队列推送,如果指定了routingKey,那就和 ‘直接交换机’ 差不多了)
*/
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("等待接收队列消息, 写入日志文件...");
DeliverCallback deliverCallback = (tag, message) -> {
String messageContent = "[日志文件<消费者B>] ==<接收成功>==> " + new String(message.getBody());
SimpleDateFormat sf = new SimpleDateFormat("yyyyMMddHHmmss");
String date = sf.format(new Date());
System.out.println(messageContent);
writeFile(messageContent, "" + date + ".log");
};
CancelCallback cancelCallback = (tag) -> {
System.out.println("[日志文件<消费者B>] ==<接收取消>==> 失败!");
};
// 接收消息
channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
}
public static void writeFile(String message, String path) {
try {
String basePath = new String(System.getProperty("user.dir").getBytes(StandardCharsets.UTF_8)) + "\\RabbitMQ-simple\\src\\main\\java\\com\\mc\\exchange\\logs\\";
String absolutePath = basePath + path;
System.out.println("存储路径:" + absolutePath);
File Folder = new File(basePath);
File file = new File(absolutePath);
if (!Folder.exists()) {
Folder.mkdirs();
}
if (!file.exists()) {
file.createNewFile();
}
OutputStream out = new FileOutputStream(file);
byte[] bytes = message.getBytes(StandardCharsets.UTF_8);
out.write(bytes);
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
测试
消息提供者控制台案例
消费者输出控制台
消费者输出日志文件
默认交换机(Default Exchange)
默认交换机实际和直接交换机一样,只不过默认交换机并不用特意声明交换机名称和routingKey 。以下不作案例演示。
消息存活期(DDL)
在消息提供者发送消息时,给消息设置存活时间,当消息在该时间段内没有被消费,则该消息就会被自动清除。
MessageProperties messageProperties = new MessageProperties();
messageProperties.setExpiration("5000"); // 消息的存活时间为 5 秒
Message message = new Message("Hello World".getBytes(), messageProperties);
rabbitTemplate.convertAndSend("myQueue", message);
死信队列(DLX)
消费者在消费消息过程中出现意外导致消费失败即称为“死信”。而不希望这些执行失败的消息丢失,需要存储到另一个队列当中时,这种队列即为“死信队列”。
消息出现一下情况会导致 “死信”
-
消息被否定,消费者使用 channel.basicNack或channel.basicReject,并且requeue被设置为false时。
-
消息超过过期时间。消息在队列的存活时间超过了设置的生存时间(TTL)
-
消息数量超出队列长度(消息溢出)
以下将以“消息溢出”为案例
消费者
public class NormalConsumer {
// 死信队列名
private static final String DEAD_QUEUE_NAME = "dead-queue";
// 死信交换机
private static final String DEAD_EXCHANGE_NAME = "dead-exchange";
// 普通队列名称
private static final String NORMAL_QUEUE_NAME = "normal-queue";
// 普通交换机名称
private static final String NORMAL_EXCHANGE_NAME = "normal-exchange";
public static final String NORMAL_ROUTING_KEY = "normal-demo";
public static final String DEAD_ROUTING_KEY = "dead-demo";
public static void main(String[] args) throws Exception, TimeoutException {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(NORMAL_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
// 声明队列
Map<String, Object> normalArguments = new HashMap<>();
// 死信队列绑定配置:死信交换机、死信routingKey、普通队列长度限制(超出长度变为死信)
normalArguments.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
normalArguments.put("x-dead-letter-routing-key", DEAD_ROUTING_KEY);
normalArguments.put("x-max-length", 4);
channel.queueDeclare(NORMAL_QUEUE_NAME, false, false, false, normalArguments);
// 声明死信队列
Map<String, Object> deadArguments = new HashMap<>();
channel.queueDeclare(DEAD_QUEUE_NAME, false, false, false, deadArguments);
// 绑定普通交换机和普通队列
channel.queueBind(NORMAL_QUEUE_NAME, NORMAL_EXCHANGE_NAME, NORMAL_ROUTING_KEY);
// 绑定死信交换机和死信队列
channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, DEAD_ROUTING_KEY);
System.out.println("normal consumer:waiting message...");
//消息消费成功之后的回调
DeliverCallback deliverCallback = (consumerTag, message) -> {
String messageStr = new String(message.getBody(), StandardCharsets.UTF_8);
if (messageStr.contains("5")) {
System.out.println("mock ==> 请手动停止NomalConsumer程序,重启消费者,模拟普通队列的消息超出长度,导致死信");
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("normal consumer:" + messageStr);
};
channel.basicConsume(NORMAL_QUEUE_NAME, true, deliverCallback, consumer -> {
});
}
}
死信消费者
public class DeadConsumer {
// 死信队列名
private static final String DEAD_QUEUE_NAME = "dead-queue";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
System.out.println("dead consumer: waiting message...");
DeliverCallback deadCallback = (consumerTag, message) -> {
System.out.println("dead consumer: " + new String(message.getBody()));
};
channel.basicConsume(DEAD_QUEUE_NAME, true, deadCallback, consumerTag -> {
});
}
}
消息提供者
public class NormalAndDeadProducer {
// 普通交换机名称
private static final String NORMAL_EXCHANGE_NAME = "normal-exchange";
public static final String NORMAL_ROUTING_KEY = "normal-demo";
public static void main(String[] args) throws IOException, TimeoutException {
Connection connection = RabbitConnectionUtil.getConnection();
Channel channel = connection.createChannel();
// 死信消息TTL时间
AMQP.BasicProperties properties = new AMQP.BasicProperties()
.builder()
.expiration("10000")
.build();
for (int i = 0; i < 10; i++) {
String message = "message info index is " + i;
channel.basicPublish(NORMAL_EXCHANGE_NAME, NORMAL_ROUTING_KEY, properties, message.getBytes(StandardCharsets.UTF_8));
}
}
}
请按顺序执行以下步骤:
- 启动 消费者(NormalConsumer)
- 启动 死信消费者(DeadConsumer)
- 启动 消息提供者(NormalAndDeadProducer)
- 此刻可以看到消费者消费到了一半,这时立即关闭消费者程序的运行
- 重新启动一次 消息提供者,造成队列消息溢出
- 查看 死信消费者 控制台,可以看到,有一部分消息进入了死信队列并被 死信消费者 消费了