目录
一、依赖
<dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.14.2</version> </dependency>
二、代码
1、死信队列
1.1、TTL过期
1.1.1、生产者
public class Provider { // 队列名称 private static final String COMMON_EXCHANGE_NAME = "common_exchange"; private static final String COMMON_QUEUE_NAME = "common_queue"; public static void main(String[] args) { // 创建连接工厂,配置连接相关设置 ConnectionFactory factory = new ConnectionFactory(); /** * 设置连接参数 * 比如:host(localhost)、port(5672)、virtualHost(/)、 * userName(guest)、password(guest)、connectionTimeout(60s) * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值, * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了, * 下面的代码作用是说明一下如何设置我们想要的值 */ factory.setHost("localhost"); /** * 使用 try-with-resource 语法,try中代码块执行结束之后将会关闭connection和channel, * 原因是:Connection和Channel都实现了AutoCloseable接口,所以close()方法会主动执行 */ try ( // 根据上面设置的参数来创建连接 Connection connection = factory.newConnection(); // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费 Channel channel = connection.createChannel(); ) { /** * 创建交换机 * 参数1:交换机名称 * 参数2:交换机类型;direct代表直接模式,这种模式根据消息的路由键进行消息转发,其中消息携带路由键,而队列和交换机也通过路由键进行绑定,当两个路由键完全一致的时候,交换机就会把消息发送给这个队列 */ channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT); /** * 消息属性 * new AMQP.BasicProperties(XXX)是直接从MessageProperties.PERSISTENT_TEXT_PLAIN消息持久化配置中拿过来的 * expiration(XXX)用来设置消息过期时间,单位是毫秒 */ AMQP.BasicProperties properties = new AMQP.BasicProperties("text/plain", null, null, 2, 0, null, null, null, null, null, null, null, null, null).builder().expiration("10000").build(); // 消息内容 String message = "Hello World!"; /** * 发送消息到队列 * 说明:由于我们要发送消息到队列,所以我们一定要先创建队列 * 参数1:交换机名称 * ""是默认交换机的名称,其中默认交换机隐式绑定到每个队列,路由密钥等于队列名称,无法删除默认交换机 * 参数2:路由名称 * 由于默认交换机和队列之间的路由是队列名称,所以消息路由是队列名称 * 参数3:其他消息属性,比如MessageProperties.PERSISTENT_TEXT_PLAIN就是消息持久化 * 参数4,消息内容 * 消息内容是字节数组,所以我们需要使用getBytes()方法 */ channel.basicPublish(COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME, properties, message.getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { e.printStackTrace(); } } }
1.1.2、消费者
public class Consumer { // 队列名称 private static final String COMMON_EXCHANGE_NAME = "common_exchange"; private static final String DEAD_EXCHANGE_NAME = "dead_exchange"; private static final String COMMON_QUEUE_NAME = "common_queue"; private static final String DEAD_QUEUE_NAME = "dead_queue"; public static void main(String[] args) { // 创建连接工厂,配置连接相关设置 ConnectionFactory factory = new ConnectionFactory(); /** * 设置连接参数 * 比如:host(localhost)、port(5672)、virtualHost(/)、 * userName(guest)、password(guest)、connectionTimeout(60s) * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值, * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了, * 下面的代码作用是说明一下如何设置我们想要的值 */ factory.setHost("localhost"); // 不使用try-with-resource语法的原因是“我们希望进程在消费者异步监听消息到达时始终保持活动状态”,不希望try中代码运行结束,就关闭Connection和Channel try { // 根据上面设置的参数来创建连接 Connection connection = factory.newConnection(); // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费 Channel channel = connection.createChannel(); // 创建普通交换机 channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT); // 创建死信交换机 channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT); // 为死信指明死信交换机和死信路由 Map<String, Object> arguments = new HashMap<>(); // 设置死信的目的交换机 arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME); // 设置死信携带的路由 arguments.put("x-dead-letter-routing-key", DEAD_QUEUE_NAME); /** * 创建普通队列 * 注意:我们也在这里声明了队列,因为我们可能在发布者之前启动消费者,所以我们要确保队列存在,然后再尝试从中消费消息 * 参数1:队列名称 * 参数2:是否持久化 * 队列持久化之后,即使RabbitMQ重启,队列依然存在 * 参数3:是否排他 * 设置排他:仅限当前连接使用,虽然我不太懂,但是不要设置,毕竟其他连接也要使用 * 参数4:附加参数 * 比如可以设置死信队列等 */ channel.queueDeclare(COMMON_QUEUE_NAME, true, false, false, arguments); /** * 创建死信队列 */ channel.queueDeclare(DEAD_QUEUE_NAME, true, false, false, null); // 绑定普通队列和普通交换机 channel.queueBind(COMMON_QUEUE_NAME, COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME); // 绑定死信队列和死信交换机 channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, DEAD_QUEUE_NAME); // 消息创建中的回调对象;用来处理从队列异步推送过来的消息 DeliverCallback deliverCallback = (consumerTag, message) -> { String str = new String(message.getBody(), StandardCharsets.UTF_8); System.out.println(str); }; /** * 消费者异步监听队列消息 * 说明:队列会异步推送消息到我们这里,我们使用deliverCallback回调对象来接受消息 * 参数1:队列名称; * 指定消费者连接的队列 * 参数2:是否自动确认; * 如果自动确认,消费者接收消息之后(未处理消息)就会向RabbitMQ服务端发送确认消息,然后RabbitMQ就会删除对应消息 * 如果不自动确认,那我们就需要手动通知RabbitMQ服务端消息已经可以删除了,然后RabbitMQ服务端才会删除对应消息 * 参数3:消息传递时的回调对象 * 参数4:消费者被取消时的回调,暂时没有看懂啥意思 */ channel.basicConsume(DEAD_QUEUE_NAME, true, deliverCallback, consumerTag -> { }); } catch (Exception e) { e.printStackTrace(); } } }
1.1.3、说明
TTL代表 time to live
,即消息存活时间,默认情况下队列中的消息不会过期,不过部分场景需要设置过期时间,比如淘宝购买商品的时候,必须要在两个小时内付款,否则订单自动取消;又或者说在12306上买票的时候,必须在30分钟内付款,否则订单自动取消;这些过程都可以用到死信队列,毕竟这些场景的时间都是定值,所以开发者可以设置一个普通队列,该队列的消息过期时间是2小时或者30分钟,这个根据场景来定,当然这个过程时间也可以由发送者设置,比如我们上面的例子中就是由发送者设置的,但是我们现在说的场景是固定时间由普通队列设置,并且该队列和死信交换机进行绑定,并且设置了相关路由键,也就是一旦消息过期,消息将携带设置的路由键进入死信交换机,然后死信交换机把消息交给死信队列,但是这个普通队列不允许任何消费者监听,毕竟该队列就是进行计时的,不需要被监听,我们真正应该监听的是死信队列,毕竟消息过期的时候也代表时间够了,然后我们消息经由普通队列》死信交换机》死信队列》消费者,然后我们根据订单的状态来做对应的事情,比如订单已经被支付了,那我们什么都不用做,如果订单依然没有被支付,我们就可以进行取消订单操作,如果订单已经被取消了,我们也可以什么都不用做,至于这整个流程我们可以这样来看,如下:
1.2、消息数量超过队列最大长度
1.2.1、生产者
public class Provider { // 队列名称 private static final String COMMON_EXCHANGE_NAME = "common_exchange"; private static final String COMMON_QUEUE_NAME = "common_queue"; public static void main(String[] args) { // 创建连接工厂,配置连接相关设置 ConnectionFactory factory = new ConnectionFactory(); /** * 设置连接参数 * 比如:host(localhost)、port(5672)、virtualHost(/)、 * userName(guest)、password(guest)、connectionTimeout(60s) * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值, * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了, * 下面的代码作用是说明一下如何设置我们想要的值 */ factory.setHost("localhost"); /** * 使用 try-with-resource 语法,try中代码块执行结束之后将会关闭connection和channel, * 原因是:Connection和Channel都实现了AutoCloseable接口,所以close()方法会主动执行 */ try ( // 根据上面设置的参数来创建连接 Connection connection = factory.newConnection(); // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费 Channel channel = connection.createChannel(); ) { /** * 创建交换机 * 参数1:交换机名称 * 参数2:交换机类型;direct代表直接模式,这种模式根据消息的路由键进行消息转发,其中消息携带路由键,而队列和交换机也通过路由键进行绑定,当两个路由键完全一致的时候,交换机就会把消息发送给这个队列 */ channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT); for (int i = 1; i <= 10; i++) { // 消息内容 String message = "Hello World!" + i; /** * 发送消息到队列 * 说明:由于我们要发送消息到队列,所以我们一定要先创建队列 * 参数1:交换机名称 * ""是默认交换机的名称,其中默认交换机隐式绑定到每个队列,路由密钥等于队列名称,无法删除默认交换机 * 参数2:路由名称 * 由于默认交换机和队列之间的路由是队列名称,所以消息路由是队列名称 * 参数3:其他消息属性 * MessageProperties.PERSISTENT_TEXT_PLAIN:消息持久化,即消息将会存储到磁盘中,只要消息队列也是持久化的,即使RabbitMQ重启,消息依然存在于队列中 * 参数4,消息内容 * 消息内容是字节数组,所以我们需要使用getBytes()方法 */ channel.basicPublish(COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes(StandardCharsets.UTF_8)); } } catch (Exception e) { e.printStackTrace(); } } }
1.2.2、普通消费者
/* * Copyright (c) 2006-2021, BeiJing Leadal Technology Co.,Ltd. * All rights reserved. * * Project : kms-wiki * Version : 1.1.0 * JDK Version : 1.6 * * Comments * * History * Sr Date ModifiedBy Why & What is modified * 1. 2022/4/22 guoming Created */ package com.atguigu.demo.dead_letter.max_queue_length; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import java.util.HashMap; import java.util.Map; /** * 消费者(作用:用来接收普通消息;注意:该消费者只用创建交换机和队列,创建完成之后就可以关闭了) * * @author guoming * @date 2022/4/22 23:14 */ public class Consumer1 { // 队列名称 private static final String COMMON_EXCHANGE_NAME = "common_exchange"; private static final String DEAD_EXCHANGE_NAME = "dead_exchange"; private static final String COMMON_QUEUE_NAME = "common_queue"; private static final String DEAD_QUEUE_NAME = "dead_queue"; public static void main(String[] args) { // 创建连接工厂,配置连接相关设置 ConnectionFactory factory = new ConnectionFactory(); /** * 设置连接参数 * 比如:host(localhost)、port(5672)、virtualHost(/)、 * userName(guest)、password(guest)、connectionTimeout(60s) * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值, * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了, * 下面的代码作用是说明一下如何设置我们想要的值 */ factory.setHost("localhost"); // 不使用try-with-resource语法的原因是“我们希望进程在消费者异步监听消息到达时始终保持活动状态”,不希望try中代码运行结束,就关闭Connection和Channel try { // 根据上面设置的参数来创建连接 Connection connection = factory.newConnection(); // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费 Channel channel = connection.createChannel(); // 创建普通交换机 channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT); // 创建死信交换机 channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT); // 为死信指明死信交换机和死信路由 Map<String, Object> arguments = new HashMap<>(); // 设置普通队列最大长度,也就是队列同时最多能存在的消息数量 arguments.put("x-max-length", 6); // 设置死信的目的交换机 arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME); // 设置死信携带的路由 arguments.put("x-dead-letter-routing-key", DEAD_QUEUE_NAME); /** * 创建普通队列 * 注意:我们也在这里声明了队列,因为我们可能在发布者之前启动消费者,所以我们要确保队列存在,然后再尝试从中消费消息 * 参数1:队列名称 * 参数2:是否持久化 * 队列持久化之后,即使RabbitMQ重启,队列依然存在 * 参数3:是否排他 * 设置排他:仅限当前连接使用,虽然我不太懂,但是不要设置,毕竟其他连接也要使用 * 参数4:附加参数 * 比如可以设置死信队列等 */ channel.queueDeclare(COMMON_QUEUE_NAME, true, false, false, arguments); /** * 创建死信队列 */ channel.queueDeclare(DEAD_QUEUE_NAME, true, false, false, null); // 绑定普通队列和普通交换机 channel.queueBind(COMMON_QUEUE_NAME, COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME); // 绑定死信队列和死信交换机 channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, DEAD_QUEUE_NAME); } catch (Exception e) { e.printStackTrace(); } } }
1.2.3、死信消费者
public class Consumer2 { // 队列名称 private static final String DEAD_EXCHANGE_NAME = "dead_exchange"; private static final String DEAD_QUEUE_NAME = "dead_queue"; public static void main(String[] args) { // 创建连接工厂,配置连接相关设置 ConnectionFactory factory = new ConnectionFactory(); /** * 设置连接参数 * 比如:host(localhost)、port(5672)、virtualHost(/)、 * userName(guest)、password(guest)、connectionTimeout(60s) * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值, * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了, * 下面的代码作用是说明一下如何设置我们想要的值 */ factory.setHost("localhost"); // 不使用try-with-resource语法的原因是“我们希望进程在消费者异步监听消息到达时始终保持活动状态”,不希望try中代码运行结束,就关闭Connection和Channel try { // 根据上面设置的参数来创建连接 Connection connection = factory.newConnection(); // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费 Channel channel = connection.createChannel(); // 创建死信交换机 channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT); // 创建死信队列 channel.queueDeclare(DEAD_QUEUE_NAME, true, false, false, null); // 绑定死信队列和死信交换机 channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, DEAD_QUEUE_NAME); // 消息创建中的回调对象;用来处理从队列异步推送过来的消息 DeliverCallback deliverCallback = (consumerTag, message) -> { String str = new String(message.getBody(), StandardCharsets.UTF_8); System.out.println(str); }; /** * 消费者异步监听队列消息 * 说明:队列会异步推送消息到我们这里,我们使用deliverCallback回调对象来接受消息 * 参数1:队列名称; * 指定消费者连接的队列 * 参数2:是否自动确认; * 如果自动确认,消费者接收消息之后(未处理消息)就会向RabbitMQ服务端发送确认消息,然后RabbitMQ就会删除对应消息 * 如果不自动确认,那我们就需要手动通知RabbitMQ服务端消息已经可以删除了,然后RabbitMQ服务端才会删除对应消息 * 参数3:消息传递时的回调对象 * 参数4:消费者被取消时的回调,暂时没有看懂啥意思 */ channel.basicConsume(DEAD_QUEUE_NAME, true, deliverCallback, consumerTag -> { }); } catch (Exception e) { e.printStackTrace(); } } }
1.2.4、说明
如果队列中的最大消息数量有限制,超出数量的消息将成为死信,我们本次的模拟办法是设置队列的最大消息数量是6,然后我们发送10条消息,那么消息队列只能存储6个,由于队列是先进先出的,所以最前面到来的4个消息将会成为死信。普通队列对应有死信交换机,并且设置了死信消息的路由是死信队列名称,这样我们就可以把死信消息发送到死信队列,然后被消费者消费掉,整个过程如下图:
1.3、消费者拒绝接收消息,并拒绝将消息重新放回队列
1.3.1、生产者
public class Provider { // 队列名称 private static final String COMMON_EXCHANGE_NAME = "common_exchange"; private static final String COMMON_QUEUE_NAME = "common_queue"; public static void main(String[] args) { // 创建连接工厂,配置连接相关设置 ConnectionFactory factory = new ConnectionFactory(); /** * 设置连接参数 * 比如:host(localhost)、port(5672)、virtualHost(/)、 * userName(guest)、password(guest)、connectionTimeout(60s) * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值, * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了, * 下面的代码作用是说明一下如何设置我们想要的值 */ factory.setHost("localhost"); /** * 使用 try-with-resource 语法,try中代码块执行结束之后将会关闭connection和channel, * 原因是:Connection和Channel都实现了AutoCloseable接口,所以close()方法会主动执行 */ try ( // 根据上面设置的参数来创建连接 Connection connection = factory.newConnection(); // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费 Channel channel = connection.createChannel(); ) { /** * 创建交换机 * 参数1:交换机名称 * 参数2:交换机类型;direct代表直接模式,这种模式根据消息的路由键进行消息转发,其中消息携带路由键,而队列和交换机也通过路由键进行绑定,当两个路由键完全一致的时候,交换机就会把消息发送给这个队列 */ channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT); for (int i = 1; i <= 10; i++) { // 消息内容 String message = "Hello World!" + i; /** * 发送消息到队列 * 说明:由于我们要发送消息到队列,所以我们一定要先创建队列 * 参数1:交换机名称 * ""是默认交换机的名称,其中默认交换机隐式绑定到每个队列,路由密钥等于队列名称,无法删除默认交换机 * 参数2:路由名称 * 由于默认交换机和队列之间的路由是队列名称,所以消息路由是队列名称 * 参数3:其他消息属性 * MessageProperties.PERSISTENT_TEXT_PLAIN:消息持久化,即消息将会存储到磁盘中,只要消息队列也是持久化的,即使RabbitMQ重启,消息依然存在于队列中 * 参数4,消息内容 * 消息内容是字节数组,所以我们需要使用getBytes()方法 */ channel.basicPublish(COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes(StandardCharsets.UTF_8)); } } catch (Exception e) { e.printStackTrace(); } } }
1.3.2、普通消费者
public class Consumer1 { // 队列名称 private static final String COMMON_EXCHANGE_NAME = "common_exchange"; private static final String DEAD_EXCHANGE_NAME = "dead_exchange"; private static final String COMMON_QUEUE_NAME = "common_queue"; private static final String DEAD_QUEUE_NAME = "dead_queue"; public static void main(String[] args) { // 创建连接工厂,配置连接相关设置 ConnectionFactory factory = new ConnectionFactory(); /** * 设置连接参数 * 比如:host(localhost)、port(5672)、virtualHost(/)、 * userName(guest)、password(guest)、connectionTimeout(60s) * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值, * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了, * 下面的代码作用是说明一下如何设置我们想要的值 */ factory.setHost("localhost"); // 不使用try-with-resource语法的原因是“我们希望进程在消费者异步监听消息到达时始终保持活动状态”,不希望try中代码运行结束,就关闭Connection和Channel try { // 根据上面设置的参数来创建连接 Connection connection = factory.newConnection(); // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费 Channel channel = connection.createChannel(); // 创建普通交换机 channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT); // 创建死信交换机 channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT); // 为死信指明死信交换机和死信路由 Map<String, Object> arguments = new HashMap<>(); // 设置死信的目的交换机 arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME); // 设置死信携带的路由 arguments.put("x-dead-letter-routing-key", DEAD_QUEUE_NAME); /** * 创建普通队列 * 注意:我们也在这里声明了队列,因为我们可能在发布者之前启动消费者,所以我们要确保队列存在,然后再尝试从中消费消息 * 参数1:队列名称 * 参数2:是否持久化 * 队列持久化之后,即使RabbitMQ重启,队列依然存在 * 参数3:是否排他 * 设置排他:仅限当前连接使用,虽然我不太懂,但是不要设置,毕竟其他连接也要使用 * 参数4:附加参数 * 比如可以设置死信队列等 */ channel.queueDeclare(COMMON_QUEUE_NAME, true, false, false, arguments); /** * 创建死信队列 */ channel.queueDeclare(DEAD_QUEUE_NAME, true, false, false, null); // 绑定普通队列和普通交换机 channel.queueBind(COMMON_QUEUE_NAME, COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME); // 绑定死信队列和死信交换机 channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, DEAD_QUEUE_NAME); // 消息创建中的回调对象;用来处理从队列异步推送过来的消息 DeliverCallback deliverCallback = (consumerTag, message) -> { /** * 手动拒绝接收消息 * 参数1:消息标签 * 参数2:是否重新将消息放回原来的队列 * 是:消息重新放回队列,然后重新发送消费者,该消息不会生成死信消息 * 否:消息不放回队列,该消息会成为死信消息 */ channel.basicReject(message.getEnvelope().getDeliveryTag(), false); }; /** * 消费者异步监听队列消息 * 说明:队列会异步推送消息到我们这里,我们使用deliverCallback回调对象来接受消息 * 参数1:队列名称; * 指定消费者连接的队列 * 参数2:是否自动确认; * 如果自动确认,消费者接收消息之后(未处理消息)就会向RabbitMQ服务端发送确认消息,然后RabbitMQ就会删除对应消息 * 如果不自动确认,那我们就需要手动通知RabbitMQ服务端消息已经可以删除了,然后RabbitMQ服务端才会删除对应消息 * 参数3:消息传递时的回调对象 * 参数4:消费者被取消时的回调,暂时没有看懂啥意思 */ channel.basicConsume(COMMON_QUEUE_NAME, false, deliverCallback, consumerTag -> { }); } catch (Exception e) { e.printStackTrace(); } } }
1.3.3、死信消费者
public class Consumer2 { // 队列名称 private static final String DEAD_EXCHANGE_NAME = "dead_exchange"; private static final String DEAD_QUEUE_NAME = "dead_queue"; public static void main(String[] args) { // 创建连接工厂,配置连接相关设置 ConnectionFactory factory = new ConnectionFactory(); /** * 设置连接参数 * 比如:host(localhost)、port(5672)、virtualHost(/)、 * userName(guest)、password(guest)、connectionTimeout(60s) * 等都可以自定义,否则将使用默认值,其中上述括号中的就是默认值, * 我们往对应的setXXX()方法中点击一下就能够看到对应默认值了, * 下面的代码作用是说明一下如何设置我们想要的值 */ factory.setHost("localhost"); // 不使用try-with-resource语法的原因是“我们希望进程在消费者异步监听消息到达时始终保持活动状态”,不希望try中代码运行结束,就关闭Connection和Channel try { // 根据上面设置的参数来创建连接 Connection connection = factory.newConnection(); // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费 Channel channel = connection.createChannel(); // 创建死信交换机 channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT); // 创建死信队列 channel.queueDeclare(DEAD_QUEUE_NAME, true, false, false, null); // 绑定死信队列和死信交换机 channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, DEAD_QUEUE_NAME); // 消息创建中的回调对象;用来处理从队列异步推送过来的消息 DeliverCallback deliverCallback = (consumerTag, message) -> { String str = new String(message.getBody(), StandardCharsets.UTF_8); System.out.println(str); }; /** * 消费者异步监听队列消息 * 说明:队列会异步推送消息到我们这里,我们使用deliverCallback回调对象来接受消息 * 参数1:队列名称; * 指定消费者连接的队列 * 参数2:是否自动确认; * 如果自动确认,消费者接收消息之后(未处理消息)就会向RabbitMQ服务端发送确认消息,然后RabbitMQ就会删除对应消息 * 如果不自动确认,那我们就需要手动通知RabbitMQ服务端消息已经可以删除了,然后RabbitMQ服务端才会删除对应消息 * 参数3:消息传递时的回调对象 * 参数4:消费者被取消时的回调,暂时没有看懂啥意思 */ channel.basicConsume(DEAD_QUEUE_NAME, true, deliverCallback, consumerTag -> { }); } catch (Exception e) { e.printStackTrace(); } } }
1.3.4、说明
如果普通消费者通过 basicReject()
方法拒绝了消息,并且通过设置requeue参数为false,那代表不将消息放回队列,这样的话消息将成为死信,在上面的例子中,普通队列中的死信将传递给死信交换机,并且消息路由键是死信队列名称,这样的话,死信队列的消费者就可以接收到被拒绝的消息,画图说明如下:
2、优先级队列
2.1、生产者
public class Provider { // 普通交换机 public static final String COMMON_EXCHANGE_NAME = "common_exchange3"; // 普通队列 public static final String COMMON_QUEUE_NAME = "common_queue3"; public static void main(String[] args) { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); try ( Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); ) { // 声明交换机 channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT); // 消息发送 for (int i = 1; i <= 10; i++) { String message = "测试消息" + i; if (i == 5) { // 默认优先级都是0,设置5号消息的优先级是5 AMQP.BasicProperties properties = new AMQP.BasicProperties("text/plain", null, null, 2, 5, null, null, null, null, null, null, null, null, null).builder().build(); channel.basicPublish(COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME, properties, message.getBytes(StandardCharsets.UTF_8)); } else { channel.basicPublish(COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes(StandardCharsets.UTF_8)); } } } catch (Exception e) { e.printStackTrace(); } } }
2.2、消费者
public class Consumer { // 普通交换机 public static final String COMMON_EXCHANGE_NAME = "common_exchange3"; // 普通队列 public static final String COMMON_QUEUE_NAME = "common_queue3"; public static void main(String[] args) { // 创建连接工厂,配置连接相关设置 ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); try { // 根据上面设置的参数来创建连接 Connection connection = factory.newConnection(); // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费 Channel channel = connection.createChannel(); // 声明交换机 channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT); // 创建队列 Map<String, Object> arguments = new HashMap<>(1); arguments.put("x-max-priority", 10); channel.queueDeclare(COMMON_QUEUE_NAME, true, false, false, arguments); // 绑定普通队列和普通交换机 channel.queueBind(COMMON_QUEUE_NAME, COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME); // 消息创建中的回调对象;用来处理从队列异步推送过来的消息 DeliverCallback deliverCallback = (consumerTag, message) -> { String str = new String(message.getBody(), StandardCharsets.UTF_8); System.out.println(str); }; channel.basicConsume(COMMON_QUEUE_NAME, true, deliverCallback, consumerTag -> { }); } catch (Exception e) { e.printStackTrace(); } } }
2.3、说明
默认的消息优先级是0,其中消息优先级越大,在队列中越靠前,然后越早被发送,我们可以设置队列中的最大消息优先级,代码如下:
Map<String, Object> arguments = new HashMap<>(1); arguments.put("x-max-priority", 10); channel.queueDeclare(COMMON_QUEUE_NAME, true, false, false, arguments);
设置之后代表发送者发送的消息最大优先级不应该超过10,其实在rabbitmq官网上也说明了消息优先级应该是1~255之间的正整数,并且队列中设置的最大优先级不应该超过10,避免优先级过多的时候将会导致 Erlang 进程消耗更多的CPU 资源,然后我们可以在发送者发送消息的时候设置消息优先级,如下:
// 默认优先级都是0,设置5号消息的优先级是5 AMQP.BasicProperties properties = new AMQP.BasicProperties("text/plain", null, null, 2, 5, null, null, null, null, null, null, null, null, null).builder().build(); channel.basicPublish(COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME, properties, message.getBytes(StandardCharsets.UTF_8));
3、自定义延迟交换机
3.1、生产者
public class Provider { // 延时交换机 public static final String DELAY_EXCHANGE_NAME = "delay_exchange2"; // 普通队列 public static final String COMMON_QUEUE_NAME = "common_queue2"; public static void main(String[] args) { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); try ( Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); ) { // 创建延迟交换机;注意交换机类型是"x-delayed-message",另外该交换机本质上还是一个direct类型交换机 Map<String, Object> arguments = new HashMap<>(1); arguments.put("x-delayed-type", "direct"); channel.exchangeDeclare(DELAY_EXCHANGE_NAME, "x-delayed-message", true, false, arguments); // 10s消息;说明:10s消息应该比2s的消息更晚到达 String message = "我是10s的消息"; HashMap<String, Object> header = new HashMap<>(); // 设置延迟10s:这个延迟操作是在交换机中进行了,10s之后会将消息发送给消息队列,当然本次使用的交换机是延迟交换机 header.put("x-delay", 10000); AMQP.BasicProperties properties = new AMQP.BasicProperties("text/plain", null, null, 2, 0, null, null, null, null, null, null, null, null, null).builder().headers(header).build(); channel.basicPublish(DELAY_EXCHANGE_NAME, COMMON_QUEUE_NAME, properties, message.getBytes(StandardCharsets.UTF_8)); // 2s消息;说明:2s消息应该比10s的消息更早到达 message = "我是2s的消息"; header = new HashMap<>(); header.put("x-delay", 2000); properties = new AMQP.BasicProperties("text/plain", null, null, 2, 0, null, null, null, null, null, null, null, null, null).builder().headers(header).build(); channel.basicPublish(DELAY_EXCHANGE_NAME, COMMON_QUEUE_NAME, properties, message.getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { e.printStackTrace(); } } }
3.2、消费者
public class Consumer { // 延时交换机 public static final String DELAY_EXCHANGE_NAME = "delay_exchange2"; // 普通队列 public static final String COMMON_QUEUE_NAME = "common_queue2"; public static void main(String[] args) { // 创建连接工厂,配置连接相关设置 ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); try { // 根据上面设置的参数来创建连接 Connection connection = factory.newConnection(); // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费 Channel channel = connection.createChannel(); // 创建延迟交换机 Map<String, Object> arguments = new HashMap<>(1); arguments.put("x-delayed-type", "direct"); channel.exchangeDeclare(DELAY_EXCHANGE_NAME, "x-delayed-message", true, false, arguments); // 创建队列 channel.queueDeclare(COMMON_QUEUE_NAME, true, false, false, null); // 绑定普通队列和延迟交换机 channel.queueBind(COMMON_QUEUE_NAME, DELAY_EXCHANGE_NAME, COMMON_QUEUE_NAME); // 消息创建中的回调对象;用来处理从队列异步推送过来的消息 DeliverCallback deliverCallback = (consumerTag, message) -> { String str = new String(message.getBody(), StandardCharsets.UTF_8); System.out.println(str); }; channel.basicConsume(COMMON_QUEUE_NAME, true, deliverCallback, consumerTag -> { }); } catch (Exception e) { e.printStackTrace(); } } }
3.3、Windows中安装并启用延迟交换机插件
(1)首先从 Releases · rabbitmq/rabbitmq-delayed-message-exchange · GitHub 下载对应版本的插件
(2)解压到rabbitmq安装目录下面的plugins目录下面
(3)在rabbitmq安装目录下面的sbin目录下打开DOS窗口,然后执行 rabbitmq-plugins enable rabbitmq_delayed_message_exchange
就可以让插件起作用
(4)我们打开rabbitmq控制台之后,选择交换机,然后就可以看到如下的交换机类型
3.3、说明
在此之前,我们如果想实现延时效果,需要借助于过期时间和死信队列来打成,假设我们设置一个消息过期时间是2s,另外一个消息过期时间是10s,然后先将过期时间10s的消息发送到一个队列中,在将过期时间是2s的消息发送到同样队列中,然后让消息过期,之后通过死信交换机把消息交给死信队列,另外我们有消息着在监听着死信队列,我们肯定以为2s的消息会首先被消费,然后10s的消息在被消费,其实不是这样的,而是10s的消息被消费之后,2s的消息才被消费,原因就是队列是先进先出的,所以我们无法实现想要的效果,上面的文字用画图表示如下:
为了解决该问题,我们安装了延迟交换机插件,其中延时过程是在交换机中进行的,当延时结束之后就会将消息发送给消息队列,所以就不会产生上面的延时失败问题了,在代码编写过程中,可以设置交换机类型为 x-delayed-message
,即延时交换机,并且在消息头中设置延迟时间,如下所示:
// 创建延迟交换机;注意交换机类型是"x-delayed-message",另外该交换机本质上还是一个direct类型交换机 Map<String, Object> arguments = new HashMap<>(1); arguments.put("x-delayed-type", "direct"); channel.exchangeDeclare(DELAY_EXCHANGE_NAME, "x-delayed-message", true, false, arguments); // 10s消息;说明:10s消息应该比2s的消息更晚到达 String message = "我是10s的消息"; HashMap<String, Object> header = new HashMap<>(); // 设置延迟10s:这个延迟操作是在交换机中进行了,10s之后会将消息发送给消息队列,当然本次使用的交换机是延迟交换机 header.put("x-delay", 10000); AMQP.BasicProperties properties = new AMQP.BasicProperties("text/plain", null, null, 2, 0, null, null, null, null, null, null, null, null, null).builder().headers(header).build(); channel.basicPublish(DELAY_EXCHANGE_NAME, COMMON_QUEUE_NAME, properties, message.getBytes(StandardCharsets.UTF_8));
4、备用交换机
4.1、生产者
public class Provider { // 普通交换机 public static final String COMMON_EXCHANGE_NAME = "common_exchange5"; // 备用交换机 public static final String BACKUP_EXCHANGE_NAME = "backup_exchange5"; // 普通队列 public static final String COMMON_QUEUE_NAME = "common_queue5"; public static void main(String[] args) { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); try ( Connection connection = factory.newConnection(); Channel channel = connection.createChannel(); ) { // 声明普通交换机 Map<String, Object> arguments = new HashMap<>(); arguments.put("alternate-exchange", BACKUP_EXCHANGE_NAME); channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, arguments); // 消息发送 String message = "测试消息1"; channel.basicPublish(COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes(StandardCharsets.UTF_8)); // 消息发送(说明:我们故意将路由键写错,让交换机无法传递消息到普通队列,然后消息被被传递到了备用交换机中,然后我们的备用队列就可以收到消息了) message = "测试消息2"; channel.basicPublish(COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME + "1", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { e.printStackTrace(); } } }
4.2、普通消费者
public class Consumer1 { // 普通交换机 public static final String COMMON_EXCHANGE_NAME = "common_exchange5"; // 备用交换机 public static final String BACKUP_EXCHANGE_NAME = "backup_exchange5"; // 普通队列 public static final String COMMON_QUEUE_NAME = "common_queue5"; public static void main(String[] args) { // 创建连接工厂,配置连接相关设置 ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); try { // 根据上面设置的参数来创建连接 Connection connection = factory.newConnection(); // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费 Channel channel = connection.createChannel(); // 声明普通交换机 Map<String, Object> arguments = new HashMap<>(); arguments.put("alternate-exchange", BACKUP_EXCHANGE_NAME); channel.exchangeDeclare(COMMON_EXCHANGE_NAME, BuiltinExchangeType.DIRECT, true, false, arguments); // 声明备用交换机 channel.exchangeDeclare(BACKUP_EXCHANGE_NAME, BuiltinExchangeType.FANOUT); // 创建队列 channel.queueDeclare(COMMON_QUEUE_NAME, true, false, false, null); // 绑定死信队列和死信交换机 channel.queueBind(COMMON_QUEUE_NAME, COMMON_EXCHANGE_NAME, COMMON_QUEUE_NAME); // 消息创建中的回调对象;用来处理从队列异步推送过来的消息 DeliverCallback deliverCallback = (consumerTag, message) -> { String str = new java.lang.String(message.getBody(), StandardCharsets.UTF_8); System.out.println("普通队列接收到的消息:" + str); }; channel.basicConsume(COMMON_QUEUE_NAME, true, deliverCallback, consumerTag -> { }); } catch (Exception e) { e.printStackTrace(); } } }
4.3、备用消费者
public class Consumer2 { // 普通交换机 public static final String COMMON_EXCHANGE_NAME = "common_exchange5"; // 备用交换机 public static final String BACKUP_EXCHANGE_NAME = "backup_exchange5"; // 普通队列 public static final String COMMON_QUEUE_NAME = "common_queue5"; // 备用队列 public static final String BACKUP_QUEUE_NAME = "backup_queue5"; public static void main(String[] args) { // 创建连接工厂,配置连接相关设置 ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); try { // 根据上面设置的参数来创建连接 Connection connection = factory.newConnection(); // 创建通道;一个连接里面可以创建多个通道,而通道用于消息发送和接收,这样也可以减少资源浪费 Channel channel = connection.createChannel(); // 声明备用交换机 channel.exchangeDeclare(BACKUP_EXCHANGE_NAME, BuiltinExchangeType.FANOUT); // 创建队列 channel.queueDeclare(BACKUP_QUEUE_NAME, true, false, false, null); // 绑定死信队列和死信交换机 channel.queueBind(BACKUP_QUEUE_NAME, BACKUP_EXCHANGE_NAME, ""); // 消息创建中的回调对象;用来处理从队列异步推送过来的消息 DeliverCallback deliverCallback = (consumerTag, message) -> { String str = new String(message.getBody(), StandardCharsets.UTF_8); System.out.println("备用队列接收到的消息:" + str); }; channel.basicConsume(BACKUP_QUEUE_NAME, true, deliverCallback, consumerTag -> { }); } catch (Exception e) { e.printStackTrace(); } } }
4.4、说明
如果交换机无法把消息
发送给队列,比如通过消息路由键无法匹配到对应队列,那么在我们设置了普通交换机对应的备用交换机的前提下,消息将会发送给备用交换机,进而发送给备用队列,然后消费者就可以消费消息,过程画图如下: