1.前言
特点:生态好,好学习,易于理解,时效性强,支持很多不同语言的客户端,扩展性,可用性都很不错。
学习性价比非常高的消息队列,适用于绝大多数中小规模分布式系统。
学习网站:https://www.rabbitmq.com/
2.基本概念
AMQP协议:AMQP 0-9-1 Model Explained | RabbitMQ
高级消息队列协议(Advanced Message Queue Protocol)
生产者:发消息到某个交换机/队列
消费者:从某个队列中取消息
交换机(Exchange):负责把消息转发到对应的队列。
队列(Queue):存储消息的队列
路由(Routes):转发,就是怎么把消息从一个地方占到另一个地方(比如从生产者转发到某个队列)

3.安装
3.1本地安装
快速开始:RabbitMQ: One broker to queue them all | RabbitMQ
windows安装:Installing on Windows | RabbitMQ

先安装erlang25.3.2(因为RabbitMQ依赖erlang,RabbitMQ是erlang开发的),这个语言的性能非常高。
erlang下载:Otp 25.3.2 - Erlang/OTP

安装完erlang之后,安装RabbitMQ即可
ctrl + r 打开 services.msc(服务菜单),查看RabbitMQ服务是否已启动:

安装RabbitMQ监控面板:
在RabbitMQ安装目录的sbin中执行下述脚本:
java
rabbitmq-plugins.bat enable rabbitmq_management
输入以下信息

访问:http://localhost:15672,用户密码都是guest:

如果想要在远程服务器安装访问RabbitMQ管理面板,你要自己创建一个管理员账号,不能使用默认的guest,否则会被拦截(官方处于安全考虑)。
如果被拦截,可用自己创建管理员用户:
参考文档Adding a User:Authentication, Authorisation, Access Control | RabbitMQ

RabbitMQ端口占用:
5672:程序连接的端口
15672:webUI

3.2服务器安装
通过以下文档进行安装RabbitMQ访问RabbitMQ,访问端口是5673映射到了5672,所以进行连接访问使用5673访问。
docker部署rabbitmq消息队列_docker查看rabbitmq状态-CSDN博客
4.JAVA连接RabbitMQ可能会遇到的问题
其实进行使用的时候,除了密码错误就是没有进行创建用户,进行在RabbitMQ管理面板进行新建用户,授予权限即可。
RabbitMQ客户端连接报错原因分析_分布式消息服务RabbitMQ版 (huaweicloud.com)
5.快速入门
MQ官方教程:RabbitMQ Tutorials | RabbitMQ
5.1单向发送
文档:RabbitMQ tutorial - "Hello World!" | RabbitMQ
一个生产者给一个队列发消息,一个消费者从这个队列取出消息,一对一。

首先需要引入消息队列JAVA客户端:
java
<!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.17.0</version>
</dependency>
生产者代码:
java
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class SingleSend {
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("49.232.17.169");
factory.setUsername("admin");
factory.setPassword("123456");
factory.setPort(5673);
factory.setConnectionTimeout(30000000);
factory.setHandshakeTimeout(30000000);
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "HelloWorld";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Send '" + message + "'");
}
}
}
Channel频道:理解为操作消息队列client(比如jdbcClient,redisClient),提供了和消息队列server建立通信的传输发方法(为了服用连接,提高传输效率)。程序通过channel操作RabbitMQ(收发消息)。
创建消息队列:
参数:
queueName:消息队列名称(注意,同名称的消息队列,只能用同样的参数创建一次)。
durabale:消息队列重启后,消息是否不会丢失。
exclusive:是否只允许当前这个创建消息队列的连接操作消息队列。
autoDelete:没有人用队列后,是否要删除队列。
执行程序后,可以看到有一条消息:

消费者代码:
java
package com.yang.yangbi.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import java.nio.charset.StandardCharsets;
public class SingleConsumer {
public static final String QUEUE_NAME = "hello2";
public static void main(String[] args) throws Exception {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("49.232.17.169");
factory.setUsername("admin");
factory.setPassword("123456");
factory.setPort(5673);
factory.setConnectionTimeout(30000000);
factory.setHandshakeTimeout(30000000);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 创建队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C ");
// 定义了如何进行处理消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println(" [x] Received '" + message + "'");
};
// 消费消息, 会持续阻塞
channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
}
}
启动消费者后,可以看到消息被消费了:

5.2多消费者
官方教程:https://www.rabbitmq.com/tutorials/tutorial-two-java.html
场景:多个机器同时去接受并处理任务(尤其是每个机器的处理能力有限)。
一个生产者给一个队列发消息,多个消费者从这个队列取消息,一对多。

5.2.1队列持久化
durable参数设置为true,服务器重启后队列不丢失。
java
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
5.2.2消息持久化
指定MessageProperties.PERSISTENT_TEXT_PLAIN参数,服务器重启后队列中的消息不回丢失:
java
channel.basicPublish("", TASK_QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes("UTF-8"));
5.2.3生产者代码
使用Scanner接受用户输入,便于发送消息。
java
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
import java.util.Scanner;
public class MultiProducer {
public static final String TASK_QUEUE_NAME = "multi_queue_queue";
public static void main(String[] args) throws Exception {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("49.232.17.169");
factory.setUsername("admin");
factory.setPassword("123456");
factory.setPort(5673);
factory.setConnectionTimeout(30000000);
factory.setHandshakeTimeout(30000000);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String message = scanner.nextLine();
channel.basicPublish("", TASK_QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes("UTF-8"));
System.out.println("[x] Sent '" + message + "'");
}
}
}
5.2.4消费者代码优化方案
控制单个消费处理任务积压数
每个消费者最多处理一个任务
java
channel.basicQos(1);
消息确认机制:
为了保证消息成功被消费(快递成功被取走),RabbitMQ提供了消息确认机制,当消费者接收到消息之后,比如要给一个反馈:
ack:消费成功
nack:消费失败
reject:拒绝
只有告诉RabbitMQ服务器消费成功,服务器才会放心地移除消息。
支持配置autoack,会自动执行ack命令,接收到消息就立刻成功了。
但是还是建议将autoack改为false,根据实际情况,去手动确认。
java
channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> {});
指定确认某条消息
第一个参数是获取消息的唯一标识tag,第二个参数是是否要对当前消息以及之前的都发送ack确认消息,设置true就会对接收到的最后一个消息到之前的消息都进行ack确认,设置false就只会对现在获取到的消息进行ack。
java
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
指定拒绝某条消息:
第一个参数是获取消息的唯一标识tag。
第二个参数是是否要对当前消息以及之前的都发送nack确认消息,设置true就会对接收到的最后一个消息到之前的消息都进行nack确认,设置false就只会对现在获取到的消息进行nack。
第三个参数表示是否重新入队,可用于重试。
java
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
5.2.5消费者代码
java
package com.yang.yangbi.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
public class MultiConsumer {
public static final String TASK_QUEUE_NAME = "multi_queue";
public static void main(String[] args) throws Exception {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("49.232.17.169");
factory.setUsername("admin");
factory.setPassword("123456");
factory.setPort(5673);
factory.setConnectionTimeout(30000000);
factory.setHandshakeTimeout(30000000);
Connection connection = factory.newConnection();
for (int i = 0; i < 2; i++) {
final Channel channel = connection.createChannel();
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
channel.basicQos(1);
// 定义了如何处理消息
int finalI = i;
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
try {
// 处理工作
System.out.println(" [x] Received '" + "编号:" + finalI + ":" + message + "'");
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
// 暂停20秒, 模拟机器的处理能力有限
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
} finally {
System.out.println(" [x] Done ");
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
// 开启消费监听
channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> {});
}
}
}
5.2.6两个小技巧
1.使用Scanner接收用户的输入,便于快速发送多条消息
2.使用for循环创建多个消费者,便于快速验证队列模型工作机制。
5.3交换机
一个生产者给多个队列发送消息,一个生产者对多个队列。
交换机的作用:提供消息转发功能,类似网络路由器。
要解决的问题:怎么把消息转发到不同队列上,好让消费者从不同的队列消费?
绑定:交换机和队列关联起来,也可以叫路由,算是一个算法或者转发策略。
绑定代码
java
channel.queueBind(queueName, EXCHANGE_NAME, "绑定规则");

教程:RabbitMQ tutorial - Publish/Subscribe | RabbitMQ
交换机有很多种类别:fanout,direct,topic,headers
5.4fanout
扇出,广播
特点:消息会转发到所有绑定到盖交换机的队列
场景:很适用于发布订阅的场景。比如写日志,可以多个系统间共享。

示例场景:

5.4.1生产者代码
java
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.Scanner;
public class FanoutProducer {
public static final String EXCHANGE_NAME = "fanout-exchange";
public static void main(String[] args) throws Exception {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("49.232.17.169");
factory.setUsername("admin");
factory.setPassword("123456");
factory.setPort(5673);
factory.setConnectionTimeout(30000000);
factory.setHandshakeTimeout(30000000);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 创建交换机
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String message = scanner.nextLine();
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
5.4.2消费者代码
注意:
1.消费者和生产者要绑定同一个交换机
2.要现有队列,才能进行绑定
所以绑定交换机之间,必须进行创建队列
java
package com.yang.yangbi.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
public class FanoutConsumer {
public static final String EXCHANGE_NAME = "fanout-exchange";
public static void main(String[] args) throws Exception {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("49.232.17.169");
factory.setUsername("admin");
factory.setPassword("123456");
factory.setPort(5673);
factory.setConnectionTimeout(30000000);
factory.setHandshakeTimeout(30000000);
Connection connection = factory.newConnection();
Channel channel1 = connection.createChannel();
Channel channel2 = connection.createChannel();
// 声明交换机
channel1.exchangeDeclare(EXCHANGE_NAME, "fanout");
// 创建队列,随机分配一个队列名称
String queueName = "xiaowang_queue";
channel1.queueDeclare(queueName, true, false, false, null);
channel1.queueBind(queueName, EXCHANGE_NAME, "");
String queueName2 = "xiaoli_queue";
channel2.queueDeclare(queueName2, true, false, false, null);
channel2.queueBind(queueName2, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 获取到信息
DeliverCallback deliverCallback1 = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [小王] Received '" + message + "'");
};
DeliverCallback deliverCallback2 = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [小李] Received '" + message + "'");
};
// 监听队列进行消费
channel1.basicConsume(queueName, true, deliverCallback1, consumberTag -> {});
channel2.basicConsume(queueName2, true, deliverCallback2, consumberTag -> {});
}
}
效果:所有消费者都可以收到消息。
5.5Direct交换机
官方教程:RabbitMQ tutorial - Routing | RabbitMQ
绑定:可以让交换机及和队列进行关联,可以指定让交换机把什么样的消息发给哪个队列(类似于计算机网络中的两个路由器,或者网络设备相互连接,也可以理解为网线)
routingKey:路由键,控制消息要转发给哪个队列(IP地址)
特点:消息会根据路由键转发到指定的队列。
场景:特定的消息只交给特定的系统(程序)来处理
绑定关系:完全匹配字符串

可以绑定相同的路由键
比如发日志的场景,希望使用独立的程序来处理不同级别的日志,比如C1系统处理error日志,C2系统处理其他级别的日志

示例场景

5.5.1生产者
java
package com.yang.yangbi.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.Scanner;
public class DirectProducer {
public static final String EXCHANGE_NAME = "direct-exchange";
public static void main(String[] args) throws Exception {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("49.232.17.169");
factory.setUsername("admin");
factory.setPassword("123456");
factory.setPort(5673);
factory.setConnectionTimeout(30000000);
factory.setHandshakeTimeout(30000000);
Connection connection = factory.newConnection();
Channel channel1 = connection.createChannel();
// 声明交换机并进行使用
channel1.exchangeDeclare(EXCHANGE_NAME, "direct");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String userInput = scanner.nextLine();
String[] strings = userInput.split(" ");
if (strings.length < 1) {
continue;
}
String message = strings[0];
String routingKey = strings[1];
channel1.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");
}
}
}
5.5.2消费者
java
package com.yang.yangbi.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
public class DirectConsumer {
public static final String EXCHANGE_NAME = "direct-exchange";
public static void main(String[] args) throws Exception {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("49.232.17.169");
factory.setUsername("admin");
factory.setPassword("123456");
factory.setPort(5673);
factory.setConnectionTimeout(30000000);
factory.setHandshakeTimeout(30000000);
Connection connection = factory.newConnection();
Channel channel1 = connection.createChannel();
// 声明交换器
channel1.exchangeDeclare(EXCHANGE_NAME, "direct");
// 创建队列,随机分配一个队列名称
String queueName = "xiaoyu_queue";
channel1.queueDeclare(queueName, true, false, false, null);
channel1.queueBind(queueName, EXCHANGE_NAME, "xiaoyu");
// 创建队列,随机分配一个队列名称
String queueName2 = "xiaopi_queue";
channel1.queueDeclare(queueName2, true, false, false, null);
channel1.queueBind(queueName2, EXCHANGE_NAME, "xiaopi");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback xiaoyuDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [xiaoyu] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
DeliverCallback xiaopiDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [xiaopi] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel1.basicConsume(queueName, true, xiaoyuDeliverCallback, consumerTag -> {
});
channel1.basicConsume(queueName2, true, xiaopiDeliverCallback, consumerTag -> {
});
}
}
5.6topic交换机
官方教程:RabbitMQ tutorial - Topics | RabbitMQ
特点:消息会根据一个模糊的路由键转发到指定的队列
场景:特定的一类消息可以交给特定的一类系统(程序)来处理
绑定关系:可以模糊匹配多个绑定
*:匹配一个单词,比如*.orange,那么a.orange,b.orange都能匹配
#:匹配0个或者多个单词,比如a.#,那么a.a,a.b,a.a.a都能匹配
注意:这里的匹配和MySQL的like的%不一样,只能按照单个单词来进行匹,每个.进行分割单词,如果是 '#.',其实可以忽略,匹配 0 个词也 ok

应用场景:
老板要下发一个任务,让多个组进行处理

5.6.1生产者代码
java
package com.yang.yangbi.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.Scanner;
public class TopicProducer {
public static final String EXCHANGE_NAME = "topic-exchange";
public static void main(String[] args) throws Exception {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("49.232.17.169");
factory.setUsername("admin");
factory.setPassword("123456");
factory.setPort(5673);
factory.setConnectionTimeout(30000000);
factory.setHandshakeTimeout(30000000);
Connection connection = factory.newConnection();
Channel channel1 = connection.createChannel();
// 声明交换机
channel1.exchangeDeclare(EXCHANGE_NAME, "topic");
// 进行生产消息
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String userInput = scanner.nextLine();
String[] strings = userInput.split(" ");
if (strings.length < 1) {
continue;
}
String message = strings[0];
String routingKey = strings[1];
channel1.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");
}
}
}
5.6.2消费者代码
java
package com.yang.yangbi.mq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
public class TopicConsumer {
public static final String EXCHANGE_NAME = "topic-exchange";
public static void main(String[] args) throws Exception {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("49.232.17.169");
factory.setUsername("admin");
factory.setPassword("123456");
factory.setPort(5673);
factory.setConnectionTimeout(30000000);
factory.setHandshakeTimeout(30000000);
Connection connection = factory.newConnection();
Channel channel1 = connection.createChannel();
// 声明交换机
channel1.exchangeDeclare(EXCHANGE_NAME, "topic");
// 创建队列
String queueName = "frontend_queue";
channel1.queueDeclare(queueName, true, false, false, null);
channel1.queueBind(queueName, EXCHANGE_NAME, "#.前端.#");
// 创建队列
String queueName2 = "backend_queue";
channel1.queueDeclare(queueName2, true, false, false, null);
channel1.queueBind(queueName2, EXCHANGE_NAME, "#.后端.#");
// 创建队列
String queueName3 = "product_queue";
channel1.queueDeclare(queueName3, true, false, false, null);
channel1.queueBind(queueName3, EXCHANGE_NAME, "#.产品.#");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback xiaoaDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [xiaoa] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
DeliverCallback xiaobDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [xiaob] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
DeliverCallback xiaocDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [xiaoc] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel1.basicConsume(queueName, true, xiaoaDeliverCallback, consumerTag -> {
});
channel1.basicConsume(queueName2, true, xiaobDeliverCallback, consumerTag -> {
});
channel1.basicConsume(queueName3, true, xiaocDeliverCallback, consumerTag -> {
});
}
}
5.7Headers交换机
类似主题和直接交换机,可以根据headers中的内容来指定发送到哪个队列
由于性能比较差,比较复杂,一般不推荐使用。
5.8RPC
支持用消息队列来模拟RPC的调用,但是一般没必要,直接用Dubbo,GRPC等RPC框架就好了。
实现一个场景,总要有更合适的,更专注的技术。
6.核心特性
6.1消息过期机制
官方文档:Time-To-Live and Expiration | RabbitMQ
可以个i每条消息指定一个有效期,一段时间内违背消费者处理,就过期了。
示例场景:消息者(库存系统)挂了,一个订单15分钟还没被库存系统处理,这个订单其实已经失效了,哪怕库存系统再恢复,其实也不用扣减库存。
使用场景:清理过期数据,模拟延迟队列的实现(不开会员就慢速),专门让某个程序处理过期请求即可。
6.1.1给队列中的所有消息进行指定过期时间
时间的设置单位为毫秒
java
// 创建队列,指定消息过期参数
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 5000);
// args 指定参数
channel.queueDeclare(QUEUE_NAME, false, false, false, args);
如果在过期时间内,还没有消费者取消息,消息才会过去。
注意,如果消息已经被渠道,但是没确认,是不会过期的,消息会一直存在于队列中。
如果消息处于待消费状态并且过期时间到达后,消息将被标记为过期。但是,如果消息已经被消费者消费,并且正在处理过程中,即使过期时间到达,消息仍然会被正常处理。
6.1.1.1消费者代码
java
public class TtlConsumer {
private final static String QUEUE_NAME = "ttl_queue";
public static void main(String[] argv) throws Exception {
// 创建连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
// 创建队列,指定消息过期参数
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 5000);
// args 指定参数
channel.queueDeclare(QUEUE_NAME, false, false, false, args);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
// 定义了如何处理消息
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println(" [x] Received '" + message + "'");
};
// 消费消息,会持续阻塞
channel.basicConsume(QUEUE_NAME, false, deliverCallback, consumerTag -> { });
}
}
6.1.1.2生产者代码
java
public class TtlProducer {
private final static String QUEUE_NAME = "ttl_queue";
public static void main(String[] argv) throws Exception {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
// factory.setUsername();
// factory.setPassword();
// factory.setPort();
// 建立连接、创建频道
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
// 发送消息
String message = "Hello World!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
6.1.2给某条消息指定过期时间
java
// 给消息指定过期时间
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.expiration("1000")
.build();
channel.basicPublish("my-exchange", "routing-key", properties, message.getBytes(StandardCharsets.UTF_8));
实例代码
java
public class TtlProducer {
private final static String QUEUE_NAME = "ttl_queue";
public static void main(String[] argv) throws Exception {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
// factory.setUsername();
// factory.setPassword();
// factory.setPort();
// 建立连接、创建频道
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
// 发送消息
String message = "Hello World!";
// 给消息指定过期时间
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.expiration("1000")
.build();
channel.basicPublish("my-exchange", "routing-key", properties, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
6.2消息确认机制
官方文档:Consumer Acknowledgements and Publisher Confirms | RabbitMQ
为了保证消息成功被消费(快递成功被取走),RabbitMQ提供了消息确认机制,当消费者接收到消息后,要给一个反馈:
ack:消费成功
nack:消费失败
reject:拒绝
如果告诉RabbitMQ服务器消费成功,服务器才会放心地移除消息。
支持配置autoack,会自动执行ack命令,接收到消息立刻就成功了。
java
channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> {});
一般情况,建议autoack设置为false,根据实际情况,去手动进行确认。
指定确认某条消息:
java
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), );
第二个参数multiple批量确认:是指是否要一次性确认所有的历史消息直到这条
指定拒绝某条消息:
java
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
第三个参数标识是否重新入队,可用于重试。
6.3死信队列
官方文档:Dead Letter Exchanges | RabbitMQ
为了保证消息的可靠性,比如每条消息都成功消费,需要提供一给容错机制,即:失败的消息怎么处理?
死信:过期的消息,拒收的消息,消息队列满了,处理失败的消息的统称。
死信队列:专门处理死信的队列(注意,仅仅是一个普通的队列,只不过是专门用来进行处理死信的,你甚至可以理解这个队列名称叫"死信队列")
死信交换机:专门给死信队列转发消息的交换机(注意,它就是一个普通交换机,只不过是专门给死信队列发消息而已,理解为这个交换机的名称就叫"死信交换机")。也存在路由绑定。
死信可以通过死信交换机绑定到死信队列。
示例场景:

6.3.1如何实现死信队列?
1)创建死信交换机和死信队列,并且绑定关系

2)给失败之后需要容错处理的队列绑定死信交换机
示例代码:
java
// 指定死信队列参数
Map<String, Object> args = new HashMap<>();
// 要绑定到哪个交换机
args.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
// 指定死信要转发到哪个死信队列
args.put("x-dead-letter-routing-key", "waibao");
// 创建队列,随机分配一个队列名称
String queueName = "xiaodog_queue";
channel.queueDeclare(queueName, true, false, false, args);
channel.queueBind(queueName, EXCHANGE_NAME, "xiaodog");
3)可以给需要容错的队列指定死信之后的转发规则,死信应该再转发到哪个死信队列
java
// 指定死信要转发到哪个死信队列
args.put("x-dead-letter-routing-key", "waibao");
4)可以通过程序来读取死信队列中的消息,从而进行处理
java
// 创建队列,随机分配一个队列名称
String queueName = "laoban_dlx_queue";
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, DEAD_EXCHANGE_NAME, "laoban");
String queueName2 = "waibao_dlx_queue";
channel.queueDeclare(queueName2, true, false, false, null);
channel.queueBind(queueName2, DEAD_EXCHANGE_NAME, "waibao");
DeliverCallback laobanDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
// 拒绝消息
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
System.out.println(" [laoban] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
DeliverCallback waibaoDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
// 拒绝消息
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
System.out.println(" [waibao] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, false, laobanDeliverCallback, consumerTag -> {
});
channel.basicConsume(queueName2, false, waibaoDeliverCallback, consumerTag -> {
});
6.3.2生产者代码
生产者主要是进行创建死信队列和死信交换,并将信息发布到工作交换机。
java
public class DlxDirectProducer {
private static final String DEAD_EXCHANGE_NAME = "dlx-direct-exchange";
private static final String WORK_EXCHANGE_NAME = "direct2-exchange";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
// 声明死信交换机
channel.exchangeDeclare(DEAD_EXCHANGE_NAME, "direct");
// 创建队列,随机分配一个队列名称
String queueName = "laoban_dlx_queue";
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, DEAD_EXCHANGE_NAME, "laoban");
String queueName2 = "waibao_dlx_queue";
channel.queueDeclare(queueName2, true, false, false, null);
channel.queueBind(queueName2, DEAD_EXCHANGE_NAME, "waibao");
DeliverCallback laobanDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
// 拒绝消息
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
System.out.println(" [laoban] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
DeliverCallback waibaoDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
// 拒绝消息
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
System.out.println(" [waibao] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, false, laobanDeliverCallback, consumerTag -> {
});
channel.basicConsume(queueName2, false, waibaoDeliverCallback, consumerTag -> {
});
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String userInput = scanner.nextLine();
String[] strings = userInput.split(" ");
if (strings.length < 1) {
continue;
}
String message = strings[0];
String routingKey = strings[1];
channel.basicPublish(WORK_EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");
}
}
}
//..
}
6.3.3消费者代码
消费者主要进行创建工作队列,并且给工作队列绑定死信交换机,消费消息。
java
public class DlxDirectConsumer {
private static final String DEAD_EXCHANGE_NAME = "dlx-direct-exchange";
private static final String WORK_EXCHANGE_NAME = "direct2-exchange";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(WORK_EXCHANGE_NAME, "direct");
// 指定死信队列参数
Map<String, Object> args = new HashMap<>();
// 要绑定到哪个交换机
args.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
// 指定死信要转发到哪个死信队列
args.put("x-dead-letter-routing-key", "waibao");
// 创建队列,随机分配一个队列名称
String queueName = "xiaodog_queue";
channel.queueDeclare(queueName, true, false, false, args);
channel.queueBind(queueName, WORK_EXCHANGE_NAME, "xiaodog");
Map<String, Object> args2 = new HashMap<>();
args2.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
args2.put("x-dead-letter-routing-key", "laoban");
// 创建队列,随机分配一个队列名称
String queueName2 = "xiaocat_queue";
channel.queueDeclare(queueName2, true, false, false, args2);
channel.queueBind(queueName2, WORK_EXCHANGE_NAME, "xiaocat");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
DeliverCallback xiaoyuDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
// 拒绝消息
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
System.out.println(" [xiaodog] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
DeliverCallback xiaopiDeliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
// 拒绝消息
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
System.out.println(" [xiaocat] Received '" +
delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
};
channel.basicConsume(queueName, false, xiaoyuDeliverCallback, consumerTag -> {
});
channel.basicConsume(queueName2, false, xiaopiDeliverCallback, consumerTag -> {
});
}
}
7.RabbitMQ重点知识
也是面试的重点考点。
1.消息队列的概念,模型,应用场景
消息队列就是有生产者,消费者,生产者将信息推送到队列里,消费者去把从队列中取出,两者其他的事情并没有任何耦合的关系
模型有,一对一,一对多,交换机
一对一使用于,生产者生产出信息后,消费者系统去取信息,其实更加适用于一个系统对接另一个系统,不涉及多系统的
一对多适用于,比如老板发送一次信息,所有员工都能收到,或者一个庞大的系统进行推送了消息,其他子系统都要收到等
fanout交换机适用于,比如每个部门都有自己和老板进行沟通的队列,老板进行发布信息,fanout交换机可以同时推送到所有部门的队列中,直接可以通知到所有的部门
direct交换机可以进行指定一个routingKey,比如老板要给特定的部门进行通知,并且每个部门都有自己的队列存储老板的任务,老板可以根据routingKey的不同,进行去指定派送某任务通知某部门。
topic交换机可以指定一个模糊匹配的routingKey,比如老板要同时将消息派发给多个部门,所以可以通过模糊匹配,匹配到多个部门,进行发送消息
2.交换机的类别,路由的绑定关系
fanout(无路由)
direct(routingKey)
topic(模糊Key)
3.消息可靠性
1.消息的确认机制(ack,nack,reject)
2.消息持久化(durable)
3.消息过期机制
4.死信队列
4.延迟队列(类似死信队列)
5.顺序消费,消费幂等特性
6.可拓展性(仅作了解)
集群
故障的恢复机制
镜像
7.运维监控告警(仅作了解)