RocketMQ是怎样实现延迟消息的?
延迟消息时MQ中一种特殊的消息,可用于投递延迟任务,本文我们一起来聊一聊Rocket中延迟消息的实现。
1. RocketMQ的存储架构
在聊延迟消息的具体实现之前,我们需要先复习一下在RocketMQ中,一条普通消息的生命周期是怎样的。
- Productor生产消息。
- 消息存储到CommitLog。
- CommitLog中的消息索引投递到对应的Topic队列。
- Consumer消费对应Topic中的消息。
2. 多级延迟队列
2.1. 多级延迟队列介绍
延迟队列是一种定时器的实现方式,简单来说,我们可以借助队列先进先出的方式,将需要延迟发送的消息投递到一个队列中,之后用一个监听器监听队首节点是否到时间,到了的话就进行出队操作。延迟队列的设计可以避免对于每个节点都起一个监听线程。
但是,其问题也是显而易见的,队首节点是否出队是按照是否到定时结束时间判断的,那么,我们就必须保障队列中的节点倒计时是从队首递增排列到队尾的,否则就会有队伍中间的节点已经到了出队时间,但是被前面的节点“卡主”的情况。
多级延迟队列就是为了处理上述问题,我们可以预设多个队列,队列1处理延迟1s发动的消息,队列2处理延迟2s发动的消息...每个延迟级别的消息投递到对应的队列,来避免单个队列中的定时乱序问题。
2.2. Rocket中的多级延迟队列实现
知道了什么是多级延迟队列,那么Rocket中是如果实现它的呢?
我们之前复习了RocketMQ消息消费的流程,消息会写写入CommitLog,之后投递到对应的TopicQueue。RocketMQ的多级延迟队列正式借助这两个结构实现的,对于延迟消息,RocketMQ在首次写入CommitLog时会根据其延迟级别改写消息的ID和Topic,之后,会由对应延迟等级的延迟TopicQueue承接消费。当延迟TopicQueue中的消息满足延迟时间后,会重新写入CommitLog中,之后由正常的TopicQueue进行消费,来达到延迟发送消息的效果。
2.3. 多级延迟队列的优劣
这个设计很巧妙的复用了RocketMQ的现有逻辑,通过其本身架构保障了延迟消息的稳定性。但是,这个实现也有一个很明显的问题,延迟队列的延迟时间是写死的,也就是说,延迟队列只能支持固定时间粒度的延迟消息,并且,随着延迟级别的增加,每个延迟等级都要有承接的TopicQueue与监听器,成本会越来越高。
3. 时间轮
3.1. 什么是时间轮?
时间轮由一个首位相连的环形链表与一个不断移动的指针组成,链表上的每个节点都连接着一个待执行的任务列表,指针移动的速度就代表节点间的时间步长。
当定时任务产生时,其根据延迟时间挂载到对应的节点上,指针移动到对应节点时,执行节点的所有待执行任务。
随着时间跨度的增加,单层时间轮的轮询效率会受到限制,文件数量过多难以管理等问题。通过多个时间了不同级别刻度进行映射(类比时钟时分秒刻度盘),以少量空间换时间,有效避免大量空轮询“空推进”情况。
3.2. 时间轮在RocketMQ中的实现
RocketMQ通过一个TimerLog日志承接了时间轮中待执行任务列表的统计工作,TimerLog将随机写转变为顺序写,每个延迟任务会计算出自己应该接入的队列,之后通过一个双向指针与TimerLog中的前向节点连在一起。
- 产生新的定时消息时,根据延迟时长定位到时间轮的对应时间格,完成链表的记录插入。
- 指针移动到对应出发节点时,将消息投递回CommitLog,进行消费。
3.3. 时间轮的优劣
时间轮可以达到比多级延迟队列更细粒度控制消息延迟的效果,但是肉眼可见的,其实现也比延迟队列复杂了很多。TimerLog日志的引入也会带来日志维护,压缩等一系列的问题。另外,虽然TimerLog的写入虽然是顺序的,但是其读取是随机的,大量消息的堆积可能会导致性能问题。
转载自:https://juejin.cn/post/7394279685969018915