简介:
大家好,我是xp,一年更新一篇,惭愧,太懒了,逐渐躺平…
时间轮,是一种实现延迟功能(定时器)的巧妙算法,常用于Netty、Dubbo、Kafka等等。此篇文章分析Dubbo。
说白了,就是延时任务,常用于定时重发、延迟生产、拉取等等。
最简单的写法,DelayQueue
、RocketMQ
延时队列等等,其中DelayQueue
博主已经分析过,这里不过多介绍。不过这么多有些优秀中间件都是自己实现的,为啥?装x呗
当然是为了性能了,坐稳了,开卷。
先简单看个图
这是一个长度为8的数组,只不过我画成了圆形。如果一秒走一步,一圈走下来就是8秒。
- 延时2秒:
wheel[1]
- 延时5秒:
wheel[4]
- 延时10秒???
最简单的就是将wheel加大超过10,但是这样会造成浪费,1个延时2秒的任务,1个延时100秒的任务,初始化一个长度为100的wheel,属实浪费。
可以换个思路,一圈是8秒,10秒 = 1圈+延时2秒,最后就是: - 延时10秒:一圈 + wheel[1]
说到这,貌似so easy?
好的,我们进入源码看下,是不是这样的。
坐稳了,开始源码之旅:
这是我从Dubbo
中复制出来的代码,方便调试的。
使用很简单:
初始化时间轮和添加任务
先看1部分:
初始化时间轮还是比较简单的,简单看下传的参数:
threadFactory:线程工厂
tickDuration:时间轮上每个Bucket之间的间隔,可以简单理解成每次循环的时间,实际每次循环的时候会小于这个时间点,下面会说
unit:时间单位
ticksPerWheel:时间轮的大小
maxPendingTimeouts:最大等待数量
还有我框出来的地方,可以稍微看下:
createWheel:
将wheel
的长度初始化为2的幂次方,
还定义了mask
,为wheel
的长度-1。
看到这,大部分人应该都知道为啥了,不知道可以去看博主的HashMap
篇,里面有详细介绍。
继续看添加任务:
先不说start()
,就是计算任务需要执行的时间点,然后实例化成HashedWheelTimeout
,加入到timeouts
。这里的timeouts
是待执行队列,任务不是直接挂到时间轮的,而是先加入到待执行队列的。
重点看start():
看起来很简单,主要是 workerThread
这个线程在干什么,workerThread
是Worker
线程,初始化时间轮的时候赋值的。
Worker:
分为4部分:
单纯的初始化startTime,这里对应着start()里面的startTimeInitialized.await(),startTime初始化成功,就可以计算任务需要执行的时间了,所以初始化完就countDown了。
waitForNextTick:
也很简单,相当于每次休眠tickDuration,但是有个细节:
//看似这里得到的值都一样,其实不然
//因为每走一步,都会处理任务,浪费一些时间
//+ 999999是为了多1毫秒,
//当线程调用 Thread.sleep 方法的时候,JVM 会进行一个特殊的调用,将中断周期设置为 1ms
//JVM每1ms检查一次,如果(deadline - currentTime)恰好是0.5ms,那么JVM可能检查的时候,这个任务时间过了,而不执行了,所以+ 999999
long sleepTimeMs = (deadline - currentTime + 999999) / 1000000;
解释起来就是:比如说走1步需要10秒,那么就是休眠10秒,但是处理任务需要时间,那么这里就是减掉这个处理时间,一圈走完是多少秒就是多少秒,尽可能的准确,当然处理时间超过每步的时间,那就没办法了,有误差
-
transferTimeoutsToBuckets:
这里就是取待分配队列的timeouts
,然后计算需要多少圈+下标,再追加到那个下标bucket
的next,addTimeout
相当于链表的添加,博主也写过链表,不再多述。 -
expireTimeouts:
判断bucket
里面的任务:
1.是否有需要执行的
2.是否需要取消掉
3.不执行,不取消,就把圈数-1
到这里差不多就讲完了,整个时间轮,其实也很简单,看下面的图
这是Dubbo
里面的时间轮,Kafka
的层级时间轮还要更牛叉,下次在分析