背景
使用延迟队列的场景非常多,支付超时关闭、用户签约超时、拼团失败取消等,延迟 15分钟、30分钟 ...
实现延迟处理的手段很多:
- 定时任务扫描,到时间就处理
- 利用 Redis 队列,不断投递检查,到时间就处理
- JDK 的延迟队列
- Rocket MQ的延迟队列
- Rabbit MQ的延迟队列
- ...
工具无好坏,选择最适合自己场景的一款即可 ~
本文主要讲解 RocketMQ 的延迟队列,主要有以下优点:
- 高吞吐量
- 低延迟
- 使用人群范围广
RocketMQ 延迟队列
RocketMQ 是阿里巴巴开源的一款分布式消息中间件,具有高性能、高可靠性、分布式等特性,受众群体广。
特性
1、高吞吐
得益于 RocketMQ 队列设计的高吞吐能力,通过高效的存储和网络传输机制,能够支持每秒数百万条消息的吞吐量。
2、低延迟
RocketMQ 的消息传输延迟通常在毫秒级别,能够满足对实时性要求较高的应用场景。
每个延迟级别的扫描时间间隔是固定的,通常为 1 秒。这意味着 RocketMQ 的定时任务会每秒钟扫描一次所有的延迟队列,检查是否有消息的延迟时间已经到达。如果有消息的延迟时间到达,这些消息会被重新投递到目标队列中。
3、可靠性
RocketMQ 提供了多种机制来保证消息的可靠传输和存储。
- 消息持久化:RocketMQ 支持将消息持久化到磁盘,确保在系统故障时消息不会丢失。
- 消息重试:RocketMQ 支持消息重试机制,当消息消费失败时,可以自动进行重试,确保消息最终被成功处理。
- 消息确认:RocketMQ 支持消息确认机制,消费者在成功处理消息后需要发送确认,确保消息不会被重复消费。
4、设计简单
多个 Level 队列,定时任务扫描每个 Level 数据,到时间则投递至目标队列进行等待消费。
基本使用
生产者:
java
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
public class DelayProducer {
public static void main(String[] args) throws Exception {
// 创建生产者实例
DefaultMQProducer producer = new DefaultMQProducer("delay_producer_group");
producer.setNamesrvAddr("localhost:9876");
producer.start();
// 创建消息
Message msg = new Message("DelayTopic", "Hello RocketMQ".getBytes());
// 设置延迟级别,延迟10秒
msg.setDelayTimeLevel(3);
// 发送消息
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
// 关闭生产者
producer.shutdown();
}
}
消费者:
java
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
public class DelayConsumer {
public static void main(String[] args) throws Exception {
// 创建消费者实例
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("delay_consumer_group");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("DelayTopic", "*");
// 注册消息监听器
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), new String(msg.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 启动消费者
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
原理
RocketMQ 实现原理简单:支持 18 个级别的 Level,最多 2 小时;每个 Level 都对应一个队列,定时任务直接扫描每个延迟级别的队列,并在延迟时间到达时将消息投递到目标队列。
1、延迟级别
RocketMQ 通过预定义的延迟级别来实现延迟队列。每个延迟级别对应一个固定的延迟时间。默认情况下,RocketMQ 提供了 18 个延迟级别,延迟时间从 1 秒到 2 小时不等。
lua
生产者发送消息 -> 延迟队列(根据延迟级别存储消息)
+----------------+ +----------------+ +----------------+
| 生产者 | | 延迟队列 | | 目标队列 |
| | | Level 1: 1s | | |
| +------------+ | | Level 2: 5s | | +------------+ |
| | 发送消息 | | ----> | Level 3: 10s | ----> | | 消费者 | |
| +------------+ | | ... | | +------------+ |
+----------------+ +----------------+ +----------------+
2、消息存储
当生产者发送一条延迟消息时,消息会被存储在一个特殊的队列中,这个队列根据消息的延迟级别进行划分。每个延迟级别都有一个对应的队列。
3、定时扫描
RocketMQ 的 Broker 端有一个定时任务,用于扫描这些延迟队列。当定时任务发现某个延迟队列中的消息已经到达指定的延迟时间时,会将这些消息重新投递到目标队列中。
lua
定时任务扫描延迟队列 -> 检查消息延迟时间 -> 重新投递到目标队列
+----------------+ +----------------+ +----------------+
| 定时任务 | | 延迟队列 | | 目标队列 |
| +------------+ | | Level 1: 1s | | |
| | 扫描队列 | | ----> | Level 2: 5s | ----> | +------------+ |
| +------------+ | | Level 3: 10s | | | 消费者 | |
| | | ... | | +------------+ |
+----------------+ +----------------+ +----------------+
4、消息重新投递
当延迟时间到达时,消息会被重新投递到目标队列中,消费者可以从目标队列中消费这些消息。
lua
消息重新投递到目标队列 -> 消费者消费消息
+----------------+ +----------------+ +----------------+
| 定时任务 | | 延迟队列 | | 目标队列 |
| | | Level 1: 1s | | |
| | | Level 2: 5s | | +------------+ |
| | | Level 3: 10s | ----> | | 消费者 | |
| | | ... | | +------------+ |
+----------------+ +----------------+ +----------------+