死信队列概念
死信队列是指消息被投递到队列后,由于各种原因导致队列中的消息无法被消费掉,这样的消息如果没有后续处理就变成了死信,有死信自然就有了死信队列。
业务场景
为了保证订单消息不丢失,需要使用到RabbitMq的死信队列机制,当消息消费异常时,就把消息放到死信队列中。
用户在商城下单成功后在指定时间内未支付时订单自动取消。
死信来源
消息TTL过期
队列达到最大长度(队列满了,无法再添加到队列中)
消息被拒绝(basic.reject或basic.nack)
死信队列代码示例
死信队列代码示例图
生产者
package dead;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import utils.RabbitUtils;
public class DeadProduct {
public static void main(String[] args) throws Exception{
String normalExchange = "normalExchange";
Channel channel = RabbitUtils.getChannel();
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10").build();
channel.basicPublish(normalExchange, "zhangsan", null, "111AAA".getBytes()); //Q1队列接收
System.out.println("消息发送完成");
}
}
正常队列的消费者
在这里是通过设置正常队列的“x-max-length”属性限制正常队列的最大长度,令正常队列的消息超过最大长度时消息会从正常队列转到死信队列。还有一种消费者在消费消息时应答拒绝,令消息从正常队列进入到死信队列。至于TTL消息过期的方式,我无法测试成功。
package dead;
import com.rabbitmq.client.*;
import utils.RabbitUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class DeadConsumer1 {
public static void main(String[] args) throws Exception {
String normalQueue = "normal_queue";
String deadQueue = "dead_queue";
String normalExchange = "normalExchange";
String deadExchange = "deadExchange";
Channel channel = RabbitUtils.getChannel();
// 设置死信队列的参数,然后在普通队列中把这些参数设置进去,RabbitMq当发现普通队列的消息过期或无法被消费后就会根据这些规则把消息转发到死信队列
Map<String, Object> arguments = new HashMap<>();
// TTL过期时间10秒 = 10000ms毫秒 一般都是生产者发送消息时再指定过期时间
// arguments.put("x-message-ttl", 10000);
// 设置过期之后死信交换机
arguments.put("x-dead-letter-exchange", deadExchange);
// 设置死信路由
arguments.put("x-dead-letter-routing-key", "lisi");
// 设置队列最多长度,当普通队列中的消息数量超过3个之后,新增的消息会放到死信队列
arguments.put("x-max-length", 3);
// 表示开启信道的发布确认
channel.confirmSelect();
// 声明一个普通队列
channel.queueDeclare(normalQueue, true, false, false, arguments);
// 声明一个普通交换机是DIRECT类型
channel.exchangeDeclare(normalExchange, BuiltinExchangeType.DIRECT);
// 把交换机和队列进行绑定binding
channel.queueBind(normalQueue, normalExchange, "zhangsan");
// 声明一个死信交换机是DIRECT类型
channel.exchangeDeclare(deadExchange, BuiltinExchangeType.DIRECT);
// 声明一个死信队列
channel.queueDeclare(deadQueue, true, false, false, null);
// 把死信交换机和死信队列进行绑定binding
channel.queueBind(deadQueue, deadExchange, "lisi");
System.out.println("DeadConsumer1,在等待消息:");
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
System.out.println(Thread.currentThread().getName() + "接收到了消息:" + new String(message.getBody()));
// 消息手动应答拒绝
channel.basicNack(message.getEnvelope().getDeliveryTag(), false, false);
System.out.println("拒绝应答");
}
};
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
System.out.println(Thread.currentThread().getName()+"取消发送了");
}
};
System.out.println(Thread.currentThread().getId() + "消费者正在等待消息----");
/**
* 第一个参数是队列名称
* 第二个参数表示是否自动应答,如果是true,则无需手动应答,如果是false,则需要手动应答;
* 手动应答是消费者处理完本条消息,然后会向mq服务器进行消息确认,允许mq服务器在队列中把消息删除;一旦消费者没有向mq服务器进行确认,即使消费者断开后;
* mq服务器也会把消息重新入到mq队列,给其他的消费者消费。
* 生产上一般都开启手动应答;自动应答存在消费者还没处理完消息时,队列清空了消息,导致消息无法重新入队,有消息丢失的风险;
*/
channel.basicConsume(normalQueue, false, deliverCallback, cancelCallback);
}
}
延迟队列
延迟队列和死信队列差不多,只是无需消费者消费正常队列的消息,然后通过TTL消息过期机制让消息从正常队列进入到死信队列的这种思路来实现延迟队列的需求。
延迟队列使用场景
订单在多少分钟内未支付则自动取消
新建店铺在多少天内未上传商品,进行提醒
预定会议后,在会议开始前10分钟通知参会人员参加会议
实际上就是需要在某个事件发生之后或之前的指定时间内完成某些任务
延迟队列代码示例
pom依赖,代码示例是springBoot工程,需要依赖springBoot和spring-boot-start-amqp(rabbitMq整合SpringBoot)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置类:
package springbootrabbitmq.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TtlQueueConfig {
// 普通交换机名称
public static final String X_EXCHANGE = "X";
// 死信交换机名称
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
// 普通队列名称
public static final String QUEUE_A = "QA";
public static final String QUEUE_B = "QB";
public static final String DEAD_LETTER_QUEUE = "QD";
public static final String QUEUE_C = "QC";
// 声明交换机
@Bean("xExchange")
public DirectExchange xExchange() {
return new DirectExchange(X_EXCHANGE);
}
// 声明交换机
@Bean("yExchange")
public DirectExchange yExchange() {
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
//普通队列A如果消息不消费之后与死信队列QD关联起来
@Bean("queueA")
public Queue queueA() {
Queue queue = QueueBuilder.durable(QUEUE_A)
.ttl(10000)
.deadLetterExchange(Y_DEAD_LETTER_EXCHANGE)
.deadLetterRoutingKey("YD")
.build();
return queue;
}
//普通队列B如果消息不消费之后与死信队列QD关联起来
@Bean("queueB")
public Queue queueB() {
Queue queue = QueueBuilder.durable(QUEUE_B)
.ttl(40000)
.deadLetterExchange(Y_DEAD_LETTER_EXCHANGE)
.deadLetterRoutingKey("YD")
.build();
return queue;
}
//普通队列C如果消息不消费之后与死信队列QD关联起来
@Bean("queueC")
public Queue queueC() {
Queue queue = QueueBuilder.durable(QUEUE_C)
.deadLetterExchange(Y_DEAD_LETTER_EXCHANGE)
.deadLetterRoutingKey("YD")
.build();
return queue;
}
// 死信队列
@Bean("queueD")
public Queue queueD() {
Queue queue = QueueBuilder.durable(DEAD_LETTER_QUEUE)
.build();
return queue;
}
//把队列与交换机绑定
@Bean
public Binding queueABindingX(@Qualifier("queueA") Queue queueA, @Qualifier("xExchange") DirectExchange xExchange) {
return BindingBuilder.bind(queueA).to(xExchange).with("XA");
}
@Bean
public Binding queueBBindingX(@Qualifier("queueB") Queue queueB, @Qualifier("xExchange") DirectExchange xExchange) {
return BindingBuilder.bind(queueB).to(xExchange).with("XB");
}
@Bean
public Binding queueDBindingY(@Qualifier("queueD") Queue queueD, @Qualifier("yExchange") DirectExchange yExchange) {
return BindingBuilder.bind(queueD).to(yExchange).with("YD");
}
@Bean
public Binding queueCBindingX(@Qualifier("queueC") Queue queueC, @Qualifier("xExchange") DirectExchange xExchange) {
return BindingBuilder.bind(queueC).to(xExchange).with("XC");
}
}
死信队列监听器(消费者)
package springbootrabbitmq.consumer;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
@Slf4j
public class DeadLetterQueueConsumer {
//监听器接收消息
@RabbitListener(queues ="QD")
public void receiveD(Message message, Channel channel) {
String msg = new String(message.getBody());
log.info("当前时间:{}, 收到一条消息:{} 到TTL队列", new Date().toString(), msg);
}
}
生产者
package springbootrabbitmq.controller;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import springbootrabbitmq.config.TtlQueueConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMsgController {
@Autowired
private RabbitTemplate rabbitTemplate;
//把消息发送到队列,通队列的过期时间让消息转到死信队列
@GetMapping("/sendMsg/{message}")
public String sendMsg(@PathVariable String message) {
log.info("当前时间:{}, 发送一条消息:{} 到TTL队列", new Date().toString(), message);
rabbitTemplate.convertAndSend(TtlQueueConfig.X_EXCHANGE, "XA", message);
rabbitTemplate.convertAndSend(TtlQueueConfig.X_EXCHANGE, "XB", message);
return "success";
}
// 通过生产者发送消息时设置过期时间让消息转到死信队列
@GetMapping("/sendMsg2/{message}/{ttlTime}")
public String sendMsg2(@PathVariable String message, @PathVariable String ttlTime) {
log.info("当前时间:{}, 发送一条消息:{} 到TTL队列, 过期时间:{} ", new Date().toString(), message, ttlTime);
// 设置过期时间
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration(ttlTime);
return message;
}
};
rabbitTemplate.convertAndSend(TtlQueueConfig.X_EXCHANGE, "XC", message, messagePostProcessor);
return "success";
}
}
测试结果:
当前时间:Wed Jan 25 20:48:35 CST 2023, 发送一条消息:9998 到TTL队列, 过期时间:20000
当前时间:Wed Jan 25 20:48:40 CST 2023, 发送一条消息:9997 到TTL队列, 过期时间:2000
当前时间:Wed Jan 25 20:48:55 CST 2023, 收到一条消息:9998 到TTL队列
当前时间:Wed Jan 25 20:48:55 CST 2023, 收到一条消息:9997 到TTL队列
当前时间:Wed Jan 25 21:29:06 CST 2023, 发送一条消息:9999 到TTL队列
当前时间:Wed Jan 25 21:29:16 CST 2023, 收到一条消息:9999 到TTL队列
当前时间:Wed Jan 25 21:29:46 CST 2023, 收到一条消息:9999 到TTL队列
从上面的测试结果来看:
如果是调用了sendMsg2/{message}/{ttlTime} 接口,第一次先设置过期时间为20秒,第二次请求设置过期时间为2秒,消费者在收到消息的时候是先等第一个消息20秒后进入到死信队列,第二个消息才会进入到死信队列,正常来说我们是希望2秒后过期的先被消费,20秒后过期的后被消费,这是由于RabbitMq对一个队列中设置过期时间的监控问题导致的,我们可以通过使用插件(rabbitmq_delayed_message_exchange插件)的方式实现我们希望的效果。
如果是调用了/sendMsg/{message}接口,就会看到消息会按消息队列设置的固定的过期时间发送到死信队列,如果消息的过期时间都是相同的可以直接使用这种方式,设置队列的过期时间即可。
关于延迟队列的实现小结:如果是固定的过期时间则可以通过队列设置过期时间的方式实现。当过期时间不固定且有很多的过期时间且不希望有过多的过期队列时则可以使用插件的方式(rabbitmq_delayed_message_exchange插件)实现延迟队列的效果。一定不能通过设置生产者的过期时间来实现消息的任意过期时间的目的,否则就会出现我们示例中的问题。