【笔记】时间轮算法

76 阅读3分钟

Github: 代码Demo地址

首先时间轮最基本的结构其实就是一个数组

怎么变成一个轮呢?

首尾相接就可以了:

假如每个元素代表一秒钟,那么这个数组一圈能表达的时间就是 8 秒,就是这样的:

那两圈就是16s,三圈就是24s,.....

虽然数组长度只有 8,但是它可以在上叠加一圈又一圈,那么能表示的数据就多了。

比如我把上面的图的前三圈改成这样画:

把这个数组美化一下,从视觉上也把它变成一个轮子:

然后,把前面的数据给填进去大概是长这样的:

那么问题就来了。

假设这个时候我有一个需要在 800 秒之后执行的任务,应该是怎么样的呢?

800 mod 8 =0,说明应该挂在下标为 0 的地方:

假设又来一个 400 秒之后需要执行的任务呢?

同样的道理,继续往后追加即可:

好,现在又来一个 403 秒后需要执行的任务,应该挂在哪儿?

403 mod 8 = 3,那么就是这样的:

不要误以为下标对应的链表中的圈数必须按照从小到大的顺序来,这个是没有必要的。

我为什么要不厌其烦的给你说怎么计算,怎么挂到对应的下标中去呢?

因为我还需要引出一个东西: 待分配任务的队列

上面画 800 秒、 400 秒和 403 秒的任务的时候,我还省略了一步。

其实应该是这样的:

任务并不是实时挂到时间轮上去的,而是先放到一个待分配的队列中,等到特定的时间再把待分配队列中的任务挂到时间轮上去。

具体是什么时候呢?写代码在看...

除了待分配队列外,还有一个任务取消的队列。

因为放入到时间轮的任务是可以被取消的。

这是Netty 的 HashedWheelTimer 思路,使用增加轮次/圈数的概念。

Kafka使用多层时间轮的概念 (Kafka 的 TimingWheel),相较于上个方案,层级时间轮能更好控制时间粒度,可以应对更加复杂的定时任务处理场景,适用的范围更广:

类似于时钟,外圈一格,里面一圈

TimingWheel是一个 存储定时任务的环形队列,底层采用数组实现,数组中的每个元素可以存放一个定时任务列表(TimerTaskList) 。TimerTaskList 是一个环形的双向链表,链表中的每一项表示的都是定时任务项(TimerTaskEntry),其中封装了真正的定时任务 TimerTask。

TimingWheel封装

  • tickMs(基本时间跨度) :时间轮由多个时间格组成,每个时间格代表当前时间轮的基本时间跨度(tickMs)。
  • wheelSize(时间单位个数) :时间轮的时间格个数是固定的,可用(wheelSize)来表示,那么整个时间轮的总体时间跨度(interval)可以通过公式 tickMs × wheelSize计算得出。
  • currentTime(当前所处时间) :时间轮还有一个表盘指针(currentTime),用来表示时间轮当前所处的时间,currentTime 是 tickMs 的整数倍。currentTime 可以将整个时间轮划分为到期部分和未到期部分,currentTime 当前指向的时间格也属于到期部分,表示刚好到期,需要处理此时间格所对应的 TimerTaskList 的所有任务。

新增延迟任务的时候:

新来的 TimerTaskEntry 会复用原来的 TimerTaskList,所以它会插入到原本已经到期的时间格 1 中(一个显而易见的环形结构)。

总之,整个时间轮的总体跨度是不变的,随着指针 currentTime 的不断推进,当前时间轮所能处理的时间段也在不断后移,总体时间范围在 currentTime 和 currentTime + interval 之间