知道时间轮算法吗?在Netty和Kafka中如何应用的?( 二 )


知道时间轮算法吗?在Netty和Kafka中如何应用的?

文章插图
 
然后我们再来看下延迟队列是如何获取元素的 。
知道时间轮算法吗?在Netty和Kafka中如何应用的?

文章插图
 
小结一下也是利用优先队列实现的,元素通过实现 Delayed 接口来返回延迟的时间 。不过延迟队列就是个容器,需要其他线程来获取和执行任务 。
这下是搞明白了 Timer 、ScheduledThreadPool 和 DelayQueue,总结的说下它们都是通过优先队列来获取最早需要执行的任务,因此插入和删除任务的时间复杂度都为O(logn),并且 Timer 、ScheduledThreadPool 的周期性任务是通过重置任务的下一次执行时间来完成的 。
问题就出在时间复杂度上,插入删除时间复杂度是O(logn),那么假设频繁插入删除次数为 m,总的时间复杂度就是O(mlogn),这种时间复杂度满足不了 Kafka 这类中间件对性能的要求,而时间轮算法的插入删除时间复杂度是O(1) 。我们来看看时间轮算法是如何实现的 。
时间轮算法俗话说艺术源于生活,技术也能从日常生活中找到灵感 。咱们先来看块表,嗯金色的表 。
知道时间轮算法吗?在Netty和Kafka中如何应用的?

文章插图
 
【知道时间轮算法吗?在Netty和Kafka中如何应用的?】都看清楚了吧,时间轮就是和手表时钟很相似的存在 。时间轮用环形数组实现,数组的每个元素可以称为槽,和 HashMap一样称呼 。
槽的内部用双向链表存着待执行的任务,添加和删除的链表操作时间复杂度都是 O(1),槽位本身也指代时间精度,比如一秒扫一个槽,那么这个时间轮的最高精度就是 1 秒 。
也就是说延迟 1.2 秒的任务和 1.5 秒的任务会被加入到同一个槽中,然后在 1 秒的时候遍历这个槽中的链表执行任务 。
知道时间轮算法吗?在Netty和Kafka中如何应用的?

文章插图
 
从图中可以看到此时指针指向的是第一个槽,一共有八个槽0~7,假设槽的时间单位为 1 秒,现在要加入一个延时 5 秒的任务,计算方式就是 5 % 8 + 1 = 6,即放在槽位为 6,下标为 5 的那个槽中 。更具体的就是拼到槽的双向链表的尾部 。
然后每秒指针顺时针移动一格,这样就扫到了下一格,遍历这格中的双向链表执行任务 。然后再循环继续 。
可以看到插入任务从计算槽位到插入链表,时间复杂度都是O(1) 。那假设现在要加入一个50秒后执行的任务怎么办?这槽好像不够啊?难道要加槽嘛?和HashMap一样扩容?
不是的,常见有两种方式,一种是通过增加轮次的概念 。50 % 8 + 1 = 3,即应该放在槽位是 3,下标是 2 的位置 。然后 (50 - 1) / 8 = 6,即轮数记为 6 。也就是说当循环 6 轮之后扫到下标的 2 的这个槽位会触发这个任务 。Netty 中的 HashedWheelTimer 使用的就是这种方式 。
还有一种是通过多层次的时间轮,这个和我们的手表就更像了,像我们秒针走一圈,分针走一格,分针走一圈,时针走一格 。
多层次时间轮就是这样实现的 。假设上图就是第一层,那么第一层走了一圈,第二层就走一格 。
可以得知第二层的一格就是8秒,假设第二层也是 8 个槽,那么第二层走一圈,第三层走一格,可以得知第三层一格就是 64 秒 。
那么一格三层,每层8个槽,一共 24 个槽时间轮就可以处理最多延迟 512 秒的任务 。
知道时间轮算法吗?在Netty和Kafka中如何应用的?

文章插图
 
而多层次时间轮还会有降级的操作,假设一个任务延迟 500 秒执行,那么刚开始加进来肯定是放在第三层的,当时间过了 436 秒后,此时还需要 64 秒就会触发任务的执行,而此时相对而言它就是个延迟 64 秒后的任务,因此它会被降低放在第二层中,第一层还放不下它 。
再过个 56 秒,相对而言它就是个延迟 8 秒后执行的任务,因此它会再被降级放在第一层中,等待执行 。
降级是为了保证时间精度一致性 。Kafka内部用的就是多层次的时间轮算法 。
Netty中的时间轮在 Netty 中时间轮的实现类是 HashedWheelTimer,代码中的 wheel 就是上图画的循环数组,mask 的设计和HashMap一样,通过限制数组的大小为2的次方,利用位运算来替代取模运算,提高性能 。tickDuration 就是每格的时间即精度 。可以看到配备了一个工作线程来处理任务的执行 。
知道时间轮算法吗?在Netty和Kafka中如何应用的?

文章插图
 
接下来我们再来看看任务是如何添加的 。


推荐阅读