目录
一、死信队列
1.1 概念
1.2 来源
1.3 演示
二、延迟队列
2.1 TTL-消息最大存活时间
2.2 在SpringBoot中演示延迟队列与死信队列
2.2.1 基本演示
2.2.2 优化-动态设置TTL
2.2.3 使用插件实现延迟队列
2.3 总结
死信顾名思义就是无法被消费的消息,一般来说,producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息 进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息的消费发生异常时,将消息投入死信队列中。还有比如用户在商城下单成功并点击去支付后在指定时间未支付时自动失效。
1.消息 TTL 过期
2.队列达到最大长度(队列满了,无法再添加数据到 mq 中)
3.消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false.
(1)消息TTL过期
Producer:
class Producer implements Callable {
private static final String NORMAL_EXCHANGE = "normal_exchange";
@Override
public Object call() 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);
}
return null;
}
}
C1:
class C1 implements Callable {
//普通交换机名称
private static final String NORMAL_EXCHANGE = "normal_exchange";
//死信交换机名称
private static final String DEAD_EXCHANGE = "dead_exchange";
@Override
public Object call() throws Exception {
Channel channel = rabbitMQUtils.getChannel();
//声明死信和普通交换机 类型为 direct
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
//声明死信队列
channel.queueDeclare("dead-queue", false, false, false, null);
//死信队列绑定死信交换机与 routingkey
channel.queueBind("dead-queue", DEAD_EXCHANGE, "lisi");
//正常队列绑定死信队列信息
Map params = new HashMap<>();
//正常队列设置死信交换机 参数 key 是固定值
params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//正常队列设置死信 routing-key 参数 key 是固定值
params.put("x-dead-letter-routing-key", "lisi");
//正常队列绑定正常交换机
channel.queueDeclare("normal-queue", false, false, false, params);
channel.queueBind("normal-queue", NORMAL_EXCHANGE, "zhangsan");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
System.out.println("Consumer01 接收到消息"+message);
};
Thread.sleep(1000000);
channel.basicConsume("normal-queue", true, deliverCallback, consumerTag -> {
});
return null;
}
}
C2:
class C2 implements Callable{
private static final String DEAD_EXCHANGE = "dead_exchange";
@Override
public Object call() throws Exception {
Channel channel = rabbitMQUtils.getChannel();
//声明死信交换机
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
//声明死信队列
channel.queueDeclare("dead-queue", false, false, false, null);
//绑定死信交换机
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("Consumer02 接收死信队列的消息" + message);
};
channel.basicConsume("dead-queue", true, deliverCallback, consumerTag -> {
});
return null;
}
}
测试:
public static void main(String[] args) throws InterruptedException {
ExecutorService service= Executors.newFixedThreadPool(10);
service.submit(new C1());
service.submit(new C2() {
});
Thread.sleep(1000);
service.submit(new Producer());
}
//结果:
等待接收死信队列消息.....
等待接收消息.....
生产者发送消息:info1
生产者发送消息:info2
生产者发送消息:info3
生产者发送消息:info4
生产者发送消息:info5
生产者发送消息:info6
生产者发送消息:info7
生产者发送消息:info8
生产者发送消息:info9
生产者发送消息:info10
Consumer02 接收死信队列的消息info1
Consumer02 接收死信队列的消息info2
Consumer02 接收死信队列的消息info3
Consumer02 接收死信队列的消息info4
Consumer02 接收死信队列的消息info5
Consumer02 接收死信队列的消息info6
Consumer02 接收死信队列的消息info7
Consumer02 接收死信队列的消息info8
Consumer02 接收死信队列的消息info9
Consumer02 接收死信队列的消息info10
解读:Producer给C1发送的消息生存时间是10s,而C1的代码中我让这个线程延时100s,相当于把TTL耗完了。这些过期的消息都经过死信交换机处理,最终送到了C2手中。
(2)队列达到最大长度
需要修改两处:
1.Producer中删除TTL的配置,修改后如下
class Producer implements Callable {
private static final String NORMAL_EXCHANGE = "normal_exchange";
@Override
public Object call() throws Exception {
Channel channel = rabbitMQUtils.getChannel();
//声明交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
//该信息是用作演示队列个数限制
for (int i = 1; i <11 ; i++) {
String message="info"+i;
channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", null, message.getBytes());
System.out.println("生产者发送消息:"+message);
}
return null;
}
}
2.C1中添加新的参数
//设置队列长度限制为6
params.put("x-max-length",6);
其他代码不变,删掉之前的Queue,再次测试:
//结果:
等待接收死信队列消息.....
等待接收消息.....
生产者发送消息:info1
生产者发送消息:info2
生产者发送消息:info3
生产者发送消息:info4
生产者发送消息:info5
生产者发送消息:info6
生产者发送消息:info7
生产者发送消息:info8
生产者发送消息:info9
生产者发送消息:info10
Consumer02 接收死信队列的消息info1
Consumer02 接收死信队列的消息info2
Consumer02 接收死信队列的消息info3
Consumer02 接收死信队列的消息info4
可见,C1队列满之后,多余的消息都被C2接收了
(3)消息被拒绝
Producer代码不变,只修改C1的代码,将某条消息拒绝掉:
class Consumer01 implements Callable {
//普通交换机名称
private static final String NORMAL_EXCHANGE = "normal_exchange";
//死信交换机名称
private static final String DEAD_EXCHANGE = "dead_exchange";
@Override
public Object call() throws Exception {
Channel channel = rabbitMQUtils.getChannel();
//声明死信和普通交换机 类型为 direct
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
//声明死信队列
channel.queueDeclare("dead-queue", false, false, false, null);
//死信队列绑定死信交换机与 routingkey
channel.queueBind("dead-queue", DEAD_EXCHANGE, "lisi");
//正常队列绑定死信队列信息
Map params = new HashMap<>();
//正常队列设置死信交换机 参数 key 是固定值
params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//正常队列设置死信 routing-key 参数 key 是固定值
params.put("x-dead-letter-routing-key", "lisi");
//设置队列长度限制为6
params.put("x-max-length",6);
//正常队列绑定正常交换机
channel.queueDeclare("normal-queue", false, false, false, params);
channel.queueBind("normal-queue", NORMAL_EXCHANGE, "zhangsan");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
if(message.equals("info5")){
System.out.println("Consumer01 接收到消息" + message + "并拒绝签收该消息");
//requeue 设置为 false 代表拒绝重新入队 该队列如果配置了死信交换机将发送到死信队列中
channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false);
}else {
System.out.println("Consumer01 接收到消息"+message);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
Thread.sleep(1000000);
channel.basicConsume("normal-queue", true, deliverCallback, consumerTag -> {
});
return null;
}
}
注:因为C1的线程我不知道怎么关闭,所以没办法完全演示C1挂掉的情况,流程其实就是info5被C1拒绝,且C1挂了,那么info5将被死信交换机处理,最终送到C2手中。
结果:
等待接收死信队列消息.....
等待接收消息.....
生产者发送消息:info1
生产者发送消息:info2
生产者发送消息:info3
生产者发送消息:info4
生产者发送消息:info5
生产者发送消息:info6
生产者发送消息:info7
生产者发送消息:info8
生产者发送消息:info9
生产者发送消息:info10
Consumer01 接收到消息info1
Consumer01 接收到消息info2
Consumer01 接收到消息info3
Consumer01 接收到消息info4
Consumer01 接收到消息info5并拒绝签收该消息
Consumer01 接收到消息info6
Consumer01 接收到消息info7
Consumer01 接收到消息info8
Consumer01 接收到消息info9
Consumer01 接收到消息info10
Consumer02 接收到死信队列的消息info5
延时队列的内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
适用场景:
1.订单在十分钟之内未支付则自动取消
2.新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
3.用户注册成功后,如果三天内没有登陆则进行短信提醒。
4.用户发起退款,如果三天内没有得到处理则通知相关运营人员。
5.预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
对于这些场景,我们其实可以采用轮询的方式定时查询。但如果数据量、并发量巨大,对于数据库来说将有很大的压力,导致性能低下。
TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒。换句话说,如果一条消息设置了 TTL 属性或者进入了设置 TTL 属性的队列,那么这 条消息如果在 TTL 设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的 TTL 和消息的 TTL,那么较小的值将会被使用。
有两种方式设置 TTL:
1.消息设置
... ...
channel.basicPublish(exchangeName,routingKey,mandatory,
new AMQP.BasicProperties().builder().expiration("6000").build(),msg.getBytes());
... ...
2.队列设置
... ...
Map args = new HashMap();
args.put("x-message-ttl",6000);//延时6秒
channel.queueDeclare(queueName, durable, exclusive, autoDelete, args);
... ...
两种方式的对比:
对于队列设置,消息一旦过期会被立刻抹掉,因为队列中已过期的消息肯定在队列头部,RabbitMQ只要定期从队头开始扫描是否有过期消息即可。
对于消息设置,即使消息过期,也不会马上从队列中抹去,因为每条消息是否过期时在即将投递到消费者之前判定的,每条消息的过期时间不同,如果要删除所有过期消息,势必要扫描整个队列,但RabbitMQ不会这样做。
1.引入依赖
org.springframework.boot
spring-boot-starter-amqp
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.projectlombok
lombok
io.springfox
springfox-swagger2
2.9.2
io.springfox
springfox-swagger-ui
2.9.2
org.springframework.amqp
spring-rabbit-test
test
2.配置application.yml
spring:
rabbitmq:
host: 192.168.80.128
port: 5672
username: admin
password: 123
如图,创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后在创建一个交换机 X 和死信交 换机 Y,它们的类型都是 direct,创建一个死信队列 QD 及绑定关系.
3.配置类
@Configuration
public class TTLQueueConfig {
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 X_EXCHANGE = "X";
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
//声明交换机X
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
//声明交换机Y
@Bean("yExchange")
public DirectExchange yExchange(){
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
//声明队列A并绑定到交换机Y
@Bean("QA")
public Queue QA(){
Map args=new HashMap<>();
args.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);//声明A绑定的死信交换机
args.put("x-dead-letter-routing-key","YD");//声明队列的死信路由key
args.put("x-message-ttl",10000);//声明TTL-10s
return QueueBuilder.durable(QUEUE_A).withArguments(args).build();//声明队列A
}
//A绑定到交换机X
@Bean
public Binding ABindX(@Qualifier("QA") Queue QA,@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(QA).to(xExchange).with("XA");
}
//声明队列B并绑定到交换机Y
@Bean("QB")
public Queue QB(){
Map args=new HashMap<>();
args.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);//声明B绑定的死信交换机
args.put("x-dead-letter-routing-key","YD");//声明队列的死信路由key
args.put("x-message-ttl",40000);//声明TTL-40s
return QueueBuilder.durable(QUEUE_B).withArguments(args).build();//声明队列B
}
//B绑定到交换机X
@Bean
public Binding BBindX(@Qualifier("QB") Queue QB,@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(QB).to(xExchange).with("XB");
}
//声明死信队列D
@Bean("QD")
public Queue QD(){
return new Queue(DEAD_LETTER_QUEUE);
}
//D绑定到死信交换机Y
@Bean
public Binding DBindY(@Qualifier("QD") Queue QD,
@Qualifier("yExchange") DirectExchange yExchange){
return BindingBuilder.bind(QD).to(yExchange).with("YD");
}
}
4.生产者和消费者
@RestController
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/ttl/{msg}")
public String sendMsg(@PathVariable String msg){
System.out.println(new Date()+":发送一条信息给两个 TTL 队列:"+ msg);
rabbitTemplate.convertAndSend("X","XA","来自ttl为10s的队列:"+msg);
rabbitTemplate.convertAndSend("X","XB","来自ttl为40s的队列:"+msg);
return "发送成功!";
}
}
@Component
public class Consumer {
@RabbitListener(queues = "QD")
public void receive(Message message, Channel channel){
System.out.println(new Date()+":收到死信队列的信息:"+ new String(message.getBody()));
}
}
5.测试
//发送请求:
http://localhost:8080/ttl/我爱新世纪百货
//控制台结果:
Sun Aug 14 09:41:40 CST 2022:发送一条信息给两个 TTL 队列:我爱新世纪百货
Sun Aug 14 09:41:50 CST 2022:收到死信队列的信息:来自ttl为10s的队列:我爱新世纪百货
Sun Aug 14 09:42:21 CST 2022:收到死信队列的信息:来自ttl为40s的队列:我爱新世纪百货
第一条消息在 10S 后变成了死信消息,然后被消费者消费掉,第二条消息在 40S 之后变成了死信消息, 然后被消费掉,这样一个延时队列就打造完成了。
对于上面的案例,如果这样使用的话,每增加一个新的时间需求,就要新增一个队列。所以接下来对代码进行优化。接下来新增一个队列 QC,绑定关系如下,该队列不设置 TTL 时间
优化后的配置类:
@Configuration
public class TTLQueueConfigOptimized {
public static final String X_EXCHANGE = "X";
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
public static final String QUEUE_C = "QC";
public static final String QUEUE_A = "QA";
public static final String QUEUE_B = "QB";
public static final String DEAD_LETTER_QUEUE = "QD";
//声明交换机X
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
//声明交换机Y
@Bean("yExchange")
public DirectExchange yExchange(){
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
//声明队列A并绑定到交换机Y
@Bean("QA")
public Queue QA(){
Map args=new HashMap<>();
args.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);//声明A绑定的死信交换机
args.put("x-dead-letter-routing-key","YD");//声明队列的死信路由key
args.put("x-message-ttl",10000);//声明TTL-10s
return QueueBuilder.durable(QUEUE_A).withArguments(args).build();//声明队列A
}
//A绑定到交换机X
@Bean
public Binding ABindX(@Qualifier("QA") Queue QA,@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(QA).to(xExchange).with("XA");
}
//声明队列B并绑定到交换机Y
@Bean("QB")
public Queue QB(){
Map args=new HashMap<>();
args.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);//声明B绑定的死信交换机
args.put("x-dead-letter-routing-key","YD");//声明队列的死信路由key
args.put("x-message-ttl",40000);//声明TTL-40s
return QueueBuilder.durable(QUEUE_B).withArguments(args).build();//声明队列B
}
//B绑定到交换机X
@Bean
public Binding BBindX(@Qualifier("QB") Queue QB,@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(QB).to(xExchange).with("XB");
}
//声明死信队列D
@Bean("QD")
public Queue QD(){
return new Queue(DEAD_LETTER_QUEUE);
}
//D绑定到死信交换机Y
@Bean
public Binding DBindY(@Qualifier("QD") Queue QD,
@Qualifier("yExchange") DirectExchange yExchange){
return BindingBuilder.bind(QD).to(yExchange).with("YD");
}
//声明队列C并绑定死信交换机
@Bean("QC")
public Queue QC(){
Map args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//没有声明 TTL 属性
return QueueBuilder.durable(QUEUE_C).withArguments(args).build();
}
//声明队列C绑定X交换机
@Bean
public Binding CBindingX(@Qualifier("QC") Queue queueC,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueC).to(xExchange).with("XC");
}
}
优化后的生产者类(消费者类不变):
@RestController
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/ttl/{msg}/{TTL}")
public String sendMsg(@PathVariable String msg,@PathVariable String TTL){
System.out.println(new Date()+":发送3条信息给 TTL 队列:"+ msg);
rabbitTemplate.convertAndSend("X","XC","来自ttl为"+TTL+"ms的队列:"+msg,correlationData->{
correlationData.getMessageProperties().setExpiration(TTL);
return correlationData;
});
rabbitTemplate.convertAndSend("X","XA","来自ttl为10000ms的队列:"+msg);
rabbitTemplate.convertAndSend("X","XB","来自ttl为40000ms的队列:"+msg);
return "发送成功!";
}
}
测试:
//在浏览器中输入地址:
http://localhost:8080/ttl/我爱新世纪百货/2000
//结果:
Sun Aug 14 10:04:20 CST 2022:发送3条信息给 TTL 队列:我爱新世纪百货
Sun Aug 14 10:04:22 CST 2022:收到死信队列的信息:来自ttl为2000ms的队列:我爱新世纪百货
Sun Aug 14 10:04:30 CST 2022:收到死信队列的信息:来自ttl为10000ms的队列:我爱新世纪百货
Sun Aug 14 10:05:00 CST 2022:收到死信队列的信息:来自ttl为40000ms的队列:我爱新世纪百货
前面提到,在消息设置中设置TTL时会存在一些问题。比如第一个消息的延时时长很长,而第二个消息的延时时长很短,但第二个消息并不会优先执行。想要解决这样的问题要使用到RabbitMQ的插件。
1.首先去下载 rabbitmq_delayed_message_exchange 插件,后缀名是 .ez 。注意版本号要与安装的rabbitMQ一致!
2.复制到 /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins (插件目录),若权限不足,使用下面命令修改文件夹权限:
chmod -R 777 xx/
//xx表示当前所处文件夹下的子文件夹名字,该命令同时修改所有子文件夹的权限
3.在插件目录下使用管理员权限安装插件
su
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
接下来对下图的结构作实现:
1.配置类
@Configuration
public class DelayedQueueConfig {
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
//声明延迟队列
@Bean("Q")
public Queue delayedQueue() {
return new Queue(DELAYED_QUEUE_NAME);
}
//声明自定义交换机
@Bean("E")
public CustomExchange delayedExchange() {
Map args = new HashMap<>();
//自定义交换机的类型
args.put("x-delayed-type", "direct");
return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, args);
}
//绑定routingKey
@Bean
public Binding bindingDelayedQueue(@Qualifier("Q") Queue queue,
@Qualifier("E") CustomExchange delayedExchange) {
return BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
}
}
2.生产者
@RestController
public class Producer {
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("delayMsg/{message}/{delayTime}")
public String sendMsg(@PathVariable String message, @PathVariable Integer delayTime) {
rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, message,
correlationData ->{
correlationData.getMessageProperties().setDelay(delayTime);
return correlationData;
});
System.out.println(new Date()+":发送信息给队列:"+ message);
return "发送成功!";
}
}
3.消费者
@Component
public class Consumer {
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
@RabbitListener(queues = DELAYED_QUEUE_NAME)
public void receiveDelayedQueue(Message message){
System.out.println(new Date()+":收到延迟队列的信息:"+ new String(message.getBody()));
}
}
4.测试:
//浏览器输入地址:
http://localhost:8080/delayMsg/我爱新世纪百货/2000
//结果:
Sun Aug 14 11:02:48 CST 2022:发送信息给队列:我爱新世纪百货
Sun Aug 14 11:02:50 CST 2022:收到延迟队列的信息:我爱新世纪百货
延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用 RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。
当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis 的 zset,利用 Quartz 或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景