时间轮(TimingWheel)算法总结

article/2025/7/28 3:30:54

通过阅读篇文章您可以很容易理解平时所使用的开源框架是如何进行任务调度的。而且对于以后业务上碰到需要做时间任务调度的需求,也可以尝试着用实践论算法去实现。


一、时间轮的应用

其实早在1987年,时间轮算法的论文已经发布。论文名称:Hashed and Hierarchical Timing Wheels,公众号回复:TimingWheel,获取PDF版本。

时间轮(TimingWheel)算法应用范围非常广泛,各种操作系统的定时任务调度都有用到,我们熟悉的linux crontab,以及Java开发过程中常用的Dubbo、Netty、Akka、Quartz、ZooKeeper 、Kafka等,几乎所有和时间任务调度都采用了时间轮的思想。

二、时间轮算法的作用

时间轮是一种实现延迟功能(定时器)巧妙算法。如果一个系统存在大量的任务调度,时间轮可以高效的利用线程资源来进行批量化调度。把大批量的调度任务全部都绑定时间轮上,通过时间轮进行所有任务的管理,触发以及运行。能够高效地管理各种延时任务,周期任务,通知任务等。相比于JDK自带的 Timer、DelayQueue + ScheduledThreadPool 来说,时间轮算法是一种非常高效的调度模型。

不过,时间轮调度器的时间精度可能不是很高,对于精度要求特别高的调度任务可能不太适合,后面我们会分享linux高精度任务调度的实现。因为时间轮算法的精度取决于时间段“指针”单元的最小粒度大小,比如时间轮的格子是一秒跳一次,那么调度精度小于一秒的任务就无法被时间轮所调度。

三、时间轮的实现

3.1 如何实现定时任务调度

定时的任务调度分两种:

  • 一段时间后执行,即:相对时间

  • 指定某个确定的时间执行,即:绝对时间

当然,这两者之间是可以相互转换的,例如当前时间是12点,定时在5分钟之后执行,其实绝对时间就是:12:05;定时在12:05执行,相对时间就是5分钟之后执行。

3.2 时间轮的类型
3.2.1 概念

973b7fab8e031195a8172052058fc97a.png

ptr : 指针,随着时间的推移,指针不停地向前移动。

bucket : 时间轮由bucket组成,如上图,有12个bucket。每个bucket都挂载了未来要到期的节点(即: 定时任务)。

slot : 指相邻两个bucket的时间间隔。

jiffy:slot的单位,1s(1HZ),如上图,总共12个bucket,那么两个相邻的bucket的时间间隔就是一秒。

3.2.2 简单的单时间轮

1e5e9710e265d6185f3e83125e3034d3.png

如上图中相邻bucket到期时间的间隔为slot=1s,从0s开始计时,1s时到期的定时任务挂在bucket[1]下,2s时到期的定时任务挂在bucket[2]下,当检查到时间过去了1s时,bucket[1]下所有节点执行超时动作,当时间到了2s时,bucket[2]下所有节点执行超时动作…….以此类推。

上图的时间轮通过数组实现,可以很方便地通过下标定位到定时任务链路,因此,添加、删除、执行定时任务的时间复杂度为O(1)。

这种单时间轮是存在限制的,只能设置定时任务到期时间在12s内的,这显然是无法满足实际的业务需求的。当然也可以通过扩充bucket的范围来实现,例如将bucket设置成 2^32个,但是这样会带来巨大的内存消耗,显然需要优化改进。


3.2.3 改进版单时间轮

90887797812fe166909cbcf702a1e180.png

改进的单时间轮原理:每个bucket下不单单可以挂载到期时间expire=slot的定时任务,还可以挂载expire%N=slot的定时器(N为bucket个数),如上图,expire代表到期时间,rotation表示时间轮要在转动几圈之后才执行定时器,也就是说当指针转到某个bucket时,不能像简单的单时间轮那样直接执行bucket下所有的定时器。而且要去遍历该bucket下的链表,判断判断时间轮转动的次数是否等于节点中的rotation值,只有当expire和rotation都相同的情况下,才能执行定时器。

改进版单时间轮是时间和空间折中的方案,不像单时间轮那样有O(1)的时间复杂度,也不会像单时间轮那样,为了满足需求产生大量的bucket。

缺点:改进版的时间轮如果某个bucket上挂载的定时器特别多,那么需要花费大量的时间去遍历这些节点,如果bucket下的链表每个节点的rotation都不相同,那么一次遍历下来可能只有极少数的定时器需要立刻执行的,因此很难在时间和空间上都达到理想效果。

3.2.4 多时间轮

实现多时间轮算法可以借鉴了生活中水表的度量方法,通过低刻度走得快的轮子带动高一级刻度轮子走动的方法,达到了仅使用较少刻度即可表示很大范围度量值的效果。

下面以Linux实现的多时间轮算法为例,讲述多时间轮的实现,linux将时间轮分为5个级别的轮子(L1 ~ L5),L1的bucket大小是256,L2、L3、L4、L5的bucket大小是64,每一层的轮子都有一个指针在转动规律是次级时间轮总和是上一级一个bucket的大小,例如:L1的bucket是256,每个slot的 1 jiffy,那么L2上每个slot大小就是 256 jiffy;这样就模拟了水表的量度规律,相邻的时间轮低级别的时间轮转一圈,比它高一个级别的轮子就要走一个bucket的效果。而且最高一级的时间轮L5的范围可以达到2^32 jiffies,基本能满足大部分业务场景的需求。如下图:

732d310577c1e5081729dbcd60a61a03.png

Linux时间轮定时器算法的关键在于添加定时器操作和时间轮进位迁移链表操作。先来说添加定时器。添加定时器的关键又在于知道每个时间轮每一个刻度所能表示的到期时间的范围。上图列出了每一级bucket能度量的jiffies的大小。假设有一个定时器在1000个jiffies后到期,根据上图容易看出其应该挂在L2轮上。L2轮每个刻度表示的大小为256个jiffies,则其应该挂在(1000/256)=3即第三个bucket上。

Linux在定时器到期检查上的操作也实现得很巧妙。假设curr_time=0x12345678,那么下一个检查的时刻为0x12345679。如果L1.bucket[0x79]上链表非空,则下一个检查时刻L1.bucket[0x79]上的定时器节点超时。如果curr_time到了0x12345700,低8位为空,说明有进位产生,这时移出8~13位对应的定时器链表(即正好对应着L2轮),重新加入定时器系统,这就完成了一次进位迁移操作。同样地,当curr_time的第8-13位为0时,这表明L2轮对L3轮有进位发生,将curr_time第14-19位的值作为下标,移出L3中对应的定时器链表,然后将它们重新加入到定时器系统中来。L4,L5依次类推。之所以能够根据curr_time来检查超时链,是因为L1~L5轮的度量范围正好依次覆盖了整型的32位:L1(1-8位),L2(9-14位),L3(15-20位),L4(21-26位),L5(27-32位);而curr_time计数的递增中,低位向高位的进位正是低级时间轮转圈带动高级时间轮走动的过程。

根据上述的设计,最高一级的时间轮L5的范围可以达到2^32 jiffies,基本能满足大部分业务场景的需求。

四、总结

多级时间轮和单个简单时间轮的时间复杂度及空间复杂度:linux使用了总计256+64+64+64+64=512个bucket,即可实现[0,2^32) jiffies的超时范围。相比简单的单时间轮,时间上仅仅多了1/256次(约等于值,忽略了L2以上产生的进位操作)的链表迁移操作耗时。可以认为其添加、删除定时器节点及到期check的操作时间复杂度均为O(1)。接下来的文章将分享时间轮的Java版本实现和应用。

期阅读

Lombok我们真的不应该使用了嘛?

云原生之可观测性-OpenTracing、OpenSensus、OpenTelemetry

云原生之可观测性 - APM概念及选型

大厂“断子绝孙式”、“养蛊式”招聘有多害人?

Kafka Producer全流程分析和思考

MySQL的Replace用法详解

卷不动了,我选择降薪去外企来平衡工作和生活

HBase、Cassandra、LevelDB、RocksDB 相关数据结构是什么?

c51f3d30ec78fa8b7691a19979215cc3.png

快乐程序员、读书狂魔、爱撸代码、开源项目、硬核互联网技术分享

觉得写得不错,请点在看,谢谢!


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

相关文章

时间轮 (史上最全)

缓存之王 Caffeine 中,涉及到100w级、1000W级、甚至亿级元素的过期问题,如何进行高性能的定时调度,是一个难题。 注: 本文从 对 海量调度任务场景中, 高性能的时间轮算法, 做了一个 系统化、由浅入深的 穿透…

时间轮(Timing Wheel)案例和原理

什么是时间轮(Timing Wheel) 时间轮(Timing Wheel)是George Varghese和Tony Lauck在1996年的论文Hashed and Hierarchical Timing Wheels: data structures to efficiently implement a timer facility实现的,它在Linux内核中使用广泛,是Linux内核定时器…

时间轮(TimingWheel)

一、什么是时间轮 时间轮其实就是一种环形的数据结构,可以想象成时钟,分成很多格子,一个格子代表一段时间(这个时间越短,Timer的精度越高)。并用一个双向链表存储放在该格子上的延时任务,同时一…

高性能定时器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;增加了藏、蒙等少数民族的…