前言
首先,感谢学相伴的平台以及飞哥的知识分享。
本文的笔记整理于视频:【学相伴】RabbitMQ最新完整教程IDEA版通俗易懂。
代码可在Gitee上拉取: rabbitmq-demo。
本人才疏学浅,如果本文有错误之处,欢迎指正。
最后的碎碎念:
飞哥说的还是蛮不错的,认真听下去会很有收获的。我感觉飞哥的整体结构很清晰,有自己的理解成分在里面,就感觉看完之后就把知识点大纲理顺了,只不过讲述的知识点以及代码编写没有尚硅谷的那么详细。相应的,尚硅谷的就很乱,看了很蒙蔽。
个人推荐,跟B站尚硅谷发布的RabbitMQ 课件文档 1 食用更加,可以补充知识点。
为什么消息中间件采用的是http协议?
分布式事务;
消息的持久化;
高性能、高可靠的处理优势;
物联网的重要组成部分。
低延迟、低带宽、不支持事务
RocketMQ采用的协议。国内的阿里、雅虎等公司一起创作。
支持事务,持久化
基于TCP/IP协议,采用二进制进行传输。
结构简单,不支持事务,支持持久化
ActiveMQ | RabbitMQ | Kafka | RocketMQ | |
---|---|---|---|---|
发布订阅 | √ | √ | √ | √ |
轮询分发 | √ | √ | √ | |
公平分发 | √ | √ | ||
重发 | √ | √ | √ | |
消息拉取 | √ | √ | √ |
轮询分发、公平分发它们都是保证消息只能够读取一次。
轮询分发:每个消费者消费的消息总数量是一致的;
公平分发:能者多劳,消费者性能好,处理的请求就会比较多;必须手动应答,不支持自动应答
生产者将消息发送到主节点,所有的都节点连接这个消息队列共享这块的数据区域。主节点写入,一旦主节点挂掉,从节点继续服务。
与Redis的主从同步差不多
与2差不多,写入是可以任意节点进行写入。
元数据共享,当查找数据的时候,就会判断消息的元数据是否存在,存在则返回,否则就去问其他的消费者。
集群模式的总结
消息的传输:协议保证
消息的存储:持久化
流量消峰、应用解耦、异步处理
在说这个部分的时候,跟自己的业务结合一起去阐述三个场景。
首先可以进入RabbitMQ官网上查看 RabbitMQ Erlang版本要求
Linux安装视频:https://www.bilibili.com/video/BV1dX4y1V73G?p=9
Windows安装文章:https://www.cnblogs.com/saryli/p/9729591.html
Docker安装视频:https://www.bilibili.com/video/BV1dX4y1V73G?p=10
生产者、交换机、队列、消费者
队列可以没有交换机吗?
不可以。没有指明交换机的时候,有一个默认的AMQP default交换机绑定队列,且默认的交换机是路由模式。
RabbitMQ的七种模式,其中前五种一定要掌握
生产者:
public class Producer {
public static void main(String[] args) {
// 1. 创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 2. 创建链接
connection = connectionFactory.newConnection("生产者");
// 3. 通过链接获取通道
channel = connection.createChannel();
// 4. 通过通道,创建交换机、队列、绑定关系、路由key,发送消息以及接受消息
// 队列名,持久化,排他性,自动删除,携带额外的参数
// 将autoDelete设置为true ,当最后一个消费者消费完后并断开连接后 队列会自动进行删除
String queueName = "queue1";
channel.queueDeclare(queueName, false, false, false, null);
// 5. 准备消息内容
String message = "hello world";
// 6. 发送消息给队列
channel.basicPublish("", queueName, null, message.getBytes());
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}finally {
// 7. 关闭通道
if (channel!= null && channel.isOpen()){
try {
channel.close();
}catch (Exception ex){
ex.printStackTrace();
}
}
// 8. 关闭链接
if (connection!= null && connection.isOpen()){
try {
connection.close();
}catch (Exception ex){
ex.printStackTrace();
}
}
}
}
}
消费者:
public class Consumer {
public static void main(String[] args) {
// 1. 创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("localhost");
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
// 2. 创建链接
connection = connectionFactory.newConnection("生产者");
// 3. 通过链接获取通道
channel = connection.createChannel();
// 4.通过通道,创建交换机、队列、绑定关系、路由key,发送消息以及接受消息
// 队列名,持久化,排他性,自动删除,携带额外的参数
channel.basicConsume("queue1", true, new DeliverCallback() {
// 成功的接受信息处理
public void handle(String consumerTag, Delivery message) throws IOException {
System.out.println("收到的消息是:" + new String(message.getBody(), "utf-8"));
}
// 失败接受信息处理
}, new CancelCallback() {
public void handle(String consumerTag) throws IOException {
System.out.println("消息接受失败");
}
}
);
System.out.println("消息阻断完成");
System.in.read();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}finally {
// 7. 关闭通道
if (channel!= null && channel.isOpen()){
try {
channel.close();
}catch (Exception ex){
ex.printStackTrace();
}
}
// 8. 关闭链接
if (connection!= null && connection.isOpen()){
try {
connection.close();
}catch (Exception ex){
ex.printStackTrace();
}
}
}
}
}
/**
* 消息阻断
* 接收到的消息是:hello world
*/
持久化肯定是存入磁盘。但是非持久化也会存入内存,但是重启之后,会消失;
自动删除,是队列中的最后一个消息被消费时。
如果设置了自动删除,那么就会未持久化的队列就会自动删除。
如果没有设置自动删除,那么未持久化的队列还会存在,直到重启。
发布与订阅模式。
交换机为fanout类型,通过交换机,向他下面的队列都发送一样的消息。
指定路由key是没有意义的,仍然会所有的队列都会收到消息。
web图像化界面绑定:
消费者1:
// 声明该通道的名称以及类型,是否持久化:fanout类型 名称为faout-exchang,不持久化
String exchangeName = "faout-exchang";
String type = "fanout";
channel.exchangeDeclare(exchangeName, type,true );
// 声明队列:订阅者queue1
String queueName = "queue1";
// 队列名,持久化,排他性,自动删除,携带额外的参数
channel.queueDeclare(queueName, false, false, false, null);
// 临时队列绑定交换机,其中 routingkey(也称之为 binding key)为空字符串
channel.queueBind(queueName, exchangeName, "");
// 发送消息给队列
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
消费者2:
// 订阅该通道的名称以及类型:fanout类型 名称为faout-exchang
String exchangeName = "faout-exchang";
String type = "fanout";
channel.exchangeDeclare(exchangeName, type);
// 声明队列:订阅者queue2
String queueName = "queue2";
// 队列名,持久化,排他性,自动删除,携带额外的参数
channel.queueDeclare(queueName, false, false, false, null);
// 临时队列绑定交换机,其中 routingkey(也称之为 binding key)为空字符串
channel.queueBind(queueName, exchangeName, "");
// 发送消息给队列
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
生产者:
// 声明发布与订阅该通道的名称以及类型
String exchangeName = "faout-exchang";
// 声明该通道的名称以及类型:fanout
channel.exchangeDeclare(exchangeName, "fanout");
// 声明该通道的交换机的类型
channel.basicPublish("faout-exchang", "", null, "hello world".getBytes());
路由模式。
空的交换机有默认交换机,direct模式。
direct类型的交换机通过队列设置不同的 Routing key ,来接受不同的消息。
交换机在发消息的时候,通过指定的不同的 Routing key ,来转发到指定的 Routing key的队列。
消费者1:
// 声明该通道的名称以及类型
String exchangeName = "direct-exchange";
String type = "direct";
channel.exchangeDeclare(exchangeName, type);
// 声明队列
String queueName = "queue1";
channel.queueDeclare(queueName, false, false, false, null);
// 临时队列绑定交换机,其中 routingkey(也称之为 binding key)
String routingkey = "error";
channel.queueBind(queueName, exchangeName, routingkey);
// 发送消息给队列
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
消费者2:
// 声明该通道的名称以及类型
String exchangeName = "direct-exchange";
String type = "direct";
channel.exchangeDeclare(exchangeName, type);
// 声明队列
String queueName = "queue2";
channel.queueDeclare(queueName, false, false, false, null);
// 临时队列绑定交换机,其中 routingkey(也称之为 binding key)
String routingkey1 = "warning";
String routingkey2 = "info";
channel.queueBind(queueName, exchangeName, routingkey1);
channel.queueBind(queueName, exchangeName, routingkey2);
// 发送消息给队列
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
生产者:
// 声明该通道的名称以及类型
String exchangeName = "direct-exchange";
String type = "direct";
String routingkey = "warning";// "info","error"
// 声明该通道的名称以及类型
channel.exchangeDeclare(exchangeName, type);
// 声明该通道的交换机的类型
channel.basicPublish(exchangeName, routingkey, null, "hello world".getBytes());
主题模式
设置交换机为topic类型,通过Routing key模式匹配来分发队列里面的消息。
* : 代表着必须有1级别
*.orange.* 代表:前面有1级,orange,后面有1级,
Q2:
*.*.rabbit代表:前面有1级,中间有1级,rabbit
lazy.# 代表:lazy后面可以有0个或者多个级别都是匹配的
测试:
com.lazy.orange : 没人收到消息
lazy.orange:Q2 收到消息,消息是匹配于lazy.#
lazy.orange.rabbit.com:Q2收到消息,消息是匹配于lazy.#
lazy.orange.rabbit:Q1 以及 Q2 均收到消息
消费者1
// 声明该通道的名称以及类型
String exchangeName = "headers-exchange";
String type = "headers";
channel.exchangeDeclare(exchangeName, type);
// 声明队列
String queueName = "queue2";
channel.queueDeclare(queueName, false, false, false, null);
// 临时队列绑定交换机,其中 routingkey(也称之为 binding key)
String routingkey = "*.orange.*";
channel.queueBind(queueName, exchangeName, routingkey);
// 发送消息给队列
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
消费者2
// 声明该通道的名称以及类型
String exchangeName = "topic-exchange";
String type = "topic";
channel.exchangeDeclare(exchangeName, type);
// 声明队列
String queueName = "queue2";
channel.queueDeclare(queueName, false, false, false, null);
// 临时队列绑定交换机,其中 routingkey(也称之为 binding key)
String routingkey1 = "*.*.rabbit";
String routingkey2 = "lazy.#";
channel.queueBind(queueName, exchangeName, routingkey1);
channel.queueBind(queueName, exchangeName, routingkey2);
// 发送消息给队列
channel.basicConsume(queueName,true,deliverCallback,cancelCallback);
生产者:
// 声明该通道的名称以及类型
String exchangeName = "topic-exchange";
String type = "topic";
String routingkey = "com.lazy.orange";// "lazy.orange"....
// 声明该通道的名称以及类型
channel.exchangeDeclare(exchangeName, type);
// 声明该通道的交换机的类型
channel.basicPublish(exchangeName, routingkey, null, "hello world".getBytes());
交换机为Headers模式,队列设置不同的参数。
在发送消息的时候,通过不同的参数来进行消息转达到不同的队列。
To | Routing key | Arguments |
---|---|---|
queue1 | x:1 y:1 | |
queue2 | x:1 | |
queue3 | x:2 y:1 |
x=1,Q2收到了
x=1,y=1,Q1、Q2都收到了
x=2,都没有队列匹配
消费者
// 声明该通道的名称以及类型
String exchangeName = "topic-exchange";
String type = "topic";
channel.exchangeDeclare(exchangeName, type);
// 声明队列
String queueName = "queue1";
channel.queueDeclare(queueName, false, false, false, null);
// 临时队列绑定交换机,其中 routingkey(也称之为 binding key)
String routingkey = "";
channel.queueBind(queueName, exchangeName, "");
// queue1的参数组
Map<String, Object> header = new HashMap<String, Object>();
header.put("x-match", "all"); //x-match: all表所有key-value全部匹配才匹配成功 ,any表只需要匹配任意一个key-value 即匹配成功。
header.put("name", "张三");
header.put("idcard","123321");
// 消息消费
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(message);
}
};
channel.basicConsume(queueName, true, consumer);
生产者:
// 声明该通道的名称以及类型
String exchangeName = "topic-exchange";
String type = "topic";
// 声明该通道的名称以及类型
channel.exchangeDeclare(exchangeName, type);
// 创建参数组
Map<String, Object> header = new HashMap<String, Object>();
header.put("name", "张三");
header.put("idcard","123321");
header.put("phone","18888888888");
AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties().builder().headers(header);
// 发送消息
String message = "Hello headers消息!";
channel.basicPublish(exchangeName, "", properties.build(), message.getBytes("UTF-8"));
生产者:
public class Task01 {
// 队列名称
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 默认消息存储在内存中
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String message = scanner.next();
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("发送消息完毕:" + message);
}
}
}
消费者
public class Work01 {
// 队列名称
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
// 获取连接通道
Channel channel = RabbitMqUtils.getChannel();
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("接收到的消息:"+ new String(message.getBody()));
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println("消费消息被中断");
};
System.out.println("消费者A等待接受消息。。。。。。。。");
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
启动 Work01 ,看到控制台打印出信息:
如何用Idea来模拟,有多条消费者呢?(我的Idea版本为:Idea2020.3)
结果
两位消费者循环的消费了生产者的消息。
默认情况下,RabbitMQ 会按顺序将每条消息发送给下一个消费者。平均而言,每个消费者都会收到相同数量的消息。
消费者必须设置为手动应答机制,且设置参数 channel.basicQos(1);
int prefetchCount = 1;
channel.basicQos(prefetchCount );
意思:如果这个任务我还没有处理完或者我还没有应答你,你先别分配给我,我目前只能处理1个任务,然后 rabbitmq 就会把该任务分配给没有那么忙的那个空闲消费者。
当然如果所有的消费者都没有完成手上任务,队列还在不停的添加新任务,队列有可能就会遇到队列被撑满的情况,这个时候就只能添加新的 worker 或者改变其他存储任务的策略。
导入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
创建生产者module:springboot-rabbitmq-producer,启动类为ProducerApplication
创建消费者module:springboot-rabbitmq-comsumer,启动类为ConsumerApplication
FanoutRabbitMqConfig
: 声明交换机、队列、以及他们的绑定。
(这个配置类也可以在消费者中复制一份)
(补:建议在消费者这边,因为消费者直接跟队列进行交互)
@Configuration
public class FanoutRabbitMqConfig {
// 1. 声明fanout交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("fanout_order_exchange",true,false);
}
// 2. 声明队列
@Bean
public Queue smsQueue(){
return new Queue("sms.fanout.queue",true) ;
}
@Bean
public Queue emailQueue(){
return new Queue("email.fanout.queue",true) ;
}
@Bean
public Queue wechatQueue(){
return new Queue("wechat.fanout.queue",true) ;
}
// 3. 交换机与队列之间的绑定
@Bean
public Binding smsBinding(){
return BindingBuilder.bind(emailQueue()).to(fanoutExchange());
}
@Bean
public Binding emailBinding(){
return BindingBuilder.bind(smsQueue()).to(fanoutExchange());
}
@Bean
public Binding wechatBinding(){
return BindingBuilder.bind(wechatQueue()).to(fanoutExchange());
}
}
OrderService
: 业务处理类
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 模拟用户进行商品的下单
*
* @param userId 用户id
* @param goodsId 商品id
* @param num 数量
* @date 2022/4/2 14:02
*/
public void makeOrder(String userId,String goodsId, int num){
String orderId = UUID.randomUUID().toString();
System.out.println("订单生成成功:" + orderId);
// MQ实现消息的转发
String exchangeName = "fanout_order_exchange";
String routingKey = "";
rabbitTemplate.convertAndSend(exchangeName,routingKey,orderId);
}
}
FanoutEmailConsumer
:接受email.fanout.queue的消息
// 绑定的队列名
@RabbitListener(queues = {"email.fanout.queue"})
@Component
public class FanoutEmailConsumer {
// 处理收到的消息
@RabbitHandler
public void receiveMessage(String message){
System.out.println("email fanout 接受的订单信息为:" + message);
}
}
FanoutSmsConsumer
:接受sms.fanout.queue的消息
@RabbitListener(queues = {"sms.fanout.queue"})
@Component
public class FanoutSmsConsumer {
@RabbitHandler
public void receiveMessage(String message){
System.out.println("sms fanout 接受的订单信息为:" + message);
}
}
FanoutWechatConsumer
:接受wechat.fanout.queue的消息,与上述的代码类似。
因为生产队列以及交换机的方法,在生产模块里面,所以先启动生产模块。
创建测试类,调用生产者的业务类,生产消息。
@Test
void testFanout(){
orderService.makeOrder("1","1",12);
}
其中,控制台的打印如下:
运行消费者启动类ConsumerApplication
,准备接受消息。
DirectRabbitMqConfig
: 声明交换机、队列、以及他们的绑定关系和routing key
@Configuration
public class DirectRabbitMqConfig {
// 1. 声明fanout交换机
@Bean
public DirectExchange directExchange(){
return new DirectExchange("direct_order_exchange",true,false);
}
// 2. 声明队列
@Bean
public Queue smsDirectQueue(){
return new Queue("sms.direct.queue",true) ;
}
@Bean
public Queue emailDirectQueue(){
return new Queue("email.direct.queue",true) ;
}
@Bean
public Queue wechatDirectQueue(){
return new Queue("wechat.direct.queue",true) ;
}
// 3. 交换机与队列之间的绑定
@Bean
public Binding smsDirectBinding(){
return BindingBuilder.bind(smsDirectQueue()).to(directExchange()).with("sms");
}
@Bean
public Binding emailDirectBinding(){
return BindingBuilder.bind(emailDirectQueue()).to(directExchange()).with("email");
}
@Bean
public Binding wechatDirectBinding(){
return BindingBuilder.bind(wechatDirectQueue()).to(directExchange()).with("wechat");
}
}
在OrderService
里面,创建方法 makeOrderDirect。
/**
* 路由模式模拟用户下单
*
* @param userId 用户id
* @param goodsId 商品id
* @param num 数量
* @param routingKey 路由key
* @date 2022/4/2 16:36
*/
public void makeOrderDirect(String userId,String goodsId, int num, String routingKey ){
String orderId = UUID.randomUUID().toString();
String message = "订单生成成功 : " + orderId + " ,userId : " + userId + ",goodsId : "+ goodsId + ", num : " + num;
System.out.println(message);
// MQ实现消息的转发
String exchangeName = "direct_order_exchange";
rabbitTemplate.convertAndSend(exchangeName,routingKey,message);
}
DirectEmailConsumer
:接受email.direct.queue的消息
@RabbitListener(queues = {"email.direct.queue"})
@Component
public class DirectEmailConsumer {
@RabbitHandler
public void receiveMessage(String message){
System.out.println("email direct 接受的订单信息为:" + message);
}
}
DirectSmsConsumer
:接受 sms.direct.queue 的消息。与上述代码类似。
DirectWechatConsumer
:接受 wechat.direct.queue 的消息。与上述代码类似。
因为生产队列以及交换机的方法,在生产模块里面,所以先启动生产模块。
创建测试方法,测试生产业务类的 makeOrderDirect() 方法:
@Test
void testDirect(){
orderService.makeOrderDirect("12","12",12,"email");
orderService.makeOrderDirect("22","22",22,"sms");
}
其中,控制台打印的方法如下:
运行消费者启动类ConsumerApplication
,准备接受消息。
PS:上述代码是使用配置类的方式来实现交换机与队列之间的绑定。
除此之外,还有一种是基于注解来实现交换机与队列之间的绑定关系。
建议使用配置类的方式进行统一的管理以及维护。
该类型的演示,就使用注解来演示:
在消费者这边,修改原来的@RabbitListener
,在这个注解里面,表明当前队列的参数,绑定的交换机等信息
TopicWechatConsumer
@Component
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "wechat.topic.queue",durable = "true",autoDelete = "false"),
exchange = @Exchange(value = "topic.order.exchange", type = ExchangeTypes.TOPIC),
key = "com.#"
))
public class TopicWechatConsumer {
@RabbitHandler
public void receiveMessage(String message){
System.out.println("wechat topic 接受的订单信息为:" + message);
}
}
TopicEmailConsumer
:修改路由key以及队列的名称
@Component
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "email.topic.queue",durable = "true",autoDelete = "false"),
exchange = @Exchange(value = "topic.order.exchange", type = ExchangeTypes.TOPIC),
key = "*.email.#"
))
public class TopicEmailConsumer {
@RabbitHandler
public void receiveMessage(String message){
System.out.println("email topic 接受的订单信息为:" + message);
}
}
TopicWechatConsumer
与上述绑定队列与交换机的方法相同,其中路由key为 com.#
在启动并运行消费者,查看RabbitMQ的可视化界面。如下图所示,表明已经实现创建路由器以及队列并实现了他们的绑定。
/**
* 主题模式模拟用户下单
*
* @param userId 用户id
* @param goodsId 商品id
* @param num 数量
* @param routingKey 路由key
* @date 2022/4/2 16:36
*/
public void makeOrderTopic(String userId,String goodsId, int num, String routingKey ){
String orderId = UUID.randomUUID().toString();
String message = "订单生成成功 : " + orderId + " ,userId : " + userId + ",goodsId : "+ goodsId + ", num : " + num;
System.out.println(message);
// MQ实现消息的转发
String exchangeName = "topic.order.exchange";
rabbitTemplate.convertAndSend(exchangeName,routingKey,message);
}
因为生产队列以及交换机的方法,在生产模块里面,所以先启动生产模块。
创建测试方法testTopic()
,测试生产业务类的 makeOrderDirect() 方法,并Debug来启动 testTopic()
@Test
void testTopic(){
/*
* userId = 12,wechat、email收到
* userId = 22,wechat、email、sms全部都会收到
*/
orderService.makeOrderTopic("12","12",12,"com.email");
orderService.makeOrderTopic("22","22",22,"com.email.sms");
}
其中,控制台打印的方法如下:
此刻,消费者接受消息的情况如下所示,与预期一致。
设置消息的预期时间,只有在这个时间段之内,消息才能够被消费者接受。过了时间之后,消息就会被自动删除。
总共有两种方式设置过期时间:队列和消息。
队列设置的话,则放入队列里面的消息有过期时间。
消息设置的话,则是单独对消息进行设置。
如果在TTL队列里面,单独对消息设置了过期时间。
那么,队列的过期时间与消息的过期时间哪一个更短,就以哪一个过期时间为准。
消息在TTL队列里面,一旦超过了设置的TTL值。那么,就被称为dead message,会被投递到死信队列,消费者将无法再收到消息。(TTL消息过期,则移除)
综上所述,我们在开发中,一般使用的就是TTL队列。
使用配置类进行交换机与队列之间的绑定,此处采用的是主题模式的交换机。
其中,配置类如下所示:
@Configuration
public class RabbitmqConfig {
@Bean
public DirectExchange directTtlExchange() {
return new DirectExchange("direct_ttl_exchange", true, false);
}
@Bean
public Queue directTtlQueue() {
// 设置队列参数中的的过期时间,单位毫秒,且为整型
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 5000);
return new Queue("direct_ttl_queue", true,false,false,args);
}
@Bean
public Binding directTtlBinding(){
return BindingBuilder.bind(directTtlQueue()).to(directTtlExchange()).with("ttl");
}
}
模拟用户在往TTL的队列进行发送消息
@Test
void testTtlQueue(){
orderService.makeTtlDirect("12","12",12,"ttl");
}
队列业务类处理。
/**
* 路由模式来测试过期时间TTL:设置队列的过期时间
*
* @param userId 用户id
* @param goodsId 商品id
* @param num 数量
* @param routingKey 路由key
* @date 2022/4/2 16:36
*/
public void makeTtlDirect(String userId, String goodsId, int num, String routingKey) {
String orderId = UUID.randomUUID().toString();
String message = "订单生成成功 : " + orderId + " ,userId : " + userId + ",goodsId : " + goodsId + ", num : " + num;
System.out.println(message);
// MQ实现消息的转发
String exchangeName = "direct_ttl_exchange";
rabbitTemplate.convertAndSend(exchangeName, routingKey, message);
}
判断TTL队列是否设置成功:
查看队列的特征:
点击队列里面,查看设置了多少的过期时间(毫秒):
配置类指定交换机以及队列,和他们的绑定关系。
@Configuration
public class TtlMessageRabbitmqConfig {
@Bean
public DirectExchange directTtlMessageExchange() {
return new DirectExchange("direct_ttl_exchange", true, false);
}
@Bean
public Queue directTtlMessageQueue() {
return new Queue("direct_ttl_message_queue", true, false, false);
}
@Bean
public Binding directTtlMessageBinding() {
return BindingBuilder.bind(directTtlMessageQueue()).to(directTtlMessageExchange()).with("ttl_message");
}
}
业务类,设置消息的过期时间。
/**
* 路由模式来测试过期时间TTL:单独设置消息的过期时间
*
* @param userId 用户id
* @param goodsId 商品id
* @param num 数量
* @param routingKey 路由key
* @date 2022/4/2 16:36
*/
public void makeTtlMessageDirect(String userId, String goodsId, int num, String routingKey) {
String orderId = UUID.randomUUID().toString();
String message = "订单生成成功 : " + orderId + " ,userId : " + userId + ",goodsId : " + goodsId + ", num : " + num + routingKey;
System.out.println(message);
String exchangeName = "direct_ttl_message_exchange";
// 设置消息的过期时间
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// 设置消息的过期时间
message.getMessageProperties().setExpiration("5000");
// 此处,还可以设置消息的编码等。
return message;
}
};
rabbitTemplate.convertAndSend(exchangeName, routingKey, message, messagePostProcessor);
}
测试类来模拟用户进行业务处理。
@Test
void testTtlMessage(){
orderService.makeTtlMessageDirect("12","12",12,"ttl_message");
}
DLX,全称为Dead-Letter-Exchange,可以称呼为死信交换机,死信邮箱。
当消息在一个队列里面变为死信。那么,它就能够被重新发送到另一个交换机中:DLX。
绑定DLX的队列称之为死信队列。
消息变成死信的原因如下:
@Configuration
public class DeadRabbitmqConfig {
@Bean
public DirectExchange deadDirectExchange(){
return new DirectExchange("direct_dead_exchange",true,false);
}
@Bean
public Queue deadQueue(){
return new Queue("direct_dead_queue",true,false,false);
}
@Bean
public Binding deadBinding(){
return BindingBuilder.bind(deadQueue()).to(deadDirectExchange()).with("dead_message");
}
}
@Configuration
public class TtlQueueRabbitmqConfig {
@Bean
public DirectExchange directTtlExchange() {
return new DirectExchange("direct_ttl_exchange", true, false);
}
@Bean
public Queue directTtlQueue() {
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 5000);
// 设置队列的存放消息的最大长度
args.put("x-max-length", 5);
// 消息过期后绑定的死信队列是:direct_dead_exchange
args.put("x-dead-letter-exchange", "direct_dead_exchange");
// 因为死信队列是direct模式。通过 routing key,配置要发送给死信队列的哪一个队列。
// 如果是fanout模式,则不需要配置 routing key
args.put("x-dead-letter-routing-key", "dead_message");
return new Queue("direct_ttl_queue", true, false, false, args);
}
@Bean
public Binding directTtlBinding() {
return BindingBuilder.bind(directTtlQueue()).to(directTtlExchange()).with("ttl");
}
}
@Test
void testTtlQueue(){
orderService.makeTtlDirect("12","12",12,"ttl");
}
public void makeTtlDirect(String userId, String goodsId, int num, String routingKey) {
String orderId = UUID.randomUUID().toString();
String message = "订单生成成功 : " + orderId + " ,userId : " + userId + ",goodsId : " + goodsId + ", num : " + num;
System.out.println(message);
// MQ实现消息的转发
String exchangeName = "direct_ttl_exchange";
rabbitTemplate.convertAndSend(exchangeName, routingKey, message);
}
@Component
public class DeadConsumer {
/**
* 监听dead_order_cancel_queue队列
* @param orderMessage
* @param channel
* @param correlationData
* @param tags
*/
@RabbitListener(queues = {"direct_dead_queue"})
public void messageOnDeadOrderCancelQueue(String orderMessage, Channel channel, CorrelationData correlationData,
@Header(AmqpHeaders.DELIVERY_TAG) long tags) throws IOException {
try {
System.out.println("======== 监听direct_dead_queue队列的消息 =========");
System.out.println("======== 死信消息:"+ orderMessage + ",当前的时间为:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));;
// 省略数据库更新订单信息
// 手动ack
System.out.println("======== 死信队列已经成功处理完消息");;
channel.basicAck(tags, false);
} catch (IOException e) {
// 当前死信队列处理消息出错
System.out.println("当前死信队列处理消息出错, 把消息存入数据库");
System.out.println("当前死信队列处理消息出错, 给运维发短信");
System.out.println("错误信息为:" + e.getMessage());
// 丢弃消息,但是由于当前队列并未绑定死信队列,所以直接丢弃
channel.basicNack(tags,false,false);
}
}
}
这里指的是消息有三种状态0,1,2。0代表消息未发送成功,1代表消息发送成功,2代表消息发送异常无法再发送
准备工作
新建goods_order
(订单)表以及 order_message
(订单冗余表)。其DDL语句如下:
goods_order
-- auto-generated definition
create table goods_order
(
order_id varchar(20) null,
user_id varchar(20) null,
order_content varchar(20) null,
create_time datetime null
);
order_message
-- auto-generated definition
create table order_message
(
order_id varchar(20) null comment '订单id',
order_status varchar(20) null comment '0代表消息未发送成功,1代表消息发送成功,2代表消息发送异常无法再发送
',
order_content varchar(20) null comment ' 订单内容',
unique_id varchar(20) null
);
application.yaml
server:
port: 8000
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/order_test?serverTimezone=GMT%2b8
username: root
password: 123456
rabbitmq:
password: guest
username: guest
# 单机
host: localhost
port: 5672
# 集群
# addresses: 127.0.0.1:5672
virtual-host: /
listener:
simple:
acknowledge-mode: manual
retry:
# 开启手动ack,让程序去控制MQ消息的重发、删除、转移
enabled: true
# 最大重试次数
max-attempts: 10
# 重试间隔时间
initial-interval: 2000ms
# 开启ack确认机制
publisher-confirm-type: correlated
#mybatis-plus
mybatis-plus:
#配置Mapper映射文件
mapper-locations: classpath:/mappers/*.xml
# 配置Mybatis数据返回类型别名(默认别名为类名)
type-aliases-package: com.hanliy.pojo
configuration:
# 自动驼峰命名
map-underscore-to-camel-case: true
# 打印分析日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
pom.xml
中的依赖如下:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.2version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.7.19version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
业务代码
RabbitmqConfig
:配置交换机以及队列之间的绑定
order_fanout_exchange - > order_queue — (死信队列)----> dead_order_fanout_exchange -> dead_order_queue
@Configuration
public class RabbitmqConfig {
@Bean
public FanoutExchange deadOrderFanoutExchange() {
return new FanoutExchange("dead_order_fanout_exchange", true, false);
}
@Bean
public Queue deadOrderQueue() {
return new Queue("dead_order_queue", true, false, false);
}
@Bean
public Binding bindDeadOrder() {
return BindingBuilder.bind(deadOrderQueue()).to(deadOrderFanoutExchange());
}
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("order_fanout_exchange", true, false);
}
@Bean
public Queue orderQueue() {
Map<String, Object> args = new HashMap<>();
// 消息过期后绑定的死信队列是:dead_order_fanout_exchange
args.put("x-dead-letter-exchange", "dead_order_fanout_exchange");
return new Queue("order_queue", true, false, false, args);
}
@Bean
public Binding bindOrder() {
return BindingBuilder.bind(orderQueue()).to(fanoutExchange());
}
}
OrderController
:控制层
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("mq")
public void OrderCreateMq() throws Exception {
Order order = new Order();
order.setOrderId("1001");
order.setUserId("1001");
order.setOrderContent("测试数据");
order.setCreateTime(new Date());
orderService.saveOrder(order);
System.out.println("订单创建成功");
}
}
OrderService
:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private OrderMessageMapper orderMessageMapper;
@Autowired
private OrderMqService orderMqService;
@Transactional(rollbackFor = Exception.class)
public void saveOrder(Order order) throws Exception {
// 订单处理
orderMapper.insert(order);
// 消息冗余
OrderMessage message = new OrderMessage();
message.setOrderId(order.getOrderId());
message.setOrderContent(order.getOrderContent());
message.setOrderStatus("0");
message.setUniqueId("1");
orderMessageMapper.insert(message);
// mq进行消息确认处理
orderMqService.sendMessage(order);
}
}
OrderMqService
: MQ的信息发送以及ack回调处理
@Service
public class OrderMqService {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private OrderMessageMapper orderMessageMapper;
@PostConstruct
public void regCallback() {
// 消息发送成功后,给与生产者的消息回执,来确保生产者可靠性
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("cause" + cause);
// 如果ack为true,代表消息已经收到
String orderId = correlationData.getId();
// 如果消息中间件收到了消息,那么就是true
if (!ack) {
// 这里可能要进行其他方式进行存储
System.out.println("MQ队列消息应答失败,orderId为: " + orderId);
return;
}
try {
OrderMessage orderMessage = new OrderMessage();
orderMessage.setOrderId(orderId);
orderMessage.setOrderStatus("1");
int update = orderMessageMapper.update(orderMessage, null);
if (update == 1) {
System.out.println("本地消息修改成功,消息成功投递到队列中");
}
} catch (Exception e) {
System.out.println("本地消息修改失败,出现异常:" + e.getMessage());
}
}
});
}
public void sendMessage(Order order) {
rabbitTemplate.convertAndSend("order_fanout_exchange", "", JSONUtil.toJsonStr(order)
, new CorrelationData(order.getOrderId()));
}
}
OrderTask
: 定时扫描未发送成功的信息
@EnableScheduling
public class OrderTask {
@Autowired
private RabbitTemplate rabbitTemplate;
@Scheduled(cron = "")
public void sendMessage() {
/**
* 把状态为0的订单重新发送到MQ中
*/
// 假设这是状态为0的订单信息
List<Order> orders = null;
// 重新发送MQ
for (Order order : orders) {
// 重新发送到MQ中
rabbitTemplate.convertAndSend("order_fanout_exchange", "", JSONUtil.toJsonStr(order)
, new CorrelationData(order.getOrderId()));
}
}
}
结果
运行结果如下所示:
在RabbitMQ中的可视化界面中,可以看到有一条待消费的信息:
前期准备
dispatcher
: 运单中心表
-- auto-generated definition
create table dispatcher
(
dispatch_id varchar(20) null comment '派送者id',
order_id varchar(20) null comment '订单id',
order_status int null comment '0待派送',
order_content varchar(20) null comment '订单内容',
create_time datetime null comment '创建时间',
user_id varchar(20) null comment '购买人'
);
application.yaml
server:
port: 9000
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/dispatch_test?serverTimezone=GMT%2b8
username: root
password: 123456
rabbitmq:
password: guest
username: guest
# 单机
host: localhost
port: 5672
# 集群
# addresses: 127.0.0.1:5672
virtual-host: /
listener:
# fanout模式的重试
simple:
acknowledge-mode: manual
retry:
# 开启手动ack,让程序去控制MQ消息的重发、删除、转移
enabled: true
# 最大重试次数
max-attempts: 10
# 重试间隔时间
initial-interval: 2000ms
#mybatis-plus
mybatis-plus:
#配置Mapper映射文件
mapper-locations: classpath:/mappers/*.xml
# 配置Mybatis数据返回类型别名(默认别名为类名)
type-aliases-package: com.hanliy.pojo
configuration:
# 自动驼峰命名
map-underscore-to-camel-case: true
# 打印分析日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
核心类
OrderMqConsumer
:监听生产者发送队列:order_queue。
这里有一个报错信息。模拟异常情况的发送,使得消息变为死信,再进行处理。
@Service
public class OrderMqConsumer {
@Autowired
private DispatcherService dispatcherService;
private int num = 1;
@RabbitListener(queues = {"order_queue"})
public void messageConsumer(String orderMessage, Channel channel, CorrelationData correlationData,
@Header(AmqpHeaders.DELIVERY_TAG) long tags) throws IOException {
try{
// 1. 获取到的消息队列的消息
System.out.println("收到的MQ的消息是:" + orderMessage + ", num = " + num++ );
OrderDTO orderDTO = JSONUtil.toBean(orderMessage, OrderDTO.class);
// 2. 派单处理
dispatcherService.save(orderDTO);
// 模拟异常
int errorMessage = 1/0;
// 执行手动ack
channel.basicAck(tags,false);
}catch (Exception e){
// 无法执行手动ack
/**
* 出现异常的情况下,根据实际的情况进行重发
* 重发一次后,丢失还是记录、存库。根据自己的业务去决定。
*
* @param tags 收到的来自 AMQP.Basic.GetOk or AMQP.Basic.Deliver标签
* @param multiple true,拒绝提供的递送标签之前的所有邮件;false仅拒绝提供的交付标签。
* @param requeue 如果被拒绝的消息应该被重新查询而不是丢弃,则为true
*/
channel.basicNack(tags,false,false);
}
}
}
DeadMqConsumer
:监听处理死信的队列:dead_order_queue。
如果在这里还出现异常,就手动进行干预。禁止套娃。
@Service
public class DeadMqConsumer {
@RabbitListener(queues = {"dead_order_queue"})
public void messageConsumer(String orderMessage, Channel channel, CorrelationData correlationData,
@Header(AmqpHeaders.DELIVERY_TAG) long tags) throws IOException {
try{
// 1. 获取到的消息队列的消息
System.out.println("进入死信处理,收到的MQ的消息是:" + orderMessage );
OrderDTO orderDTO = JSONUtil.toBean(orderMessage, OrderDTO.class);
// 2. 派单处理
// 确保消息不被重复消费,1. 可以设置orderId为主键 2. 使用分布式锁来解决
dispatcherService.save(orderDTO);
// 执行手动ack:会重发消息,直到重发次数耗尽?
channel.basicAck(tags,false);
// int errorMessage = 1/0;
}catch (Exception e){
// 人工干预
System.out.println("等待人工处理。。。。");
System.out.println("发短信预警");
System.out.println("把消息转移给数据库存储");
// 丢弃消息
channel.basicNack(tags,false,false);
}
}
}
再启动消费者服务,查看。消息会因为异常,重试后,发送到指定的队列中等待处理。
再处理完了死信以后,web管理界面如下:
默认情况下,RabbitMQ使用内存超过40%的时候,会发出内存警告,阻塞所有发布消息的连接,一旦警告解除(例如:服务器paging消息到硬盘或者分发消息到消费者并且确认)服务会恢复正常。
RabbitMQ配置详解:https://www.rabbitmq.com/configure.html
rabbitmqctl set_vm_memory_high_watermark <fraction>
rabbitmqctl set_vm_memory_high_watermark absolute 50MB
为内存阈值,默认是0.4/2GB。
通过命令行修改阈值在Broker重启之后,会失效。
通过配置文件修改阈值不会随着重启而消息,需要重启Broker才会生效。
此处,调小了磁盘的大小来演示内存警告。
可以看到连接也已经锁定了。
当前的配置文件:/etc/rabbitmq/rabbitmq.conf
# 触发流控制的内存阈值。可以是绝对的或相对于操作系统可用的 RAM 量:
# 使用 relative 相对值进行设置fraction,建议在0.4~0.7之间,不建议超过0.7
vm_memory_high_watermark.relative = 0.6
# 使用 absolute 绝对值,单位是KB、MB、GB等。
vm_memory_high_watermark.absolute = 2 GB
如果你的RAM是8G,设置的相对值0.4,也就是绝对值的3.2GB(8*0.4)。
当磁盘的剩余空间地域确定的阈值,RabbitMQ会同样的阻塞生产者。这样可以避免因非持久化的消息持续换页而耗尽磁盘空间,导致服务器崩溃。
在默认情况下,磁盘为50MB会进行预警:存储可持续化序列所在磁盘分区还剩50MB磁盘空间的时候会阻塞生产者,并停止内存消息换页到磁盘的步骤。
这个阈值可以减少,但是不能完全的消除因磁盘耗尽导致的崩溃的可能性。比如在两次磁盘空间的检查间隙内,第一次检查是60MB,第二次检查1MB,就会出现警告。
rabbitmqctl set_disk_free_limit <disk_limit>
rabbitmqctl set_disk_free_limit_memory_limit <fraction>
其中,
disk_limit:固定单位KB、MB、GB
fraction:相对阈值,建议范围在1.0~2.0之间。(相对于内存)
# RabbitMQ 存储数据的分区的磁盘可用空间限制。当可用磁盘空间低于此限制时,将触发流量控制。该值可以相对于 RAM 的总量设置,也可以设置为以字节为单位的绝对值,或者以信息单位(例如“50MB”或“5GB”)为单位:
disk_free_limit.relative = 3.0
disk_free_limit.absolute = 2 GB
# 默认情况下,可用磁盘空间必须超过 50MB。
disk_free_limit.absolute = 50 MB
在某个Broker节点以及内存阻塞生产者之前,他会尝试将队列中的消息换页到磁盘以释放内存,持久化和非持久化的消息都会写入磁盘。其中,持久化的消息本身在磁盘中有一个副本,所以,在转移的过程中持久化的消息会先从内存中消除掉。
在默认情况下,内存到达阈值是50%,就会进行换页处理。
也就是说,在默认情况下,该内存的阈值为0.4情况下,当内存超过了0.2(0.4*0.5),会进行换页动作。
# 设置需小于1,内存到达极限再进行换页意义不大
vm_memory_high_watermark_paging_ratio = 0.5
1. 报错信息
此处的报错信息就是你修改了已有的队列:direct_ttl_queue
队列在创建后,不可修改。如需修改,需要重新写起一条队列(不推荐,把原队列删除。因为在实际的开发过程中,你打算删除的那条队列或许还在工作)
2022-04-06 15:22:06.724 ERROR 12488 --- [ 127.0.0.1:5672] o.s.a.r.c.CachingConnectionFactory : Shutdown Signal: channel error; protocol method: #method(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'x-dead-letter-exchange' for queue 'direct_ttl_queue' in vhost '/': received the value 'direct_dead_exchange' of type 'longstr' but current is none, class-id=50, method-id=10)
2. 消费者在接受消息的时候,出现异常,会导致什么样的问题?应该怎么去进行处理?
问题:导致死循环,服务的消息重试投递
解决思路:
尚硅谷消息中间件课件,提取码:6syv:链接:https://pan.baidu.com/s/18kxeOdxwSUFbyvwMOGfZyg ↩︎