死信队列其实和普通队列并没有什么区别,就是一个普通的队列,当消息“死掉”成为 Dead Message 之后,就会被重新发送到一个交换机,这个交换机就是死信交换机,与死信交换机绑定的队列就是死信队列,死信交换机简称 DLX ,Dead Letter Exchange
简单来说就是无法被消费的消息会成为死信,那么消息成为死信有下面几种情况
消费者使用 basic.reject 和 basic.nack 拒绝签收消息,并且配置 requeue 参数是 false(即不重回原来的队列)
例如,当消费者端开启了消息的手动确认,消费者端在接收到消息之后,不想消费当前消息,进行消息拒绝签收操作,消息就会成为死信
消息在队列中的时间超过配置的 TTL 存活时间
例如:队列配置消息存活时间为 5s,那么消息在 5s 内未被消费就会成为死信
队列中消息的数量超过最大长度
例如,配置队列最大消息长度为1,当队列消息超过1时,消息成为死信
所有的普通队列都可以配置死信交换机
当前我们基于一个普通的订单队列来进行模拟,我们给订单队列配置一个死信交换机,通过模拟订单队列出现的异常情况,使消息变成死信,进入死信队列
@Configuration
public class DeadLetterConfig {
// 订单队列
public static final String ORDER_QUEUE = "order_queue";
// 订单交换机
public static final String ORDER_EXCHANGE = "order_exchange";
/**
* 订单队列
*/
@Bean
public Queue orderQueue() {
Map map = new HashMap<>(2);
// 绑定该队列到死信交换机
map.put("x-dead-letter-exchange", DEAD_EXCHANGE);
// // 消息 RoutingKey 路由模式需要设置,当前使用发布订阅模式不需要配置 routingKey
// map.put("x-dead-letter-routing-key","routingKey");
return new Queue(ORDER_QUEUE, true, false, false, map);
}
/**
* 订单交换机
*/
@Bean
public FanoutExchange orderExchange() {
return new FanoutExchange(ORDER_EXCHANGE);
}
/**
* 订单队列和交换机绑定
*/
@Bean
public Binding bindingOrderExchange() {
return BindingBuilder.bind(orderQueue()).to(orderExchange());
}
// ******************************* 死信队列 ************************************
// 死信队列
public static final String DEAD_QUEUE = "dead_queue";
// 死信交换机
public static final String DEAD_EXCHANGE = "dead_exchange";
/**
* 死信队列
*/
@Bean
public Queue deadQueue() {
return new Queue(DEAD_QUEUE);
}
/**
* 死信交换机
*/
@Bean
public FanoutExchange deadExchange() {
return new FanoutExchange(DEAD_EXCHANGE);
}
/**
* 死信队列和死信交换机绑定
*/
@Bean
public Binding bindingDeadExchange() {
return BindingBuilder.bind(deadQueue()).to(deadExchange());
}
}
流程:订单生产者发送一条消息,订单消费者进行消息拒收,并且设置消息不重回队列,此时消息会变成死信,监听死信队列的消费者会消费到这条消息
订单生产者发送消息
@RequestMapping("/orderSend")
public void orderSend() {
String message = "order Message";
log.info("订单队列生产者发送消息 :{}", message);
rabbitTemplate.convertAndSend(DeadLetterConfig.ORDER_EXCHANGE, "", message);
}
订单消费者拒收消息
/**
* 订单队列消费者
*
* @param message 消息内容
*/
@RabbitListener(queues = DeadLetterConfig.ORDER_QUEUE)
public void orderConsumer(Message message, Channel channel) throws IOException {
log.info("订单队列消费者接收到消息:{}", message);
long deliveryTag = message.getMessageProperties().getDeliveryTag();
channel.basicNack(deliveryTag,false,false);
}
死信队列
/**
* 死信队列消费者
*
* @param message 消息内容
*/
@RabbitListener(queues = DeadLetterConfig.DEAD_QUEUE)
public void deadConsumer(Message message, Channel channel) throws IOException {
log.info("死信队列消费者接收到消息:{}", message);
long deliveryTag = message.getMessageProperties().getDeliveryTag();
channel.basicAck(deliveryTag,false);
}
这里模拟,订单消费者使用了 basicNack ,使用 basicReject 也是一样的,消息会进入死信队列
流程:给订单队列配置一个 TTL 过期时间,过期时间设置为3秒,删除原本的订单队列,注释掉订单消费者的代码,通过订单生产者发送一条消息,当消息在过期时间内未被消费,消息进入死信队列
更改 DeadLetterConfig 配置文件中 订单队列的配置
/**
* 订单队列
*/
@Bean
public Queue orderQueue() {
Map map = new HashMap<>(2);
// 绑定该队列到死信交换机
map.put("x-dead-letter-exchange", DEAD_EXCHANGE);
// // 消息 RoutingKey 路由模式需要设置,当前使用发布订阅模式不需要配置 routingKey
// map.put("x-dead-letter-routing-key","routingKey");
// 消息3秒后过期
map.put("x-message-ttl",3000);
return new Queue(ORDER_QUEUE, true, false, false, map);
}
注意:这里更改队列配置之后,要先在 rabbitMq 的 web 管理页面先删除原来的 order_queue 队列,否则直接重启会报错,需要先删除,然后让程序重新创建队列
改变队列配置之后,不手动删除原本的订单队列就会报以下截图错误
给订单队列设置了 TTL 过期时间之后,重新创建的订单队列会有一个 TTL 的标识
注释掉订单消费者的代码,订单生产者发送一条消息,三秒之后未被消费,消息进入死信队列
流程:给订单队列配置一个 Lim 队列最大未消费消息长度,长度设置为2,删除原本的订单队列,注释掉订单消费者的代码,通过订单生产者发送两条消息,当队列中未被消费消息到达两条时,第二条消息进入死信队列
更改 DeadLetterConfig 配置文件中 订单队列的配置
/**
* 订单队列
*/
@Bean
public Queue orderQueue() {
Map map = new HashMap<>(2);
// 绑定该队列到死信交换机
map.put("x-dead-letter-exchange", DEAD_EXCHANGE);
// // 消息 RoutingKey 路由模式需要设置,当前使用发布订阅模式不需要配置 routingKey
// map.put("x-dead-letter-routing-key","routingKey");
// // 消息3秒后过期
// map.put("x-message-ttl",3000);
// 设置队列的最大长度值为1
map.put("x-max-length", 1);
return new Queue(ORDER_QUEUE, true, false, false, map);
}
通过订单生产者发送两条消息,记得要注释掉订单消费者的代码,当第二条消息进入队列时,第二条变成死信,会将第二条消息发送到死信队列
什么是延时队列?
延时队列就是,放在这个队列里面的消息不像普通队列一样,一有消息就要马上消费掉,需要延期一段时间再消费
为什么需要延时队列?
如果你用过唯品会,就会发现它有这样一种功能,我们将商品加入到购物车之后,你会发现,20分钟之后,这个商品就会从你的购物车删掉,也就是说,它会自动清理20分钟内购物车没有下单的商品
还有一些其他场景,例如
订单下单30分钟之后没有付款,自动取消订单
会议开始前15分钟前提醒等等
像诸如此类的场景,我们就可以通过延时队列来实现,那么我们接下来使用 RabbitMq 通过两种方式来实现延时队列
流程:创建一个订单队列,订单队列配置 TTL 过期时间为3秒,配置死信队列,不需要监听订单队列的消费者,需要监听订单队列设置的死信队列
DeadLetterConfig 配置文件基本一致
@Configuration
public class DeadLetterConfig {
// 订单队列
public static final String ORDER_QUEUE = "order_queue";
// 订单交换机
public static final String ORDER_EXCHANGE = "order_exchange";
/**
* 订单队列
*/
@Bean
public Queue orderQueue() {
Map map = new HashMap<>(2);
// 绑定该队列到死信交换机
map.put("x-dead-letter-exchange", DEAD_EXCHANGE);
// // 消息 RoutingKey 路由模式需要设置,当前使用发布订阅模式不需要配置 routingKey
// map.put("x-dead-letter-routing-key","routingKey");
// 消息3秒后过期
map.put("x-message-ttl",3000);
return new Queue(ORDER_QUEUE, true, false, false, map);
}
/**
* 订单交换机
*/
@Bean
public FanoutExchange orderExchange() {
return new FanoutExchange(ORDER_EXCHANGE);
}
/**
* 订单队列和死信交换机绑定
*/
@Bean
public Binding bindingOrderExchange() {
return BindingBuilder.bind(orderQueue()).to(orderExchange());
}
// ******************************* 死信队列 ************************************
// 死信队列
public static final String DEAD_QUEUE = "dead_queue";
// 死信交换机
public static final String DEAD_EXCHANGE = "dead_exchange";
/**
* 死信队列
*/
@Bean
public Queue deadQueue() {
return new Queue(DEAD_QUEUE);
}
/**
* 死信交换机
*/
@Bean
public FanoutExchange deadExchange() {
return new FanoutExchange(DEAD_EXCHANGE);
}
/**
* 死信队列和死信交换机绑定
*/
@Bean
public Binding bindingDeadExchange() {
return BindingBuilder.bind(deadQueue()).to(deadExchange());
}
}
订单生产者发送消息
@RequestMapping("/orderSend")
public void orderSend() {
String message = "order Message";
log.info("订单队列生产者发送消息 :{}", message);
rabbitTemplate.convertAndSend(DeadLetterConfig.ORDER_EXCHANGE, "", message);
}
注意:这里我们就不用再监听订单的的队列了,直接监听死信队列即可,将我们的业务代码在死信队列消费者中实现
/**
* 死信队列消费者
*
* @param message 消息内容
*/
@RabbitListener(queues = DeadLetterConfig.DEAD_QUEUE)
public void deadConsumer(Message message, Channel channel) throws IOException {
log.info("死信队列消费者接收到消息:{}", message);
long deliveryTag = message.getMessageProperties().getDeliveryTag();
// todo 订单业务处理,判断改订单状态是否已支付,如果未支付,删除该订单,已支付则不做操作
channel.basicAck(deliveryTag,false);
}
原理
其实这种方式就是,根据我们自己的业务员需求,设置队列内消息的过期时间,只是我这里测试设定的3秒过期,假定订单20分钟未支付取消订单,那么我们设定队列过期时间为20分钟,指定对应的死信队列就可以,然后监听死信队列,在消费死信队列中消息做具体业务逻辑即可
那么这种方式会存在一个问题,假如我们有很多订单,那就需要设定很多队列,每个队列指定过期时间
接下来解决这个问题
我们动态指定生产者每一条消息的 TTL 过期时间,这样就不用创建许多队列了,一个队列,每条消息 TTL 时间不一致
更改 DeadLetterConfig 配置文件
去掉队列的 TTL 配置
/**
* 订单队列
*/
@Bean
public Queue orderQueue() {
Map map = new HashMap<>(2);
// 绑定该队列到死信交换机
map.put("x-dead-letter-exchange", DEAD_EXCHANGE);
// // 消息 RoutingKey 路由模式需要设置,当前使用发布订阅模式不需要配置 routingKey
// map.put("x-dead-letter-routing-key","routingKey");
return new Queue(ORDER_QUEUE, true, false, false, map);
}
订单生产者
生产消息时,指定消息的 TTL 时间为 3秒
@RequestMapping("/orderSend")
public void orderSend() {
String message = "order Message";
log.info("订单队列生产者发送消息 :{}", message);
// rabbitTemplate.convertAndSend(DeadLetterConfig.ORDER_EXCHANGE, "", message);
// 设置队列单条消息的 TTL 过期时间
rabbitTemplate.convertAndSend(DeadLetterConfig.ORDER_EXCHANGE, "", message, message1 -> {
message1.getMessageProperties().setExpiration("3000");
return message1;
}
);
}
重新创建队列之后,TTL 的标识就没有了
我们重新测试,一样可以达到3秒过期的效果
注意:这里我们虽然设置队列中每条消息的 TTL 过期时间比较灵活方便,但是这种方式会存在一个问题,假如,我们往队列中写入两条消息,第一条A消息过期时间为5秒,第二条B消息过期时间为3秒,按照正常逻辑,我们希望3秒的消息,先进入死信队列,然后5秒的消息再进入死信队列,但是事实可不是这样,因为消息是先进先出,所以我们会看到等到 A 消息过期之后,B消息跟着马上过期
这里测试,复制一个订单生产者接口出来,分别设置5秒,3秒过期
然后我们先调用 /orderSend,再调用 /orderSend2,就会出现这种情况
下载地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/tags
这里我就选择 3.9 的版本下载了
如果版本不对应,安装时会提示
这里我们,点到 3.9.0 版本里面去下载,选择 .ez 后缀的文件,如果服务器能直接下载,直接复制链接在线下载,不能在线就先下载后面再上传到服务器
下载命令
wget https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/3.9.0/rabbitmq_delayed_message_exchange-3.9.0.ez
注意要下载到你的 RabbitMq 插件目录
这是我的目录,可以参考
直接在线下载
执行命令
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
创建配置文件,注意这里的交换机类型
@Configuration
public class DelayConfig {
// 延时队列
public static final String DELAY_QUEUE = "delay_queue";
// 延时交换机
public static final String DELAY_EXCHANGE = "delay_exchange";
public static final String DELAY_KEY = "delay_key";
/**
* 延时队列
*/
@Bean
public Queue delayQueue() {
return new Queue(DELAY_QUEUE, true);
}
/**
* 延时交换机
*/
@Bean
public CustomExchange delayExchange() {
Map<String, Object> map = new HashMap<>();
map.put("x-delayed-type", "direct");
// 交换机名称 交换机类型 是否持久化 是否自动删除 配置参数
return new CustomExchange(DELAY_EXCHANGE,"x-delayed-message",true,false,map);
}
/**
* 延时队列和交换机绑定
*/
@Bean
public Binding bindingDelayExchange() {
return BindingBuilder.bind(delayQueue()).to(delayExchange()).with(DELAY_KEY).noargs();
}
}
延时队列生产者
@RequestMapping("/delaySend")
public void delaySend() {
String message = "delay Message";
log.info("延时队列生产者发送消息 :{}", message);
// 设置队列单条消息的延期时间
rabbitTemplate.convertAndSend(DelayConfig.DELAY_EXCHANGE, DelayConfig.DELAY_KEY, message, message1 -> {
message1.getMessageProperties().setDelay(5000);
return message1;
}
);
}
延时队列消费者
/**
* 延时队列消费者
*
* @param message 消息内容
*/
@RabbitListener(queues = DelayConfig.DELAY_QUEUE)
public void delayConsumer(Message message, Channel channel) throws IOException {
log.info("延时队列消费者接收到消息:{}", message);
long deliveryTag = message.getMessageProperties().getDeliveryTag();
// todo 订单业务处理,判断改订单状态是否已支付,如果未支付,删除该订单,已支付则不做操作
channel.basicAck(deliveryTag,false);
}
注意
我们再次模拟 二、1.2 步骤中,我们最后模拟的单条消息 TTL 场景
为了测试,新增一个3秒延时消息接口
然后先发送5秒延时消息,再发送3秒延时消息,这里可以看到 3秒的延时消息,优先被消费到了,然后5秒的延时消息被消费,由此可见,使用延时插件,就避免了我们通过死信队列设置单条消息 TTL 实现延时队列的缺陷。
注意,看代码的话,请仔细一点
https://github.com/wxwhowever/springboot-notes/tree/master/rabbitmq-demo