时间轮 作用 也是用来作定时器触发任务,只是他更高效,时间复杂度为O(1)。 – 42200(6060*12)秒 时间与每层的索引关系 举个简单的4层时间轮例子(如左上图),假设最小计时单位为1(姑且理解为秒) 时间轮初始为0,那么给定任意时间time,求time落在每层时间轮的索引 显然: 秒:3820%60 = 40 分:3820/60%60 = 3; 时:3820/60/60%12 = 1; 如何把事件与时间轮结合? 2、1层 (53) time%45 == 0 时需要找把4层的事件更新到3、2、1层 (53*3) … **注意:**时间轮会有自己的最大计时区间,区间范围取决于时间轮层数及每层数组的大小,下图只有 数据结构: 1、定义任务节点,组成任务链,节点应该包含需要执行的任务和任务执行的时间(以时间轮为起始点的时间) 2、定长链表数组,组成多层轮子,链表的节点为1定义的节点 3、定义时间变量,记录时间轮从起始时刻到当前的时间
因为时间轮算法的精度取决于,时间段“指针”单元的最小粒度大小,比如时间轮的格子是一秒跳一次,那么调度精度小于一秒的任务就无法被时间轮所调度。 2.2.带圈数的时间轮 第一种最基础的时间轮中其实缺陷还是挺明显的。例如我设置一个间隔为1分钟,环形队列节点数为60个节点的时间轮。 2.3.多层时间轮 再回头看一下2.2。如果任务的跨度时间很长,那么时间轮将会空转,只有round为0才会进行任务的执行,这个无疑是很消耗cpu的。 三.应用 时间轮的使用在各大框架与中间件中有使用,xxl-job,netty,kafka都对时间轮都自己的实现。思路基本上与删除的时间轮策略一致。 四.参考 时间轮在 Kafka 的实践
一、什么是时间轮? 这个时候时间轮就呼之欲出了,下面就是时间轮的表演时间了 我们固定数组的长度为60个格子,每个格子的精度为1s,那么一圈就是60s,如果我有3个任务A、B、C,他们相对于启动轮询线程开始走第一个格子的时间差分 别为3s,50s,55s,那么其对应的格子为 轮询线程只要走到对应A、B、C的格子就可以执行它们了,但是如果我有一个一万秒之后执行的任务D,该怎么办呢? 当然是可以的,假设我有一个任务E是在某点某分某秒执行,那么我们可以定义三个时间轮, 分别是秒时间轮,分时间轮,小时时间轮 秒时间轮:总共60个格子,每格1s 分时间轮:总共60个格子,每格1分钟 30个格子之后,又会把任务E存放到秒时间轮的第20个格子上,等秒时间轮走到第20个格子上之后 就会执行任务,我们管这种时间轮叫做层级时间轮。
如下图所示: 中间的圆轮代表一个时间周期,轮子上的每个节点关联的链表代表该时间点要触发的任务。 如上图所示,假设一个格子是1秒,则整个wheel能表示的时间段为8s,假如当前指针指向2,此时需要调度一个3s后执行的任务,显然应该加入到(2+3=5)的方格中,指针再走3次就可以执行了;如果任务要在10s 3.初始化 回到刚开始创建时间轮的代码: HashedWheelTimer timer = new HashedWheelTimer(1, TimeUnit.SECONDS, 16); 构造器有3个参数 如果有时间跨度较长的定时任务,则可以交给层级时间轮去调度。 : 原生时间轮是单机的,在分布式和多实例部署的场景中乏力 宕机重新恢复执行,原生时间轮的存储是Mpsc队列,毫无疑问是内存存储,如果出现宕机或者重启,数据是不可恢复的 对于类似订单超时取消的场景,可以考虑时间轮
书中提到三种定时器,分别是:基于升序链表的定时器,基于时间轮的定时器,基于时间堆的定时器。三种定时器的实现书中均是给了C++代码,不过我对C++不太感兴趣,虽然现在在做C++开发,因此写了C版本的。 说一下时间轮,下面是截的书中的图片 时间轮,像轮子一样滚动定时,每滚一个刻度,指针就走一个滴答,滚完一圈,就进入下一圈。 因此有了这个概念,时间轮的结构也就出来了:1.齿轮(槽slot),用来标识一个滴答;2.槽间隔(slot interval ),当前槽经过多长时间到下一个槽;3.一圈的槽数量(N);4.当前指针,走一个滴答加一 ,即时间轮转多少转 //此定时器可以处于当前转,若再加上槽 //即可确定此定时器所处时间轮位置 int rotation; //处于当前时间轮转的第几个槽 int slot; //定时器到时执行的回调函数 ,每经过一个间隔时间,加一实现轮转动, //超过总槽数即归零表示当前轮转完 int cur_slot; //时间轮一转的总槽数,总槽数越大槽链表越短,效率越高 int slot_num_r; //相邻时间槽间隔时间
timewheel Golang实现的时间轮 项目地址 原理 延迟消息的实现 安装 go get -u github.com/ouqiang/timewheel 使用 package main import ( "github.com/ouqiang/timewheel" "time" ) func main() { // 初始化时间轮 // 第一个参数为tick刻度, 即时间轮多久转动一次 // 第二个参数为时间轮槽slot数量 // 第三个参数为回调函数 tw := timewheel.New(1 * time.Second, 3600, func(data timewheel.TaskData) { // do something }) // 启动时间轮 tw.Start() // 添加定时器 timewheel.TaskData{ "uid" : 105626}) // 删除定时器, 参数为添加定时器传递的唯一标识 tw.RemoveTimer(conn) // 停止时间轮
dubbo-common 包中,有个比较有趣的功能 —— HashedWheelTimer,也就是时间轮。 3. 使用 CAS 的机制,来控制线程的 start、stop。 ? 4. worker 内部类实现了Runable 接口。 ? 5. ---- 总结 Dubbo 时间轮,涉及到了大量的多线程开发代码,对学习如何运用多线程有很大的帮助。
约定某个时间点执行。 其实这两者是可以互相转换的,比如现在有一个定时任务是12点执行,当前时间是9点,那就可以认为这个任务是3小时后执行。 同样,现在又有一个任务,是3小时后执行,那也可以认为这个任务12点执行。 假设我们现在有3个定时任务A、B、C,分别需要在3点、4点和9点执行,我们把它们都转换成绝对时间。 重复步骤2和步骤3。 如果哪一天这个任务不需要再执行了,那么直接通知时间轮,找到这个任务的位置删除掉就可以了。 针对空间复杂度的问题:分层,每个时间粒度对应一个时间轮,多个时间轮之间进行级联协作。 假设现在有3个定时任务: 1. 任务一每天上午9点执行 2. 任务二每周2上午9点执行 3. 根据这三个任务的调度粒度,可以划分为3个时间轮,月轮、周轮和天轮,初始添加任务时,任务一被添加到天轮上,任务二被添加到周轮,任务三被添加到月轮上。
时间轮是一种高性能定时器。 时间轮,顾名思义,就是一个基于时间的轮子,轮子划分为多个槽,每个槽代表一个时间跨度,槽的数量*时间跨度等于时间轮可以支持的最大延迟时间。 当我们有一个需要延迟3小时的任务时,我们只需要把任务放到指定槽,并且设置round=3即可。 我们可以把时间轮分级,n+1层时间轮的槽时间跨度为n层时间轮的一圈的总时间跨度,所以当n层时间轮推进一圈时,n+1层时间轮推进一个槽,并且只有第一层时间轮实际处理定时任务,其余n+1层时间轮转动后,都是把当前槽的任务降级挂载到第 例如,我们如图有三级时间轮,一级时间轮每个槽1秒时间跨度,3600槽,即一圈总时间跨度1小时。二级时间轮每个槽1小时时间跨度,24个槽,即一圈总时间跨度1天。 但是也产生了另外一个问题,即n级时间轮推进一圈后,需要等待n+1级时间轮降级后才可以继续推进,如果n+1级时间轮的降级操作很耗时,则会影响n级时间轮的正常推进。
时间轮算法思想 无论通过何种方式实现定时任务队列,最终需要向上层提供如下接口: 添加定时任务; 删除(取走)定时任务; 执行定时任务; 2.1 简单时间轮算法 时间轮算法的核心是:轮询线程不再负责遍历所有任务 如果要将时间精度设为秒,那么整个时间轮将需要 86400 个单位的时间刻度,此时时间轮算法的遍历线程将遇到更大的运行效率低的问题。下面两个小节将着力解决此问题。 2.2 带有 round 的时间轮算法 我们发现,时间轮的时间刻度随着时间精度而增加并不是一个好的问题解决思路。现在,我们将时间轮的精度设置为秒,时间刻度个数固定为 60。 此时时间轮算法的数据结构如下图所示: 这种方式虽然简化了时间轮的刻度个数,但是并没有简化轮询线程运行效率不高的问题。时间轮每次处理一个时间刻度,就需要处理其上任务队列的所有任务。 2.3 分层时间轮算法 分层的时间轮算法在生活中有对应的模型(艺术来源于生活~),那就是水表: 此时,我们有秒、分钟、小时级别的三个时间轮,每一个时间轮分别有 60、60、24 个刻度。
任务执行: 任务在其对应的时间槽到达时被执行。执行完毕后,任务可以选择从时间轮中删除,或者如果需要周期性执行,可以重新计算其下次执行的时间并再次添加到时间轮中。 时间轮的优点高效性:时间轮避免了使用最小堆或其他数据结构频繁地插入和删除操作,这些操作通常是对数时间复杂度。时间轮的插入和删除操作可以视为常数时间复杂度,因为它们只涉及到数组索引的操作。 简单:时间轮的结构简单,使得时间的前进和任务的调度非常直接,只涉及数组的索引操作和链表操作。层级时间轮对于处理更长时间范围或更高精度的需求,可以使用多层时间轮。 层级时间轮由多个时间轮组成,每个时间轮负责不同的时间粒度和范围。例如,第一层时间轮可能每个槽代表1毫秒,而第二层时间轮的每个槽可能代表1秒。这种结构可以有效地扩展时间轮处理的时间范围和精度。 对于时间轮的实现,我们可以利用第三方库,如netty中的HashedWheelTimer,它是一个用于处理超时事件的高性能时间轮实现。
时间轮 简述 顾名思义,时间轮就像一个轮子,在转动的时候外界会指向轮子不同的区域,该区域就可以被使用。 因此只要将不同时间的定时器按照一定的方法散列到时间轮的不同槽(即时间轮划分的区域)之中,就可以实现在运转到某个槽时,进行判断该定时器是否已经到达运行时间(需要判断是由于有的定时器并非在这一圈就需要运行, 至于在每转到一个槽时都要检查是否到达运行时间,可以这样理解:时间轮进行散列的方法就是取余运算,假设每个槽的间隔为1s,共有n个槽,当前转到了第cur个槽,那么一个定时在 t s以后运行的定时器就要放在第 对实现时间轮来说,最主要的还是链表的操作是否熟练,当然也主要是双向链表的添加与删除。 代码分析 记录定时器的时间信息,从而获取在时间轮中槽的位置,以及在多少圈之后被触发。 class TwTimer { public: int rotation; // 定时器转多少圈后生效 int time_slot; // 记录定时器属于时间轮的哪个时间槽 client_data
如上图所示,假设一个格子是1秒,则整个wheel能表示的时间段为8s,假如当前指针指向2,此时需要调度一个3s后执行的任务,显然应该加入到(2+3=5)的方格中,指针再走3次就可以执行了;如果任务要在10s 构造器有3个参数(时间单位不介绍): tickDuration:时间间隔;HashedWheelTimer 会在每个tick中检查是否有任何 TimerTask 落后于计划并执行它们。 最后while循环是等待worker线程启动完成,由于是多线程所以采用了CountDownLatch类型的startTimeInitialized.await. 3.任务超时操作 时间轮创建和初始化完成后 如果有时间跨度较长的定时任务,则可以交给层级时间轮去调度。 : 原生时间轮是单机的,在分布式和多实例部署的场景中乏力 宕机重新恢复执行,原生时间轮的存储是Mpsc队列,毫无疑问是内存存储,如果出现宕机或者重启,数据是不可恢复的 对于类似订单超时取消的场景,可以考虑时间轮
时间轮是一种可以执行定时任务的数据结构和算法.这篇文章,讲解一下它在Netty 3.x系列中如何实现的,它在4.x系列将在后面的文章中讲解. 首先说明一点的是,时间轮是按照顺时针的方向'转动',也就是按照顺时针的方向执行时间轮上的任务. * n 时间轮初始化之后,它的结构如下图 假如此时时间轮正在执行下图中S格子中的任务,这时向时间轮中添加一个延时delay的任务,时间轮会根据当前所处的位置和时刻,计算新添加的任务应该放在哪个格子位置 while (i.hasNext()) { HashedWheelTimeout timeout = i.next(); // 有些任务是在这一圈就要执行,有些任务是要等到第2圈,第3圈 任务在提交到时间轮的时候,它何时被执行,已经被确定了.
关于时间轮的实现原理详细介绍可以参考文章: https://blog.csdn.net/u013256816/article/details/80697456。是介绍kafka时间轮的实现。 vpp 中实现细节时间轮也是一样的。这里不再讲解了。 tw_timer_template.h的实例化生成命名结构来实现特定的计时器时间轮。 选择包括:计时器轮的数量(当前最多支持3个),每个时间轮的环槽的数量(必须是2的幂),以及每个“对象句柄”的计时器数量。 函数可以了解到用户时间轮句柄user_handle是由2部分组成的。 1、时间轮初始化 初始化一个间隔1s的的时间轮。每秒时间轮转一个槽位,最多支持2048s。timer interval也可以设置成0.1s,那么每秒ticks就是10。
将任务添加到时间轮中十分简单,对于每个时间轮来说,比如说秒级时间轮,和分级时间轮,都有它自己的过期槽。也就是delayMs < tickMs的时候。 比如说有一个任务要在 16:41:25 执行,从分级时间轮中来看,当我们的当前时间走到 16:41的时候(分级时间轮没有秒针! 3、时间未到期,且delayMs大于interval 对于妙级时间轮来说,就是延迟时间大于等于60s,这时候就需要借助上层时间轮的力量了,很简单的代码实现,就是拿到上层时间轮,然后类似递归一样,把它扔进去 当然我们的时间轮还需要一个指针的推进机制,总不能让时间永远停留在当前吧?推进的时候,同时类似递归,去推进一下上一层的时间轮。 再倒回去好好看看,添加到最底层时间轮失败的(我们只能直接操作最底层的时间轮,不能直接操作上层的时间轮),是不是会直接返回flase?对于再入失败的任务,我们直接执行即可。 ?
时间轮由多个时间格组成,每个时间格代表当前时间轮的基本时间跨度(tickMs)。 Kafka为此引入了层级时间轮的概念,当任务的到期时间超过了当前时间轮所表示的时间范围时,就会尝试添加到上层时间轮中。 对于之前所说的350ms的定时任务,显然第一层时间轮不能满足条件,所以就升级到第二层时间轮中,最终被插入到第二层时间轮中时间格17所对应的TimerTaskList中。 这里就有一个时间轮降级的操作,会将这个剩余时间为50ms的定时任务重新提交到层级时间轮中,此时第一层时间轮的总体时间跨度不够,而第二层足够,所以该任务被放到第二层时间轮到期时间为[40ms,60ms)的时间格中 除了第一层时间轮,其余高层时间轮的起始时间(startMs)都设置为创建此层时间轮时前面第一轮的currentTime。
可以通过指定每一格的时间间隔来改变执行时间的精确度。在大多数网络应用中,I/O超时不需要十分准确,因此,默认的时间间隔是100 毫秒,这个值适用于大多数场合。 参数解释 首先创建时间轮,因为项目中只能出现一个实例所以直接用final修饰;public final HashedWheelTimer timer_wheel = new HashedWheelTimer (1L, TimeUnit.SECONDS, 60); 参数解释:/**long tickDuration:滴答时间,刻度之间的持续时间; TimeUnit unit: 滴答时间单位 int (), tickDuration, unit, ticksPerWheel);}理解起来就像钟表,tickDuration 1s滴答一下,ticksPerWheel 刻度盘为60 ;连起来就是创建一个时间论 ,一秒滴答一次,刻度盘为60,也就是60S 后重新开始/**TimerTask task:延时执行的任务,需要实现接口TimerTasklong delay:延时时间的时间TimeUnit unit:延时的单位
时间轮java实现 一、java调度方法: 前言 在开发高性能服务器中,定时器总是不可或缺的。 常见的定时器实现三种,分别是:排序链表,最小堆,时间轮。 Timer,ScheduledExecutorService 时间复杂度 O(log(n)) 因为它们使用的 是 最小堆的对排序,每当有新任务的时候都需要堆堆进行插入, 堆排序插入的时间复杂度为 O(log 1) O(1) 基于最小堆 O(lgn) O(1) O(1) 二、时间轮调度算法: 比java调度算法更高效的算法,时间复杂度为O(1) 1、如果执行任务抛出异常,会执行后面的任务的 2、1s执行任务一 ,2s执行任务二 3、1s执行多个任务 算法对比 实现方式 加入任务 取消任务 运行任务 基于排序链表 O(n) O(1) O(1) 基于最小堆 O(lgn) O(1) O(1) 基于时间轮 O(1 ) O(1) O(1) 简单时间轮算法原理: 项目下载地址: https://download.csdn.net/download/a1229451567/10970874 版权声明:本文内容由互联网用户自发贡献
Kafka对时间轮做了一层改进,使时间轮变成层级的时间轮。 一开始,第一层的时间轮所能表示时间范围是0~20Ms之间,假设现在出现一个任务的延迟时间是200Ms,那么kafka会再创建一层时间轮,我们称之为第二层时间轮。 槽的数量还是一样,其他的属性也是继承自第一层时间轮。这时第二层时间轮所能表示的时间范围就是0~400Ms了。 值得注意的是,只有当前时间轮无法容纳目标延迟任务所能表示的时间时,才需要创建更高一级的时间轮,或者说把该任务加到更高一级的时间轮中(如果该时间轮已创建)。 ,是通过expiration < currentTime + interval来计算的,也就是根据时间轮的指针往后推interval时间就是时间轮所能表示的时间范围。