背景
- 当前业务中存在着超时关闭各种类型的订单的场景。
- 项目里已集成了RocketMq,可以基于此实现延时队列。
- 由于采用RocketMq实现延时队列有个缺点,那就是它不能灵活的支持各个精度的延时,只能按照事先配置好的延时级别进行。
- 基于以上三点考虑,并在对比各种实现延时队列的方案后,决定采用Redission(项目里也有集成Redission)+ RocketMq 进行实现。
实现
几个重要的API
- RBlockingQueue
- RDelayedQueue
生产者
参数说明:
- QueueEntity :业务实体,里面就是封装具体的业务数据。
public void addQueue(QueueEntity queueEntity, TimeUnit timeUnit, Long time) {
RBlockingQueue<String> queue = redissonClient.getBlockingQueue(queueEntity.getRedisQueueEnum().getQueueKey());
RDelayedQueue rDelayedQueue = redissonClient.getDelayedQueue(queue);
String value = JSON.toJSONString(queueEntity);
rDelayedQueue.offer(value, time, timeUnit);
//释放队列
rDelayedQueue.destroy();
}
消费者
关于消费者的实现,可以在项目启动时,就开启监听:
@Component
public class QueueRunner implements CommandLineRunner {
@Autowired
private RDelayQueueTask rDelayQueueTask;
@Override
public void run(String... args) throws Exception {
rDelayQueueTask.comsumeQueue();
}
}
RDelayQueueTask实现:
- RedisQueueEnum: 这个也是业务里定义的一个枚举类,存放所有需要延时操作的枚举key。
public void comsumeQueue() {
for (RedisQueueEnum redisQueueEnum : RedisQueueEnum.values()) {
RBlockingQueue<String> queue = redissonClient.getBlockingQueue(redisQueueEnum.getQueueKey());
// 重新初始化一次延时队列
// 有多少枚举就启动多少个线程进行监听
new Thread(() -> {
while (true) {
String json = null;
try {
json = queue.take();
// 发送到MQ
} catch (Exception e) {
}
}
}).start();
}
}
原理
图片来自:Redis延时队列,这次彻底给你整明白了
两个重要延时队列:
- redisson_delay_queue:
- redisson_delay_queue_timeout:
通过观察redis种的数据存储,可以发现前者是个list类型数据结构,后者是一个ZSet类型数据结构。
- 在消费端启动的时候,就会订阅这
redisson_delay_queue
- 当生产者有数据
offer
时,redisson先把数据放到redisson_delay_queue_timeout
(ZSet集合,按延时到期时间的时间戳为score
排序)。 - 同时发布内容到上面订阅的key,此时客户端进程开启一个延时任务,延时时间为发布的timeout。(HashedWheelTimer,时间轮)
- 客户端进程的延时任务到了时间执行,从zset分页取出过了当前时间的数据,再将数据rpush到第一步的阻塞队列里。
- 最终回调到 RBlockingQueue 的 take方法上,从而取到数据。
总结
优点:通过结合RocketMQ,基于Redission实现的延时队列会更具有可靠性。在MQ的重试机制、持久化等手段加持下,会更加可靠和可控。
缺点:随着业务规模的发展,越来越多的延时操作会导致线程数目不断增加,这里可能会有性能影响。另一方面如果某个时间点的数据突增,可能会产生一种情况,实际发送消息的时间比定好的延迟时间会更加久。