时间轮(TimingWheel)

article/2025/7/28 3:40:56

一、什么是时间轮

时间轮其实就是一种环形的数据结构,可以想象成时钟,分成很多格子,一个格子代表一段时间(这个时间越短,Timer的精度越高)。并用一个双向链表存储放在该格子上的延时任务,同时一个指针随着时间一格一格转动,并执行相应格子中的到期任务。任务通过取模决定放入哪个格子。

二、时间轮结构

tickMs:表示一个槽所代表的时间范围,kafka的默认值为1ms

wheelSize:表示该时间轮有多少个槽,kafka的默认值是20

startMs:表示该时间轮的开始时间

interval:时间轮所能表示的时间跨度,也就是tickMs*wheelSize

currentTime:表示当前时间,也就是时间轮指针指向的时间

buckets:表示TimerTaskList的数组,即各个槽,每个bucket都有一个TimerTaskList。

TimerTaskList:存储TimerTaskEntry的双向链表

TimerTaskEntry:延迟任务,有两个值delayTime和expireTime

delayTime:TimerTask延迟时间

expireTime:TimerTask过期时间

queue:存储TimerTaskList的延迟队列,这些TimerTaskList都被加到这个延迟队列中,expireTime最小的槽会排在队列的最前面。

taskCounter:表示该时间轮的任务总数

三、运行原理

  1. 当定时器新增一个延迟任务时,通过buckets[expiration / tickMs % wheelSize]先计算出它应该属于哪个槽。比如延迟任务的delayMs=2ms,当前时间currentTime是0ms,则expiration=delayMs+startMs=2ms,通过前面的公式算出它应该落于2号槽。并把任务封装成TimerTaskEntry然后加入到TimerTaskList链表中。

  1. kafka会启动一个线程,通过定时器去推动时间轮的指针转动。其实现原理其实就是通过queue.poll()取出放在最前面的槽的TimerTaskList。由于queue是一个延迟队列,如果队列中的expireTime没有到达,该操作会阻塞住,直到expireTime到达。如果通过queue.poll()取到了TimerTaskList,说明该槽里面的任务时间都已经到达。这时候就可以遍历该TimerTaskList中的任务,然后执行对应的操作了。

  1. 定时器中只需持有TimingWheel的第一层时间轮的引用,并不会直接持有其他高层的时间轮,但是每一层时间轮都会有一个引用(overflowWheel)指向更高一层的应用,以此层级调用而可以实现定时器间接持有各个层级时间轮的引用。

四、时间溢出处理

在kafka的默认实现中,tickMs=1Ms,wheelSize=20,这就表示该时间轮所能表示的延迟时间范围是0~20Ms,那如果延迟时间超过20Ms要如何处理呢?Kafka对时间轮做了一层改进,使时间轮变成层级的时间轮。

一开始,第一层的时间轮所能表示时间范围是0~20Ms之间,假设现在出现一个任务的延迟时间是200Ms,那么kafka会再创建一层时间轮,我们称之为第二层时间轮。

也就是第二层时间轮每一个槽所能表示的时间是第一层时间轮所能表示的时间范围,也就是20Ms。槽的数量还是一样,其他的属性也是继承自第一层时间轮。这时第二层时间轮所能表示的时间范围就是0~400Ms了

同理,如果第二层时间轮的时间范围还容纳不了新的延迟任务,就会创建第三层、第四层...

五、时间轮用来解决什么问题

如果一个系统中存在着大量的调度任务,而大量的调度任务如果每一个都使用自己的调度器来管理任务的生命周期的话,浪费cpu的资源并且很低效。

时间轮是一种高效来利用线程资源来进行批量化调度的一种调度模型。把大批量的调度任务全部都绑定到同一个调度器上面,使用这一个调度器来进行所有任务的管理(manager),触发(trigger)以及运行(runnable)。能够高效的管理各种延时任务,周期任务,通知任务等等。

执行过程

1、时间轮

Kafka中一个时间轮TimingWheel是由20个时间格组成,wheelSize = 20;每格的时间跨度是1ms,tickMs = 1ms。参照Kafka,图中也用了20个灰边小圆表示时间格,为了动画演示可以看得清楚,我们这里每个小圆的时间跨度是1s。

整个时间轮的时间跨度就是 tickMs * wheelSize ,也就是 20s。从0s到19s,我们都分别有一个灰边小圆来承载。

Kafka的时间轮还有一个表盘指针 currentTime,表示时间轮当前所处的时间。也就是图中用黑色粗线表示的圆,随着时间推移, 这个指针也会不断前进;

添加定时任务

每一个定时任务都有延时时间delayTime,和过期时间ExpiredTime。比如当前时间是0s,我们添加了个延时时间为2s的任务,那么这个任务的过期时间就是2s,也就是当前时间0s再走两秒,变成了2s的时候,就到了触发这个定时任务的时间。

初始的时候, 时间轮的指针定格在0。此时添加一个超时时间为2s的任务, 那么这个任务将会插入到第二个时间格中。

如果这个时候又插入一个延时时间为8s的任务进来, 这个任务的过期时间就是在当前时间2s的基础上加8s, 也就是10s, 那么这个任务将会插入到过期时间为10s的时间格中。

2、"动态"时间轮

那么如果在当前时间是2s的时候, 插入一个延时时间为19s的任务时,这个任务的过期时间就是在当前时间2s的基础上加19s, 也就是21s。

当前的时间轮是没有过期时间为21s的时间格。这个任务将会插入到过期时间为1s的时间格中

复用时间格

其实呢,当指针定格在2s的位置时, 时间格0s, 1s和2s就已经是过期的时间格。也就是说指针可以用来划分过期的时间格[0,2]和未来的时间格 [3,19]。而过期的时间格可以继续复用。比如过期的时间格0s就变成了20s, 存放过期时间为20s的任务。理解了时间格的复用之后,再看回刚刚的例子,当前时间是2s时,添加延时时间为19s的任务,那么这个任务就会插入到过期时间为21s的时间格中。

3、时间轮升级

如果在当前时间是2s的时候, 插入一个延时时间为22s的任务, 这个任务的过期时间就是在2s的基础上加22s,也就是24s。

显然当前时间轮是无法找到过期时间格为24秒的时间格,因为当前过期时间最大的时间格才到21s。而且我们也没办法像前面那样再复用时间格,因为除了过期时间为2s的时间格,其他的时间格都还没过期呢。当前时间轮无法承载这个定时任务,那么应该怎么办呢?

当然我们可以选择扩展时间轮上的时间格, 但是这样一来,时间轮就失去了意义。

4、层级时间轮

如图是一个两层的时间轮:

第二层时间轮也是由20个时间格组成, 每个时间格的跨度是20s。

图中展示了每个时间格对应的过期时间范围, 我们可以清晰地看到, 第二层时间轮的第0个时间格的过期时间范围是 [0,19]。也就是说, 第二层时间轮的一个时间格就可以表示第一层时间轮的所有(20个)时间格;

添加定时任务

在当前时间是2s的时候, 插入一个延时时间为22s的任务,该任务过期时间为24s。

当第一层时间轮容纳不下时,进入第二层时间轮,并插入到过期时间为[20,39]的时间格中。

如果在当前时间是2s的时候, 插入一个延时时间为350s的任务, 这个任务的过期时间就是在2s的基础上加350s,也就是352s。

从图中可以看到,该任务插入到第二层时间轮过期时间为[340,359]s的时间格中,也就是第17格的位置。

5、"动态"层级时间轮

从图中可以看到,当第一层时间轮的指针定格在1s时,超时时间0s的时间格就过期了。而这个时候,第二层时间轮第0个时间格的时间范围就从[0,19]分为了过期的[0],和未过期的[1,19]。而过期的[0]就会被新的过期时间[400]复用。

第二层时间轮第0个时间格的过期时间范围演变如下:

[0-19]

[400][1,19]

[400,401][2,19]

......

[400,419]

所以,如果在当前时间是2s的时候, 插入一个延时时间为399s的任务, 这个任务的过期时间就是在2s的基础上加399s,也就是401s。如图,这个任务还是会插到第二层时间轮第0个时间格中去。

6、时间轮降级

在当前时间是2s的时候, 插入一个延时时间为22s的任务,该任务过期时间为24s。最后进入第二层时间轮,并插入到过期时间为[20,39]的时间格中。

当指针走到20s的时候,原本超时时间为24s的任务会被取出来,重新加入时间轮。此时该定时任务的延时时间从原本的22s,到现在还剩下4s(22s-18s)。最后停留在第一层时间轮超时时间为24s的时间格,也就是第4个时间格。

从这里可以看出时间轮的巧妙之处,两层时间轮只用了40个数组元素,却可以承载[0-399s]的定时任务。而三层时间轮用60个数组元素,就可以承载[0-7999s]的定时任务!


http://chatgpt.dhexx.cn/article/kD9cGaTk.shtml

相关文章

高性能定时器3——时间轮

​ 在网络程序中我们通常要处理三种事件,网络I/O事件、信号以及定时事件,我们可以使用I/O复用系统调用(select、poll、epoll)将这三类事件进行统一处理。我们通常使用定时器来检测一个客户端的活动状态,服务器程序通常…

C#,斐波那契数列(Fibonacci Sequence)的八种算法与源代码

一、莱昂纳多斐波那契(Leonardo Fibonacci) 斐波那契公元1170年生于意大利比萨,卒于1250年,被人称作“比萨的莱昂纳多”,是一名闻名于欧洲的数学家,其主要的著作有《算盘书》、《实用几何》和《四艺经》等。…

matlab斐波那契数列画图,斐波拉契数列 斐波那契数列 matlab程序

斐波那契数列数列从第3项开始,每一项都等于前两项之和。 例子:数列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368 应用: 生活斐波那契 斐波那契数列中的斐波脾气有点大,表面温和到别人误以为很好欺…

快速计算斐波那契数列(Fibonacci数列)

本文最后更新于 619 天前,其中的信息可能已经有所发展或是发生改变。 题目描述 输入一个正整数n,求Fibonacci数列的第n个数。Fibonacci数列的特点:第1,2个数为1,1。从第3个数开始,概述是前面两个数之和。即: 要求输入的…

递归求斐波那契数列

斐波那契数列 题目描述:编写一个函数,求斐波那契数列的第n项的值。 首先,对于斐波那契数列,我们是非常熟悉了,对斐波那契定义为如下:f(0)0,f(1)0,f(2)1,……f(n)f(n-1)f(n-2),其中n>1。对于这…

斐波那契数列(Fibonacci)

有一对兔子,出生后第3个月起每个月都生一对免子。小兔子长到第3个月后每个月又生一对兔子。假设所有兔子都不死,问40个月的免子总数为多少?解题思路: 这是一个有趣的古典数学问题。可以从表1看出兔子繁殖的规律。 …

【递归 动态规划 备忘录法】Fibonacci数列(斐波那契数列)(C++)

一、什么是Fibonacci数列 斐波那契数列指的是这样一个数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144…用文字来说,就是从第3项开始,每一项都等于前两项之和。至于包不包括0,一番查阅后没有得到证实… 不过重要的是“第3项开始…

算法-斐波那契数列Fibonacci

斐波那契数列 Fibonacci 斐波那契数列是这样的数列: 0、1、1、2、3、5, 8、13、21、34 …… 下一项是上两项的和。 2 是上两项的和(1+1) 3 是上两项的和(1+2)、 5 是(2+3)、 依此类推! 更多有意思的介绍可以见参考链接; 算法 1. 直接递归 初步想法就是采用递归的…

回顾斐波那契数列

一 概述 斐波那契数列(Fibonacci sequence),又称黄金分割数列。该数列是指递归方法定义为: F(0) 0,F(0) 1,F(n) F(n-1) F(n-2) (n ≥ 2,n ∈ N*) 的数列。 具体表现形式为:0&am…

题:斐波那契数列(Fibonacci数列)——一个数最少几步变成斐波那契数列的数

题目描述: Fibonacci数列就形如:0, 1, 1, 2, 3, 5, 8, 13, ...,在Fibonacci数列中的数我们称为Fibonacci数。给你一 个N,你想让其变为一个Fibonacci数,每一步你可以把当前数字X变为X-1或者X1,现在给你一个…

Fibonacci数列前20项(斐波那契数列)

HZKs Blog 本文标题: Fibonacci数列前20项(斐波那契数列) 本文链接: https://blog.zekun.fun/2021/coding/cppfibonacci数列前20项斐波那契数列/ 题目描述 Fibonacci数列的特点:第1,2个数为1,1。从第3个数开始,概述是前面两个数之…

for_each函数用法

Introduction 學習過STL的container後&#xff0c;想要存取每一個iterator&#xff0c;你一定寫過以下的程式 #include < vector > #include < iostream > using namespace std; int main() { int ia[] {1, 2, 3}; vector<int> ivec(ia, ia size…

each函数用法

<?php/*each()只是一个函数&#xff0c;参数就是一个数组&#xff0c;返回的值也是一个数组1.返回的值也是一个数组&#xff0c;数组固定有4个元素&#xff0c;而且下标也是固定的1(值) value(值) 0(下标) key(下标)2.each()只是处理当前的元素将当前的元素(默认当前元素是…

$.each的用法

1、示例1&#xff1a; <script type"text/javascript">var arr["1","2","3"];<span style"white-space:pre"> </span>//定义arr数组$.each(arr, function() {alert(this);<span style"white-spac…

for_each用法示例

文章目录 前言示例demo 前言 由于偶然间发现for_each能使得避免使用for循环&#xff0c;大大简化了代码。这里简单记录下for_each的一个简单示例demo,方便温习。 示例demo #include <iostream> #include "vector" #include "algorithm"void myfun…

字长、字节、字、字位的区别

字长、字节、字、字位的区别&#xff1a; &#xff08;1&#xff09;概念不一样 同一时间处理二进制数位数叫字长&#xff0c;字长是直接用二进制代码指令表达的计算机语言。 字节&#xff08;Byte &#xff09;是计算机信息技术用于计量存储容量的一种计量单位&#xff0c;…

微型计算机一个汉字多少字节,一个汉字多少字节(Byte)?

2006-01-07 一个汉字有几个字节&#xff1f; 依据编码形式&#xff1a; GB&#xff0d;231280 编码为 2个字节(Byte) 包含了 20902 个汉字&#xff0c;其编码范围是 0x8140-0xfefe。 GB18030-2000(GBK2K) 在 GBK 的基础上进一步扩展了汉字&#xff0c;增加了藏、蒙等少数民族的…

计算机中1kb等于多少字节,在计算机中1kb等于多少字节

在计算机中1kb等于1024个字节。字节是计算机信息技术用于计量存储容量的一种计量单位&#xff0c;也表示一些计算机编程语言中的数据类型和语言字符。一个字节存储8位无符号数。 本文操作环境&#xff1a;windows10系统、thinkpad t480电脑。 (学习视频分享&#xff1a;编程视频…

使用Java8新特性对List进行排序

前言&#xff1a; 在项目开发中往往会遇到各种数据需要排序展示在页面上&#xff0c;常见的从数据库查使用数据库的排序&#xff0c;还有一种就是使用我们的开发语言进行排序&#xff0c;这里给大家演示使用java8的新特性进行排序&#xff0c;众所周知java8带来了函数式编程和…

java8特性之forEach篇

java8特性之forEach篇 forEach介绍使用条件迭代原理性能 forEach介绍 forEach是java8的特性之一&#xff0c;它可以大大简化代码的操作&#xff0c;比如有关HashMap的操作&#xff1a; HashMap<Integer, String> hashMap new HashMap<>(3); hashMap.put(1, &quo…