Rocketmq 自定义延时消息实现

1、 时间轮介绍

image.jpeg

内部结构

     时间轮可以理解为一种环形结构,像钟表一样被分为多个 slot 槽位。每个 slot 代表一个时间段,每个 slot 中可以存放多个任务,使用的是链表结构保存该时间段到期的所有任务。时间轮通过一个时针随着时间一个个 slot 转动,并执行 slot 中的所有到期任务。

     时间轮内部结构其实是 HashedWheelBucket 数组,每个 HashedWheelBucket 表示时间轮中一个 slot。从 HashedWheelBucket 的结构定义可以看出,HashedWheelBucket 内部是一个双向链表结构,双向链表的每个节点持有一个 HashedWheelTimeout 对象,HashedWheelTimeout 代表一个定时任务。每个 HashedWheelBucket 都包含双向链表 head 和 tail 两个 HashedWheelTimeout 节点,这样就可以实现不同方向进行链表遍历

image.jpeg

任务添加

     当任务添加过程,可以根据任务的到期时间进行取模,然后将任务分布到不同的 slot 中。如上图所示,时间轮被划分为 8 个 slot,每个 slot 代表 1s,当前时针指向 2。假如现在需要调度一个 3s 后执行的任务,应该加入 2+3=5 的 slot 中;如果需要调度一个 12s 以后的任务,需要等待时针完整走完一圈 round 零 4 个 slot,需要放入第 (2+12)%8=6 个 slot。

     当需要把 round 信息保存在任务中。例如图中第 6 个 slot 的链表中包含 3 个任务,第一个任务 round=0,需要立即执行;第二个任务 round=1,需要等待 18=8s 后执行;第三个任务 round=2,需要等待 28=8s 后执行。所以当时针转动到对应 slot 时,只执行 round=0 的任务,slot 中其余任务的 round 应当减 1,等待下一个 round 之后执行。

     如果多个任务如果对应同一个 slot,处理冲突的方法采用的是拉链法。在任务数量比较多的场景下,适当增加时间轮的 slot 数量,可以减少时针转动时遍历的任务个数。

     时间轮定时器最大的优势就是,任务的新增和取消都是 O(1) 时间复杂度,而且只需要一个线程就可以驱动时间轮进行工作。

任务处理

整个核心任务处理代码是一个 do-while 循环
1、 首先计算出时针到下一次 tick 的时间间隔,然后 sleep 到下一次 tick。
2、通过取模运算获取当前 tick 在 HashedWheelBucket 数组中对应的下标,同时移除被取消的任务。
3、从 Queue 中取出任务加入对应的 HashedWheelBucket 中。
4、 执行当前 HashedWheelBucket 中的到期任务。

3、 自定义延时消息实现方案

image.jpeg

RocketMQ 自定义延迟消息二开实现

Broker 端存储

  1. 首先将业务topic转化为系统topic并将消息append到message_log中,然后再通过回放message_log构造schedule_index日志。schedule_index则有offsetId+size+schedule_time 构成
  2. 预加载 schedule_index,通过配置可以设置提前多久将加载schedule_index到内存中,默认1小时。 预加载过程其实在启动的时候启动一个 ScheduledExecutorService,默认1min进行扫描检测一次。
  3. 加载 主要将延迟消息的MessageId、延时时间、偏移量都加载到时间轮里。即两层HashWheel。第一层位于磁盘上,例如,以一个小时一个刻度一个文件,我们叫delay_message_segment,如延迟时间为2021年04月07日 19:00 至2021年04月07日 20:00为延迟消息将被存储在2021040719。并且这个刻度是可以配置调整的。第二层HashWheel位于内存中。也是以一个时间为刻度,比如1s,加载进内存中的延迟消息文件会根据延迟时间hash到一个HashWheel中。

Broker 端投递转化

  1. 时间轮中通过定时器进行每秒轮转,针对到期的延时消息转投到Message_log中,主要包括 通过时间轮上的MessageId 查询 原先的Message_log并将查到的Message_log转化到业务topic的Message_log中,最终被消费端快速消费。
  2. 由于时间轮基于内存,当机器宕机或者重启需要将延时消息重新加载。

     5.1.时间轮读取当前一个小时内的 schedule_index

     5.2. 对于已经过当前时间的消息,发送前还需检测 dispatch_log 是否已投递,没有则立即重新发送,否则不发送。

你可能感兴趣的:(Rocketmq 自定义延时消息实现)