时间轮算法(TimingWheel)

article/2025/7/28 3:29:43

时间轮算法的应用非常广泛,在 Dubbo、Netty、Kafka、ZooKeeper、Quartz 的组件中都有时间轮思想的应用,甚至在 Linux 内核中都有用到。

其思想理论基础可以参考论文:http://www.cs.columbia.edu/~nahum/w6998/papers/ton97-timing-wheels.pdf

任务队列的模型设计

实际生产中常用的消息处理组件 Kafka、Redis、MQ 都是基于消息队列的定时任务实现的。归纳来说,一个基本的任务队列模型需要能够支持依赖几个功能:

  • 定时任务的添加
  • 定时任务的删除/获取
  • 定时任务的执行(通常将任务分配给异步线程池来处理)

一个最直接的想法是,为了方便向任务对列表添加或者删除任务,通常选择双向链表作为任务队列的数据结构。每个任务都是指定一个时间戳字段,用以标识当前任务的执行时间。此时任务队列的实现策咯为:任务队列的轮询线程不断扫描每一个任务,比较其时间戳与当前时间,判断是否达到任务的执行时刻。但是如果大部分任务都没有达到指定的执行时间,那么线程会不停地进行轮询的动态,实现效率非常差。

一种改良的方案是,每次插入定时任务时都保证队列的有序性,将任务队列升级成一个按照任务执行时间递增的有序队列。此时任务队列的执行策咯为:轮询线程从头开始遍历任务队列,发现当前任务未达到执行时间时,就可以停止遍历。甚至在时间间隔较大时,轮询线程还可以进行休眠,以避免空轮询消耗资源。但是,保证有序性需要的代价是复杂度为 O(n) 的任务插入动作,与复杂度为 O(1) 的普通任务队列插入相比,性能上有一定的牺牲。

在此基础上再加入“分而治之”的思想进行改良,我们可以将这个可能很大的任务队列拆分成多个小队列,减小有序插入定时任务时实际的性能消耗,同时采用多轮询线程的方式分别对每个队列进行遍历,这样就能将队列规模进行简化,提升任务队列处理的效率。然而多轮询线程的存在增加了CPU的系统调度,如果定时任务数量极大时,多线程轮询的方式反而降低了系统的执行效率。

上述的方案都各自有其优缺点,没有绝对正确或者错误的方案,在特定的场景中都有其适用的因素。但是在一个量级非常大的高并发场景中,我们就需要结合上述方案的长处,设计出一个充分利用时间和空间效率的任务队列算法,以达到最佳的性能效果。时间轮算法就是其中一个可选的方案。

时间轮基本方案

基于上面的分析,我们可以归纳出一个高效的定时任务队列模型需要考虑一下几个因素:

  • 合理的数据结构:任务队列保证有序性的同时,还要保证轮询线程的执行效率;
  • 基本的并发响应:多线程轮询可以提升任务队列的遍历效率,但数量不宜太多,避免影响系统的 CPU 性能。

时间轮就是一种高效利用线程资源的任务调度模型,把大批量的调度任务全部整合到一个调度器中,对任务进行统一的调度管理。针对诸如定时任务、延时任务、通知任务等事件的管理具有很不错的效率。

时间轮本质上是一个环形队列,底层采用了数组进行实现,数组的索引代表其参考的任务执行的时间,数组中的每个元素可以存放一个定时任务列表。定时任务列表同样是一个环状的双向链表,其中每一项代表一个可执行的定时任务项。因此,时间轮的结构可以看作是将同一时刻执行的定时任务聚合在一个任务队列中,并挂在相应时间的数组索引上

请添加图片描述

这里先对时间轮的一些概念进行定义:

  • tickMs:基本时间跨度。时间轮由多个时间格组成,每个时间格代表当前时间轮的基本时间跨度。
  • wheelSize:时间格个数。一个时间轮完成定义后,其时间格的个数是固定的。
  • interval:时间跨度。一个时间轮的总体时间跨度 interval = tickMs * wheelSize。
  • currentTime:当前时间。相当于时间轮的表盘指针,表示当前所处的时间,其取值通常是 tickMs 的整数倍。已经被指向的时间格属于到期部分,其对应的任务队列都需要被取出执行。

随着时间的不断推移,指针 currentTime 的位置不断向前推进。当有一个新的定时任务进来时,则在当前 currentTime 位置的基础上,加上对应的相对时间,则为新任务应该插入的队列位置。

简单时间轮算法

任务队列在加入时间轮时已经按照其执行时间完成了有序的归位,因此当轮询线程遍历到某一个时间格时,当前时间格对应的任务队列中的所有任务就可以开始直接执行,不需要再检查任务执行的时间戳是否已达到。
请添加图片描述

类比钟表表盘的形象,我们的时间轮可以以小时为粒度,每个时间格代表一个小时,但在实际应用中,定时任务的调度常常精确到分钟或者秒钟的粒度。如果一个定时任务需要指定在每天的某一个时刻执行,精确到秒,那么时间轮则需要支持一天 24 * 60 * 60 = 86400 个刻度。其占用的内存空间很大,但实际任务占用的可能只有几个甚至几十个,严重浪费系统资源。

round 的时间轮算法

为了处理上面提到的问题,我们可以在每个定时任务中增设一个 round 字段,用以标识当前任务还需要在时间轮中遍历几轮,才进入执行的时间判断轮。其执行逻辑为:每次遍历到一个时间格后,其任务队列上的所有任务 round 字段减 1,如果 round 字段变为 0,则将任务移出队列,提交给异步线程池来执行其内容。如果这是一个重复任务,那么提交后再将它重新添加到任务队列中。

请添加图片描述

假设现在将时间轮的精度设置为秒,时间轮共有 60 个时间格,那么一个 130 秒后执行的任务,可以将其 round 字段设为 2,并将任务加入到时间刻度为 10 的任务队列中。即对于间隔时间为 x 的定时任务:

  • round = x / 60 (整除)
  • 刻度位置 pos = x % 60

这种方式虽然减少了时间轮的刻度个数,但并没有减少轮询线程的轮询次数,其效率还是相对比较低。时间轮每遍历一个时间刻度,就要完成一次判断和执行的操作,其运行效率与一般的任务队列差别不大,并没有太大的效率提升。完成了一次遍历,但是并没有提交可执行的任务,这种现象可以称之为“空轮询”。

分层时间轮算法

另一种对简单时间轮算法改良的方案,可以参照钟表中时、分、秒的设计,设置三个级别的时间轮,分别代表时、分、秒,且每个轮分别带有 24、60、60 个刻度。这样子三个时间轮结合使用,就能表达一天内所有的时间刻度了。

请添加图片描述

当 hour 时间轮的轮询线程轮询到执行的时间格时,其对应的任务队列已达到其执行的 hour 时间。此时这些任务需转移到下一层 minute 时间轮中,根据其执行时间的 minute 位,插入到对应的任务队列中。后续的步骤都类似,直至到最后一层 second 的时间轮中,被轮询到的队列即可提交其所有的任务到异步执行线程池中。

采用分层时间轮的方式,不需要引入 round 字段,只要在最后一级遍历到的任务队列,必然是可提交执行的,进而避免了空轮询的问题,提高了轮询的效率。每个时间轮的遍历由不同的轮询线程实现,虽然引入的线程并发,但是线程数仅仅跟时间轮的级数有关,并不会随着任务数量的增加的增加。

时间轮算法的进一步优化

通过分层时间轮,我们可以将一系列定时任务根据其执行时间进行分组和排序,依次分配到时间轮的对应时间格中。只要时间轮的指针到达特定时间格,相应任务队列中的所有任务即可提交执行。但是在实际应用中,真正需要执行任务的时间格在所有时间格中的占比是很小的。假如第一个待执行的任务列表的 expiration 为 100s,以每秒推进一格的方案来看,在获取到第一个可执行的任务列表前,会出现 99 次的空轮询,也就是时间轮指针推进了,但并没有任务执行的情况。这种空轮询的存在,并没有太大的业务含义,白白耗费了系统的性能资源。

为了处理好空轮询的问题,这里可以再引入 DelayQueue 来维护每个定时任务列表,进而减少空轮询的次数,实现精确轮询。具体方案就是,根据 DelayQueue 中前后相邻的任务队列的 expiration 来确定时间轮指针推进的时间,精确地在下一个任务执行的时间点时对该列表进行轮询。

性能分析

一个初级的定时任务框架,可以采用有序任务队列 + 轮询线程的方式进行实现,有序任务队列通常基于优先队列(堆)进行实现,因此其任务的插入和删除的时间复杂度为 O(log N) 。而时间轮算法,能够将时间复杂度降低至 O(1),效率明显得到提升。而算法的主要性能损耗,则体现在多个时间轮轮询线程的时间推进,以及他们与任务执行线程之间的切换。这方面的复杂度,明显小于基本的时间轮算法还有普通的任务队列。

定义:

  • n - 任务数量
  • k - 多线程轮询的线程数
  • 常数 M - 全时段时间轮刻度数量(空间单位数)
  • 常数 L - 单 round 时间轮刻度数量(空间单位数)
  • l i l_i li - 第 i 层时间轮刻度数量(空间单位数)
  • T - 存在任务队列的空间单位数
模型插入复杂度删除复杂度轮询复杂度空间复杂度
普通双向链表O(1)O(n)O(n)O(n)
有序双向链表O(n)O(n)O(n)O(n)
k 个分组的有序双向链表O(n)O(n)O(n / k) ; CPU的系统调度O(n)
简单时间轮O(1)O(1)MM
round 时间轮O(1)O(1)ML
分层时间轮O(1)O(1) Σ l i \Sigma l_i Σli Σ l i \Sigma l_i Σli
分层时间轮 + 延时队列O(1)O(1)T ( <= Σ l i \Sigma l_i Σli ) Σ l i \Sigma l_i Σli

实现 demo 可以参考资源:https://download.csdn.net/download/sinat_36645384/86800184


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

相关文章

那些惊艳的算法们(三)—— 时间轮

从定时任务说起 自然界中定时任务无处不在&#xff0c;太阳每天东升西落&#xff0c;候鸟的迁徙&#xff0c;树木的年轮&#xff0c;人们每天按时上班&#xff0c;每个月按时发工资、交房租&#xff0c;四季轮换&#xff0c;潮涨潮落&#xff0c;等等&#xff0c;从某种意义上…

1、时间轮

一、什么是时间轮&#xff1f; 作为一个粗人&#xff0c;咱不扯什么高级的词汇&#xff0c;直接上图&#xff1a; 上面是一张时间轮的示意图&#xff0c;可以看到&#xff0c;这个时间轮就像一个钟表一样&#xff0c;它有刻度&#xff0c;图中画了9个格子&#xff0c;每个格子…

浅谈时间轮算法

浅谈时间轮算法 基于队列的定时任务执行模型缺陷 在计算机世界中&#xff0c;只有待解决的问题变得大规模后&#xff0c;算法的价值才能够最大化的体现。时间轮算法可以将插入和删除操作的时间复杂度都降为 O(1)&#xff0c;在大规模问题下还能够达到非常好的运行效果。 如果…

时间轮(TimingWheel)算法总结

通过阅读篇文章您可以很容易理解平时所使用的开源框架是如何进行任务调度的。而且对于以后业务上碰到需要做时间任务调度的需求&#xff0c;也可以尝试着用实践论算法去实现。 一、时间轮的应用 其实早在1987年&#xff0c;时间轮算法的论文已经发布。论文名称&#xff1a;Hash…

时间轮 (史上最全)

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

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

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

时间轮(TimingWheel)

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

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

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

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

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

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 天前&#xff0c;其中的信息可能已经有所发展或是发生改变。 题目描述 输入一个正整数n&#xff0c;求Fibonacci数列的第n个数。Fibonacci数列的特点&#xff1a;第1,2个数为1,1。从第3个数开始&#xff0c;概述是前面两个数之和。即&#xff1a; 要求输入的…

递归求斐波那契数列

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

斐波那契数列(Fibonacci)

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

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

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

算法-斐波那契数列Fibonacci

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

回顾斐波那契数列

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

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

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

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

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

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()只是处理当前的元素将当前的元素(默认当前元素是…