常规时间轮
都知道时钟有指针、刻度、每刻度表示的时长等属性,Netty时间轮的设计也差不多,只是时钟的指针有时、分、秒,而Netty只用了一个指针。那么Netty是如何把定时任务加入时间轮的呢?下面先看一幅时间轮的构造图
当指针指向某一刻度时,它会把此刻度中的所有task任务一一取出并运行
那么问题来了:
- 时间轮的指针走一轮是多久?
- 时间轮是采用什么容器存储这些task的?
- 定时任务的运行时间若晚于指针走一轮的终点,则此时此任务该放在哪个刻度?
- 刻度的间隔时间标注为tickDuration,同时将时间轮一轮的刻度总数标注为wheelLen,两者都是时间轮的属性,可以通过构造方法由使用者传入,这样就可以得到时间轮指针走一轮的时长=tickDuration*wheelLen。
- 当指针运行到某一刻度时,需要把映射在此刻度上所有的任务都取出来,而刻度总数在时间轮初始化后就固定了。因此与Map相似,采用数组标识wheel[]加链表的方式来存储这些task,数组的大小固定为图7-1中的N,刻度的编号就是wheel[]的下标。
- 每个时间轮启动都会记录其启动时间,同时,每个定时任务都有其确定的执行时间,用这个执行时间减去时间轮的启动时间,再除以刻度的持续时长,就能获取这个定时任务需要指针走过多少刻度才运行,标注为calculated。时间轮本身记录了当前指针已经走过了多少刻度,标注为tick。通过calculated、tick、时间轮刻度总数wheelLen来计算定时任务在哪一刻度执行(此刻度标注为stopIndex)
那么问题又来了,如果设置的总刻度*刻度间隔时间 < 新任务的执行时间怎么办?
在“时间轮”的算法中,定时器检测进程只需要判断“时间轮”数组现在所指向的索引里的链表为不为空(即里面有没有超时的定时器),如果为空则不执行任何操作,如果不为空则对于这个数组元素链表里的所有定时器执行定时器超时进程(定时器的组件3)。而每当“时间轮”的周期数加 1 的时候,系统都会遍历一遍溢出列表里的定时器是否满足当前周期数,如果满足的话,则将这个位置的溢出列表全部移到“时间轮”相对应的索引位置中。注意为了溢出链表的判断复杂度比较低,溢出链表的维护也是有序的
变种时间轮
基本的“时间轮”插入操作因为维护了一个溢出列表导致定时器的插入操作无法做到 O(1) 的时间复杂度,所以为了 O(1) 时间复杂度的插入操作,一种变种的“时间轮”算法就被提出了。
在这个变种的“时间轮”算法里,我们加了一个 MaxInterval 的限制,这个 MaxInterval 其实也就是我们定义出的“时间轮”数组N的大小。假设“时间轮”数组的大小为 N,对于任何需要新加入的定时器,如果超时(绝对)时间小于 N 的话,则被允许加入到“时间轮”中,否则将不被允许加入。
注意,什么叫超时绝对时间? 还是以上面的例子为例,当前时间是 2T, 然后超时T的定时器想加进来,则绝对超时时间就是 3T,这个3T 是相对于当前时间轮周期的起点而言的,这个3T 就是超时绝对时间,只有3T <N, 该定时器才能被加入时间轮,否则拒绝其加入.
这种“时间轮”变种算法,执行定时器检测进程还有插入和删除定时器的操作时间复杂度都只有 O(1)。
分层时间轮
我们可以使用三个“时间轮”来表示不同颗粒度的时间,分别是小时“时间轮”、分钟“时间轮”和秒“时间轮”,可以称小时“时间轮”为分钟“时间轮”的上一层“时间轮”,秒“时间轮”为分钟“时间轮”的下一层“时间轮”。分层“时间轮”会维护一个“现在时间”
每层“时间轮”都需要各自维护一个当前索引来表示“现在时间”。例如,分层“时间轮”的“现在时间”是22h:20min:30s,它的结构图如下图所示:
当每次有新的定时器需要插入进分层“时间轮”的时候,将根据分层“时间轮”的“现在时间”算出一个超时的绝对时间。例如,分层“时间轮”的“现在时间”是 21h:20min:30s,而当我们要插入的新定时器超时时间为 50 分钟 10 秒时,这个超时的绝对时间则为 22h:10min:40s。
我们需要先判断最高层的时间是否一致,如果不一致的话则算出时间差,然后插入定时器到对应层的“时间轮”中,如果一致,则到下一层中的时间中计算,如此类推。在上面的例子中,最高层的时间小时相差了 22(22h:10min:40s的小时数)-21(21h:20min:30s的小时数) = 1 小时,所以需要将定时器插入到小时“时间轮”中的 (1 + 21) % 24 = 22这个索引中,定时器列表里还需要保存下层“时间轮”所剩余的时间 10min:40s,如下图所示, 这就完成了该定时器的插入:
每经过一秒钟,秒“时间轮”的索引都会加 1,并且执行定时器检测进程。定时器检测进程需要判断当前元素里的定时器列表是否为空,如果为空则不执行任何操作,如果不为空则对于这个数组元素列表里的所有定时器执行定时器超时进程。需要注意的是,定时器检测进程只会针对最下层的“时间轮”执行(原因你看完下面的举例就明白了)。
如果秒“时间轮”的索引到达 60 之后会将其归零,并将上一层的“时间轮”索引加 1,同时判断上一层的“时间轮”索引里的列表是否为空,如果不为空,则按照之前描述的算法将定时器加入到下一层“时间轮”中去,如此类推。
举个例子吧~ 在经过一段时间之后,上面的分层“时间轮”会到达以下的一个状态(即当前时间变成 22:00:00):
这时候上层“时间轮”索引里的列表不为空(挂着一个10min:40s),将这个定时器加入的索引为 10 的分钟“时间轮”中,并且保存下层“时间轮”所剩余的时间 40s,如下图所示:
如此类推,在经过 10 分钟之后(即当前时间来到 22:10:00),分层“时间轮”会到达以下的一个状态
同样的,我们将这个定时器插入到秒“时间轮”中,如下图所示:
这个时候,再经过 40 秒,秒“时间轮”的索引将会指向一个元素,里面有着非空的定时器列表,然后执行定时器超时进程并将定时器列表里所有的定时器删除。这就是为什么前面说定时器检测进程只会针对最下层的“时间轮”执行
我们可以看到,采用了分层“时间轮”算法之后,我们只需要维护一个大小为 24(小时) + 60(分钟) + 60(秒) = 144 的数组而同时保持着执行定时器检测进程还有插入和删除定时器的操作时间复杂度都只有 O(1)