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 之间。