不要误以为下标对应的链表中的圈数必须按照从小到大的顺序来,这个是没有必要的。
好,现在又来一个 403 秒后需要执行的任务,应该挂在哪儿?
403 mod 8 = 3,那么就是这样的:
我为什么要不厌其烦的给你说怎么计算,怎么挂到对应的下标中去呢?
因为我还需要引出一个东西:待分配任务的队列。
上面画 800 秒、 400 秒和 403 秒的任务的时候,我还省略了一步。
其实应该是这样的:
任务并不是实时挂到时间轮上去的,而是先放到一个待分配的队列中,等到特定的时间再把待分配队列中的任务挂到时间轮上去。
具体是什么时候呢?
下面讲源码的时候再说。
其实除了待分配队列外,还有一个任务取消的队列。
因为放入到时间轮的任务是可以被取消的。
比如在 Dubbo 里面,检验调用是否超时也用的是时间轮机制。
假设一个调用的超时时间是 5s,5s 之后需要触发任务,抛出超时异常。
但是如果请求在 2s 的时候就收到了响应,没有超时,那么这个任务是需要被取消的。
对应的源码就是这块,看不明白没关系,看一眼就行了,我只是为了证明我没有骗你:
org.apache.dubbo.remoting.exchange.support.DefaultFuture#received
原理画图出来大概就是这样,然后我还差一张图。
把源码里面的字段的名称给你对应到上面的图中去。
主要把这几个对象给你对应上,后面看源码就不会太吃力了:
对应起来是这样的:
注意左上角的“worker的工作范围”把整个时间轮包裹了起来,后面看源码的时候你会发现其实整个时间轮的核心逻辑里面没有线程安全的问题,因为 worker 这个单线程把所有的活都干完了。
最后,再提一嘴:比如在前面 FailbackClusterInvoker 的场景下,时间轮触发了重试的任务,但是还是失败了,怎么办呢?
很简单,再次把任务放进去就行了,所以你看源码里面,有一个叫做 rePut 的方法,干的就是这事:
org.apache.dubbo.rpc.cluster.support.FailbackClusterInvoker.RetryTimerTask#run
这里的含义就是如果重试出现异常,且没有超过指定重试次数,那么就可以再次把任务仍回到时间轮里面。
等等,我这里知道“重试次数”之后,还能干什么事儿呢?
比如如果你对接过微信支付,它的回调通知有这样的一个时间间隔:
我知道当前重试的次数,那么我就可以在第 5 次重试的时候把时间设置为 10 分钟,扔到时间轮里面去。
时间轮就可以实现上面的需求。
当然了,MQ 的延迟队列也可以,但是不是本文的讨论范围。
但是用时间轮来做上面这个需求还有一个问题:那就是任务在内存中,如果服务挂了就没有了,这是一个需要注意的地方。
除了 FailbackClusterInvoker 外,其实我觉得时间轮更合适的地方是做心跳。
这可太合适了, Dubbo 的心跳就是用的时间轮来做。
org.apache.dubbo.remoting.exchange.support.header.HeartbeatTimerTask#doTask
从上图可以看到,doTask 方法就是发送心跳包,每次发送完成之后调用 reput 方法,然后再次把发送心跳包的任务仍回给时间轮。
好了,不再扩展应用场景了。
接下来,进入源码分析,跟上节奏,不要乱,大家都能学。
开卷!