RabbitMQ学习(中)——交换机、死信队列和延迟队列
一、交换机
RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不会直接发送到队列。实际上,通常生产者甚至都不知道这些消息传递传递到了哪些队列中。
相反,生产者只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接收来自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们。这就的由交换机的类型来决定。
发布消息方法:
第一个参数是交换机的名称。空字符串表示默认或无名称交换机:消息能路由发送到队列中其实是由 routingKey(bindingkey)绑定 key 指定的,如果它存在的话
临时队列:
每当我们连接到 Rabbit 时,我们都需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者能让服务器为我们选择一个随机队列名称那就更好了。其次一旦我们断开了消费者的连接,队列将被自动删除。
- 创建临时队列的方式如下:
String queueName = channel.queueDeclare().getQueue();
创建出来之后长成这样:
绑定 binding:
binding 其实是 exchange 和 queue 之间的桥梁,它告诉我们 exchange 和那个队列进行了绑定关系。
1.1 Fanout exchange(发布/订阅模式)
Fanout exchange又叫发布订阅模式。扇出交换机将消息路由到与其绑定的所有队列
,并且路由键将被忽略
。如果将N个队列绑定到扇出交换,则将新消息发布到该交换时,会将消息的副本传递到所有N个队列。扇出交换机非常适合消息的广播路由
注意:fanout类型的exchange会把消息推到所有的queue中,所以不需要指定routingkey,指定了也没用
系统中默认有fanout类型的exchange
实现效果:EmitLog(生产者)发送消息给两个消费者接收并打印接收到的信息
代码示例:
EmitLog 发送消息给两个消费者接收:
public class EmitLog {
//交换机的名称
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
//声明一个交换机
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()) {
String message = scanner.next();
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes(StandardCharsets.UTF_8));
System.out.println("生成这发出消息:" + message);
}
}
}
ReceiveLogs02将接收到的消息打印
public class ReceiveLogs01 {
//交换机的名称
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
/**
* 生成一个临时队列,队列的名称是随机的
* 当消费者断开与队列的连接的时候,队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
/**
* 绑定交换机与队列,其中routingkey(也称之为 binding key)为空字符串,广播模式下路由键将被忽略
*/
channel.queueBind(queueName,EXCHANGE_NAME,"");
System.out.println("ReceiveLogs01等待接收消息,把接收到的消息打印在屏幕上.....");
//接收消息
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveLogs02控制台打印接收到的消息:" + new String(message.getBody()));
};
channel.basicConsume(queueName,true,deliverCallback,(consumerTag) -> {});
}
}
ReceiveLogs02将接收到的消息打印在控制台
public class ReceiveLogs02 {
//交换机的名称
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
/**
* 生成一个临时队列,队列的名称是随机的
* 当消费者断开与队列的连接的时候,队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
/**
* 绑定交换机与队列,其中routingkey(也称之为 binding key)为空字符串,广播模式下路由键将被忽略
*/
channel.queueBind(queueName,EXCHANGE_NAME,"");
System.out.println("ReceiveLogs02等待接收消息,把接收到的消息打印在屏幕上.....");
//接收消息
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveLogs02控制台打印接收到的消息:" + new String(message.getBody()));
};
channel.basicConsume(queueName,true,deliverCallback,(consumerTag) -> {});
}
}
效果展示:
1.2 Direct exchange(路由模式)
在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
在Direct模型下:
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个
RoutingKey
(路由key) - 消息的发送方在 向 Exchange发送消息时,也必须指定消息的
RoutingKey
。 - Exchange不再把消息交给每一个绑定的队列,而是根据消息的
Routing Key
进行判断,只有队列的Routingkey
与消息的Routingkey
完全一致,才会接收到消息
在上面这张图中,我们可以看到 X 绑定了两个队列,绑定类型是 direct。队列Q1 绑定键为 orange, 队列 Q2 绑定键有两个:一个绑定键为 black,另一个绑定键为 green.
在这种绑定情况下,生产者发布消息到 exchange 上,绑定键为 orange 的消息会被发布到队列 Q1。绑定键为 blackgreen 和的消息会被发布到队列 Q2,其他消息类型的消息将被丢弃。
多重绑定:
当然如果 exchange 的绑定类型是direct,但是它绑定的多个队列的 key 如果都相同,在这种情况下虽然绑定类型是 direct 但是它表现的就和 fanout 有点类似了,就跟广播差不多,如上图所示。
实战实现效果:
c2:绑定disk,routingKey为error
c1:绑定console,routingKey为info、warning
-
生产者:
public class DirectLogs { //交换机的名称 public static final String EXCHANGE_NAME = "direct_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //声明一个交换机 channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); //创建多个 bindingKey Map<String, String> bindingKeyMap = new HashMap<>(); bindingKeyMap.put("info", "普通 info 信息"); bindingKeyMap.put("warning", "警告 warning 信息"); bindingKeyMap.put("error", "错误 error 信息"); //debug 没有消费这接收这个消息 所有就丢失了 bindingKeyMap.put("debug", "调试 debug 信息"); for (Map.Entry<String, String> bindingKeyEntry : bindingKeyMap.entrySet()) { //获取 key value String bindingKey = bindingKeyEntry.getKey(); String message = bindingKeyEntry.getValue(); channel.basicPublish(EXCHANGE_NAME, bindingKey, null, message.getBytes("UTF-8")); System.out.println("生产者发出消息:" + message); } } }
-
消费者C1:
public class ReceiveLogsDirect01 { public static final String EXCHANGE_NAME = "direct_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //声明一个交换机 channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); channel.queueDeclare("console",false,false,false,null); //把该临时队列绑定名为EXCHANGE_NAME的交换机, 其中 routingkey(也称之为 binding key)为info channel.queueBind("console",EXCHANGE_NAME,"info"); channel.queueBind("console",EXCHANGE_NAME,"warning"); System.out.println("ReceiveLogsDirect01等待接收消息,把接收到的消息打印在屏幕上....."); //接收消息 DeliverCallback deliverCallback = (consumerTag, delivery) -> { String message = new String(delivery.getBody(), "UTF-8"); message = "接收绑定键:" + delivery.getEnvelope().getRoutingKey() + ",消息:" + message; System.out.println("info和warning 消息已经接收:\n" + message); }; channel.basicConsume("console",true,deliverCallback,(consumerTag) -> {}); } }
-
消费者C2:
public class ReceiveLogsDirect02 { public static final String EXCHANGE_NAME = "direct_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //声明一个交换机 channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); channel.queueDeclare("disk",false,false,false,null); //绑定交换机与队列 channel.queueBind("disk",EXCHANGE_NAME,"error"); System.out.println("ReceiveLogsDirect02等待接收消息,把接收到的消息打印在屏幕上....."); //接收消息 DeliverCallback deliverCallback = (consumerTag, delivery) -> { String message = new String(delivery.getBody(), "UTF-8"); message = "接收绑定键:" + delivery.getEnvelope().getRoutingKey() + ",消息:" + message; System.out.println("error 消息已经接收:\n" + message); }; channel.basicConsume("disk",true,deliverCallback,(consumerTag) -> {}); } }
-
执行结果:
1.3 Topics 模式
Topic
类型的Exchange
与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。只不过Topic
类型Exchange
可以让队列在绑定Routing key
的时候使用通配符!这种模型Routingkey
一般都是由一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
Topic的要求:
发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开。这些单词可以是任意单词,比如说:“stock.usd.nyse”, “nyse.vmw”, “quick.orange.rabbit”.这种类型的。当然这个单词列表最多不能超过 255 个字节。
在这个规则列表中,其中有两个替换符是大家需要注意的:
- *(星号)可以代替一个单词
- #(井号)可以替代零个或多个单词
Topic匹配案例:
下图绑定关系如下:
- Q1–>绑定的是
- 中间带 orange 带 3 个单词的字符串
(*.orange.*)
- 中间带 orange 带 3 个单词的字符串
- Q2–>绑定的是
- 最后一个单词是 rabbit 的 3 个单词
(*.*.rabbit)
- 第一个单词是 lazy 的多个单词
(lazy.#)
- 最后一个单词是 rabbit 的 3 个单词
上图是一个队列绑定关系图,我们来看看他们之间数据接收情况是怎么样的
例子 | 说明 |
---|---|
quick.orange.rabbit | 被队列 Q1Q2 接收到 |
azy.orange.elephant | 被队列 Q1Q2 接收到 |
quick.orange.fox | 被队列 Q1 接收到 |
lazy.brown.fox | 被队列 Q2 接收到 |
lazy.pink.rabbit | 虽然满足两个绑定但只被队列 Q2 接收一次 |
quick.brown.fox | 不匹配任何绑定不会被任何队列接收到会被丢弃 |
quick.orange.male.rabbit | 是四个单词不匹配任何绑定会被丢弃 |
lazy.orange.male.rabbit | 是四个单词但匹配 Q2 |
注意:
- 当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像 fanout 了
- 如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是 direct 了
实战实现效果:实现上面的匹配案例
-
生产者
public class EmitLogTopic { public static final String EXCHANGE_NAME = "topic_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //声明交换机 channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC); /** * Q1-->绑定的是 * 中间带 orange 带 3 个单词的字符串(*.orange.*) * Q2-->绑定的是 * 最后一个单词是 rabbit 的 3 个单词(*.*.rabbit) * 第一个单词是 lazy 的多个单词(lazy.#) */ Map<String, String> bindingKeyMap = new HashMap<>(); bindingKeyMap.put("quick.orange.rabbit","被队列 Q1Q2 接收到"); bindingKeyMap.put("lazy.orange.elephant","被队列 Q1Q2 接收到"); bindingKeyMap.put("quick.orange.fox","被队列 Q1 接收到"); bindingKeyMap.put("lazy.brown.fox","被队列 Q2 接收到"); bindingKeyMap.put("lazy.pink.rabbit","虽然满足两个绑定但只被队列 Q2 接收一次"); bindingKeyMap.put("quick.brown.fox","不匹配任何绑定不会被任何队列接收到会被丢弃"); bindingKeyMap.put("quick.orange.male.rabbit","是四个单词不匹配任何绑定会被丢弃"); bindingKeyMap.put("lazy.orange.male.rabbit","是四个单词但匹配 Q2"); for (Map.Entry<String, String> stringEntry : bindingKeyMap.entrySet()) { String routingKey = stringEntry.getKey(); String message = stringEntry.getValue(); channel.basicPublish(EXCHANGE_NAME,routingKey,null,message.getBytes(StandardCharsets.UTF_8)); System.out.println("生产者发送消息:" + message); } } }
-
消费者C1
/** * 消费者C1,接收中间带 orange 带 3 个单词的字符串 (*.orange.*) */ public class ReceiveLogsTopic01 { //交换机的名称 public static final String EXCHANGE_NAME = "topic_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //声明交换机 channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC); //声明队列 String queueName = "Q1"; channel.queueDeclare(queueName,false,false,false,null); //绑定交换机和队列 channel.queueBind(queueName,EXCHANGE_NAME,"*.orange.*"); System.out.println("Q1等待接收消息,把接收到的消息打印在屏幕上....."); //接收消息 DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println("Q1控制台打印接收到的消息:" + new String(message.getBody())); System.out.println("接收队列:" + queueName + " 绑定键:" + message.getEnvelope().getRoutingKey()); }; channel.basicConsume(queueName,true,deliverCallback,(consumerTag) ->{}); } }
-
消费者C2
/** * 消费者C2,接收最后一个单词是 rabbit 的 3 个单词 (*.*.rabbit)和第一个单词是 lazy 的多个单词 (lazy.#) */ public class ReceiveLogsTopic02 { //交换机的名称 public static final String EXCHANGE_NAME = "topic_logs"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //声明交换机 channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC); //声明队列 String queueName = "Q2"; channel.queueDeclare(queueName,false,false,false,null); //绑定交换机和队列 channel.queueBind(queueName,EXCHANGE_NAME,"*.*.rabbit"); channel.queueBind(queueName,EXCHANGE_NAME,"lazy.#"); System.out.println("Q2等待接收消息,把接收到的消息打印在屏幕上....."); //接收消息 DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println("Q2控制台打印接收到的消息:" + new String(message.getBody())); System.out.println("接收队列:" + queueName + " 绑定键:" + message.getEnvelope().getRoutingKey()); }; channel.basicConsume(queueName,true,deliverCallback,(consumerTag) ->{}); } }
-
执行结果:
二、死信队列
死信队列简介:
-
死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
-
应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生异常时,将消息投入死信队列中.还有比如说: 用户在商城下单成功并点击去支付后在指定时间未支付时自动失效
死信的来源:
-
消息 TTL 过期,TTL是Time To Live的缩写, 也就是生存时间
-
队列达到最大长度,队列满了,无法再添加数据到 mq 中
-
消息被拒绝,(basic.reject 或 basic.nack) 并且 requeue=false(手动确认)
死信实战效果:
2.1 死信之TTl
-
消费者C1,用于消费正常队列里的消息
public class Consumer01 { //普通交换机名称 private static final String NORMAL_EXCHANGE = "normal_exchange"; //死信交换机名称 private static final String DEAD_EXCHANGE = "dead_exchange"; //普通队列的名称 private static final String NORMAL_QUEUE = "normal_queue"; //死信队列的名称 private static final String DEAD_QUEUE = "dead_queue"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); //声明死信和普通交换机 类型为 direct channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT); channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT); //声明普通队列 Map<String,Object> arguments = new HashMap<>(); //过期时间 10s;可以设置普通队列中消息的过期时间,也可以在生产者发消息的时候指定过期时间 // arguments.put("x-message-ttl",10000); //正常队列设置死信交换机 arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE); //设置死信RoutingKey arguments.put("x-dead-letter-routing-key","lisi"); channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments); //声明死信队列 channel.queueDeclare(DEAD_QUEUE,false,false,false,null); //绑定普通的交换机与普通的队列:队列、交换机、路由键(routingKey) channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan"); //绑定死信的交换机与死信的队列 channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi"); System.out.println("等待接收消息........... "); //接收消息 DeliverCallback deliverCallback = (consumerTag, delivery) -> { String message = new String(delivery.getBody(), "UTF-8"); System.out.println("Consumer01 接收到消息" + message); }; channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,(consumerTag) ->{}); } }
-
消费者C2,用于消费死信队列里的消息
public class Consumer02 { //死信队列的名称 private static final String DEAD_QUEUE = "dead_queue"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); System.out.println("等待接收消息........... "); //接收消息 DeliverCallback deliverCallback = (consumerTag, delivery) -> { String message = new String(delivery.getBody(), "UTF-8"); System.out.println("Consumer02 接收到消息" + message); }; channel.basicConsume(DEAD_QUEUE,true,deliverCallback,(consumerTag) ->{}); } }
-
生产者
public class Producer { //普通交换机名称 private static final String NORMAL_EXCHANGE = "normal_exchange"; public static void main(String[] args) throws Exception { Channel channel = RabbitMqUtils.getChannel(); channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT); //设置消息的 TTL 时间 10s AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build(); //该信息是用作演示队列个数限制 for (int i = 1; i < 11; i++) { String message = "info" + i; channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", properties, message.getBytes()); System.out.println("生产者发送消息:" + message); } } }
-
运行测试:启动 C1 ,之后关闭消费者,模拟其接收不到消息。再启动生产者
以上步骤完成后,启动 C2 消费者,它消费死信队列里面的消息
2.2 死信之最大长度
-
消息生产者代码去掉TTL属性
-
C1消费者中设置队列的最大的长度,C2代码不变
//设置正常队列的长度限制,例如发10个,4个则为死信 params.put("x-max-length",6);
-
运行测试
C1启动之后关闭该消费者 模拟其接收不到消息,之后启动C2消费者
2.3 死信之消息被拒
-
生产者和C2消费者的代码不变,在C1消费中设置拒收消息 "info5"
-
模拟运行
启动生产者
启动消费者C1然后再启动消费者C2
三、延迟队列
延迟队列概念:
延时队列
,最重要的特性就体现在它的延时属性上,跟普通的队列不一样的是,普通队列中的元素总是等着希望被早点取出处理,而延时队列中的元素则是希望被在指定时间得到取出和处理,所以延时队列中的元素是都是带时间属性的,通常来说是需要被处理的消息或者任务。
使用场景:(常用于一些定时任务)
- 订单在十分钟之内未支付则自动取消
- 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
- 用户注册成功后,如果三天内没有登陆则进行短信提醒。
- 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
- 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
3.1 RabbitMQ 中的 TTL
TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒。
即:如果一条消息设置了 TTL 属性或者进入了设置TTL 属性的队列,那么这条消息如果在 TTL 设置的时间内没有被消费,则会成为"死信
"。如果同时配置了队列的TTL 和消息的 TTL,那么较小的那个值将会被使用。
两种设置 TTL的方式:
- 队列设置TTL,在创建队列的时候设置队列的“x-message-ttl”属性
- 消息设置TTL(针对每条消息设置TTL)
两者的区别:
- 如果设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队列中)。
- 而第二种方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间(后面有案例);
利用RabbitMQ实现延时队列:
利用RabbitMQ实现延时队列的两大要素:死信队列和TTL
延时队列,就是想要消息延迟多久被处理。TTL则刚好能让消息在延迟多久之后成为死信,另一方面,成为死信的消息都会被投递到死信队列里,这样只需要消费者一直消费死信队列里的消息就万事大吉了,因为里面的消息都是希望被立即处理的消息
3.2 整合SpringBoot
添加依赖:
<dependencies>
<!--RabbitMQ 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--RabbitMQ 测试依赖-->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
添加配置文件:
spring.rabbitmq.host=192.168.2.4
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123
3.3 队列设置TTL
代码架构图:
创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后在创建一个交换机 X 和死信交 换机 Y,它们的类型都是direct,创建一个死信队列 QD,它们的绑定关系如下:
-
配置文件代码
/** * 配置文件类代码 */ @Configuration public class TtlQueueConfig { //普通交换机的名称 public static final String X_EXCHANGE = "X"; //普通队列的名称 public static final String QUEUE_A = "QA"; public static final String QUEUE_B = "QB"; //死信交换机的名称 public static final String Y_DEAD_LETTER_EXCHANGE = "Y"; //死信队列的名称 public static final String DEAD_LETTER_QUEUE = "QD"; //声明xExchange @Bean("xExchange") public DirectExchange xExchange() { return new DirectExchange(X_EXCHANGE); } //声明xExchange @Bean("yExchange") public DirectExchange yExchange() { return new DirectExchange(Y_DEAD_LETTER_EXCHANGE); } //声明普通队列 TTL 为 10S @Bean("queueA") public Queue queueA() { Map<String,Object> arguments = new HashMap<>(); //设置死信交换机 arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE); //设置死信RoutingKey arguments.put("x-dead-letter-routing-key","YD"); //设置TTL 单位是ms arguments.put("x-message-ttl",10000); return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build(); } //声明普通队列 TTL 为 40S @Bean("queueB") public Queue queueB() { Map<String,Object> arguments = new HashMap<>(); //设置死信交换机 arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE); //设置死信RoutingKey arguments.put("x-dead-letter-routing-key","YD"); //设置TTL 单位是ms arguments.put("x-message-ttl",40000); return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build(); } //死信队列 @Bean("queueD") public Queue queueD() { return QueueBuilder.durable(DEAD_LETTER_QUEUE).build(); } //绑定,QA绑定交换机X,routingKey为XA @Bean public Binding queueABindingX(@Qualifier("queueA")Queue queueA,@Qualifier("xExchange") DirectExchange xExchange) { return BindingBuilder.bind(queueA).to(xExchange).with("XA"); } //绑定,QB绑定交换机X,routingKey为XB @Bean public Binding queueBBindingX(@Qualifier("queueB")Queue queueB, @Qualifier("xExchange") DirectExchange xExchange) { return BindingBuilder.bind(queueB).to(xExchange).with("XB"); } //绑定,QD绑定交换机Y,routingKey为YD @Bean public Binding queueDBindingY(@Qualifier("queueD") Queue queueD,@Qualifier("yExchange") DirectExchange yExchange) { return BindingBuilder.bind(queueD).to(yExchange).with("YD"); } }
-
生产者代码
/** * 发送延迟消息 * http://localhost:8080/ttl/sendMsg/嘻嘻嘻 */ @Slf4j @RestController @RequestMapping("ttl") public class SendMsgController { @Autowired RabbitTemplate rabbitTemplate; //开始发送消息 @GetMapping("/sendMsg/{message}") public void sendMsg(@PathVariable("message") String message) { log.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", new Date().toString(), message); rabbitTemplate.convertAndSend("X", "XA", "消息来自 ttl 为 10S 的队列: " + message); rabbitTemplate.convertAndSend("X", "XB", "消息来自 ttl 为 40S 的队列: " + message); } }
-
消费者代码
/** * 队列TTL 消费者 */ @Slf4j @Component public class DeadLetterQueueConsumer { @RabbitListener(queues = "QD") public void receiveD(Message message, Channel channel) { String msg = new String(message.getBody()); log.info("当前时间:{},收到死信队列的消息:{}",new Date().toString(), msg); } }
-
运行测试
发起一个请求
http://localhost:8080/ttl/sendMsg/嘻嘻嘻
第一条消息在 10S 后变成了死信消息,然后被消费者消费掉,第二条消息在 40S 之后变成了死信消息, 然后被消费掉,这样一个延时队列就打造完成了。
不过,如果这样使用的话,岂不是每增加一个新的时间需求,就要新增一个队列,这里只有 10S 和 40S 两个时间选项,如果需要一个小时后处理,那么就需要增加TTL 为一个小时的队列,如果是预定会议室然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?
3.4 消息设置TTL
为了解决需要增加队列才能改变时间的情况,,那么就只能将TTL设置在消息属性里了
在这里新增了一个队列QC,绑定关系如下,该队列不设置TTL 时间
-
配置文件类代码
在原有的延迟队列配置类中添加队列QC的相关配置
//普通队列的名称 public static final String QUEUE_C = "QC"; //声明普通队列QC @Bean("queueC") public Queue queueC() { Map<String,Object> arguments = new HashMap<>(); //设置死信交换机 arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE); //设置死信RoutingKey arguments.put("x-dead-letter-routing-key","YD"); return QueueBuilder.durable(QUEUE_C).withArguments(arguments).build(); } //绑定,QC绑定交换机X,routingKey为XC @Bean public Binding queueCBindingX(@Qualifier("queueC")Queue queueC, @Qualifier("xExchange") DirectExchange xExchange) { return BindingBuilder.bind(queueC).to(xExchange).with("XC"); }
-
生产者代码
/** * 消息设置TTL * @param message 消息 * @param ttlTime 延时的毫秒 * http://localhost:8080/ttl/sendExpirationMsg/你好1/20000 * http://localhost:8080/ttl/sendExpirationMsg/你好2/2000 */ @GetMapping("/sendExpirationMsg/{message}/{ttlTime}") public void sendMsg(@PathVariable String message, @PathVariable String ttlTime) { rabbitTemplate.convertAndSend("X", "XC", message, correlationData -> { //发送消息的时候 延迟时长 correlationData.getMessageProperties().setExpiration(ttlTime); return correlationData; }); log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date().toString(), ttlTime, message); }
-
运行测试:
发起请求:
http://localhost:8080/ttl/sendExpirationMsg/你好1/20000
http://localhost:8080/ttl/sendExpirationMsg/你好2/2000
结果:
看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用在消息属性上设置 TTL 的方式,消息可能并不会按时“死亡“因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列, 如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。这也就是为什么第二个延时2秒,却后执行。
3.5 基于插件的延迟队列
那如何解决这个问题呢?这里可以使用RabbitMQ插件
插件的安装:
可去官网下载 rabbitmq_delayed_message_exchange 插件,放置到 RabbitMQ 的插件目录。
[root@VM-0-6-centos software]# ls
erlang-21.3.8.21-1.el7.x86_64.rpm rabbitmq_delayed_message_exchange-3.8.0.ez rabbitmq-server-3.8.8-1.el7.noarch.rpm
#移动
cp rabbitmq_delayed_message_exchange-3.8.0.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
#安装
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
#重启服务
systemctl restart rabbitmq-server
实现效果:
在这里新增了一个队列delayed.queue,一个自定义交换机 delayed.exchange,绑定关系如下:
-
配置文件类代码:
在我们自定义的交换机中,这是一种新的交换类型,该类型消息支持延迟投递机制消息传递后并不会立即投递到目标队列中,而是存储在 mnesia(一个分布式数据系统)表中,当达到投递时间时,才投递到目标队列中。
@Configuration public class DelayedQueueConfig { //队列 public static final String DELAYED_QUEUE_NAME = "delayed.queue"; //交换机 public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange"; //routingKey public static final String DELAYED_ROUTING_KEY = "delayed.routingkey"; @Bean public Queue delayedQueue() { return new Queue(DELAYED_QUEUE_NAME); } //声明交换机,基于插件的 @Bean public CustomExchange delayedExchange() { Map<String, Object> args = new HashMap<>(); //自定义交换机的类型 args.put("x-delayed-type", "direct"); return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message", true,false,args); } //绑定 @Bean public Binding delayedQueueBingDelayedExchange( @Qualifier("delayedQueue") Queue delayedQueue, @Qualifier("delayedExchange") CustomExchange delayedExchange) { return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs(); } }
-
消息生产者代码
/** * http://localhost:8080/ttl/sendDelayMsg/hello1/20000 * http://localhost:8080/ttl/sendDelayMsg/hello2/2000 */ @GetMapping("sendDelayMsg/{message}/{delayTime}") public void sendMsg(@PathVariable String message, @PathVariable Integer delayTime) { rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, message, correlationData -> { //发送消息的时候设置延迟时间 correlationData.getMessageProperties().setDelay(delayTime); return correlationData; }); log.info("当前时间:{},发送一条延迟{}毫秒的信息给队列delayed.queue:{}", new Date().toString(), delayTime, message); }
-
消息消费者代码
/** * 消费者,基于插件的延迟消息 */ @Slf4j @Component public class DelayQueueConsumer { public static final String DELAYED_QUEUE_NAME = "delayed.queue"; @RabbitListener(queues = DELAYED_QUEUE_NAME) public void receiveDelayedQueue(Message message) { String msg = new String(message.getBody()); log.info("当前时间:{},收到延时队列的消息:{}", new Date().toString(), msg); } }
-
运行测试
发送请求:
http://localhost:8080/ttl/sendDelayMsg/hello1/20000
和http://localhost:8080/ttl/sendDelayMsg/hello2/2000
第二个消息被先消费掉了,符合预期