那些惊艳的算法—时间轮任务调度(sunwind整理)

从定时任务说起

自然界中定时任务无处不在,太阳每天东升西落,候鸟的迁徙,树木的年轮,人们每天按时上班,每个月按时发工资、交房租,四季轮换,潮涨潮落,等等,从某种意义上说,都可以认为是定时任务。

大概很少有人想过,这些“定时”是怎样做到的。当然,计算机领域的同学们可能对此比较熟悉,毕竟工作中的定时任务也是无处不在的:每天凌晨更新一波数据库,每天9点发一波邮件,每隔10秒钟抢一次火车票。。。

至于怎么实现的?很简单啊,操作系统的crontab,spring框架的quartz,实在不行Java自带的ScheduledThreadPool都可以很方便的做到定时任务的管理调度。

当你熟练的敲下

* * 9 * * ?

等着神奇的事情发生时,你是否想过背后的“玄机”?

初识时间轮

大概去年的时候,业务需要实现一个时间调度的工具,定时生成报表,同组的哥们儿想了一个取巧的办法:

  1. 启动时从DB读取cron表达式解析,算出该任务下次执行的时间。
  2. 下次执行时间-当前时间=时间差。
  3. 向ScheduleThreadPool线程池中提交一个延迟上面算出来的时间差的执行的任务。
  4. 任务执行时,算一下这个任务下次执行的时间,算时间差,提交到线程池。
  5. 当任务需要取消时,直接调用线程池返回的Future对象的cancel()方法就行了。

当时稍微想了⼀ScheduleThreadPool是怎么做到定时执⾏提交过来的任务的,⼤概有个模糊的概念,后来就把这事忘了。再后来,⼀次在地铁上看到⼀篇⽂章,讲了⼀种叫做时间轮的定时任务调度思想,感觉想法很不错,当年那个模糊的概念似乎清晰了很多,再后来,⼀个偶然的机会,⽹上搜了⼀下,竟然有⼀篇专门讲解时间轮算法的论⽂,顿时兴奋⽆⽐,赶紧打印下来,在上班的地铁上读了半个⽉,总算读完了。

戳这⾥下载:《Hashed and Hierarchical Timing Wheels》

论⽂中的思路很简单但也⼗分巧妙,对算法不断的改进对⽐,各种操作系统,框架中的基于时间的调度算法都是基于时间轮的思想实现的。下⾯我们来看看,这个神奇的时间轮到底是怎样实现定时任务的调度的。

绝对时间和相对时间

定时任务⼀般有两种:

  1. 约定⼀段时间后执⾏。
  2. 约定某个时间点执⾏。

聪明的你会很快发现,这两者之间可以相互转换,⽐如给你个任务,要求12点执⾏,你看了⼀眼时间,发现现在是9点钟,那么你可以认为这个任务三个⼩时候执⾏。

同样的,给你个任务让你3个⼩时后执⾏,你看了⼀眼现在是9点钟,那么你当然可以认为这个任务12点钟执⾏。

我们先来考虑⼀个简单的情况,你接到三个任务A、B、C(都转换成绝对时间),分别需要再3点钟,4点钟和9点钟执⾏,正当百思不得其解时,不经意间你瞅了⼀眼墙上的钟表,瞬间来了灵感,如醍醐灌顶,茅塞顿开:
请添加图片描述

如上图中所⽰,我只需要把任务放到它需要被执⾏的时刻,然后等着时针转到这个时刻时,取出该时刻放置的任务,执⾏就可以了。 这就是时间轮算法最核⼼的思想了。 什么?时针怎么转? while-true-sleep 下⾯让我们⼀点⼀点增加复杂度。

需要重复执⾏多次的任务

多数定时任务是需要重复执⾏,⽐如每天上午9点执⾏⽣成报表的任务。对于重复执⾏的任务,其实我们需要关⼼的只是下次执⾏时间,并不关⼼这个任务需要循环多少次,还是那每天上午9点的这个任务来说。

  1. ⽐如现在是下午4点钟,我把这个任务加⼊到时间轮,并设定当时针转到明天上午九点(该任务下次执⾏的时间)时执⾏。
  2. 时间来到了第⼆天上午九点,时间轮也转到了9点钟的位置,发现该位置有⼀个⽣成报表的任务,拿出来执⾏。
  3. 同时时间轮发现这是⼀个循环执⾏的任务,于是把该任务重新放回到9点钟的位置。
  4. 重复步骤2和步骤3。 如果哪⼀天这个任务不需要再执⾏了,那么直接通知时间轮,找到这个任务的位置删除掉就可以了。

由上⾯的过程我们可以看到,时间轮⾄少需要提供4个功能:

  1. 加⼊任务
  2. 执⾏任务 (继续放回)
  3. 删除任务
  4. 沿着时间刻度前进

同⼀时刻存在多个任务

上⾯说的是同⼀个时刻只有⼀个任务需要执⾏的情况,更通⽤的情况显然是同⼀时刻可能需要执⾏多个任务,⽐如每天上午九点除了⽣成报表之外,还需要执⾏发送邮件的任务,需要执⾏创建⽂件的任务,还需执⾏数据分析的任务等等,于是你刚才可能就⽐较好奇的时间轮的数据结构到现在可能更加好奇了,那我们先来说说时间轮的数据结构吧。

时间轮的数据结构

⾸先,时钟可以⽤数组或者循环链表表⽰,这个每个时钟的刻度就是⼀个槽,槽⽤来存放该刻度需要执⾏的任务,如果有多个任务需要执⾏呢?每个槽⾥⾯放⼀个链表就可以了,就像下⾯图中这样:
在这里插入图片描述

同⼀时刻存在多个任务时,只要把该刻度对应的链表全部遍历⼀遍,执⾏(扔到线程池中异步执⾏)其中的任务即可。

时间刻度不够⽤怎么办?

如果任务不只限定在⼀天之内呢?⽐如我有个任务,需要每周⼀上午九点执⾏,我还有另⼀个任务,需要每周三的上午九点执⾏。⼀种很容易想到的解决办法是:

增⼤时间轮的刻度

⼀天24个⼩时,⼀周168个⼩时,为了解决上⾯的问题,我可以把时间轮的刻度(槽)从12个增加到168个,⽐如现在是星期⼆上午10点钟,那么下周⼀上午九点就是时间轮的第9个刻度,这周三上午九点就是时间轮的第57个刻度,⽰意图如下:
在这里插入图片描述

仔细思考⼀下,会发现这中⽅式存在⼏个缺陷:

  1. 时间刻度太多会导致时间轮⾛到的多数刻度没有任务执⾏,⽐如⼀个⽉就2个任务,我得移动720次,其中718次是⽆⽤功。

  2. 时间刻度太多会导致存储空间变⼤,利⽤率变低,⽐如⼀个⽉就2个任务,我得需要⼤⼩是720的数组,如果我的执⾏时间的粒度精确到秒,那就更恐怖了。

于是乎,聪明的你脑袋⼀转,想到另⼀个办法:

列表中的任务中添加round属性

这次我不增加时间轮的刻度了,刻度还是24个,现在有三个任务需要执⾏,

  1. 任务一每周二上午九点。
  2. 任务⼆每周四上午九点。
  3. 任务三每个⽉12号上午九点。

⽐如现在是9⽉11号星期⼆上午10点,时间轮转⼀圈是24⼩时,到任务⼀下次执⾏(下周⼆上午九点),需要时间轮转过6圈后,到第7圈的第9个刻度开始执⾏。

任务⼆下次执⾏第3圈的第9个刻度,任务三是第2圈的第9个刻度。

⽰意图如下:
在这里插入图片描述

时间轮每移动到⼀个刻度时,遍历任务列表,把round值-1,然后取出所有round=0的任务执⾏,若是重复任务,执行完后将round恢复到最初值,再将任务重新放入时间轮中。

这样做能解决时间轮刻度范围过⼤造成的空间浪费,但是却带来了另⼀个问题:时间轮每次都需要遍历任务列表,耗时增加,当时间轮刻度粒度很⼩(秒级甚⾄毫秒级),任务列表⼜特别长时,这种遍历的办法是不可接受的。

当然,对于⼤多数场景,这种⽅法还是适⽤的。有没有既节省空间,⼜节省时间的办法呢? 答案是有的,正如《Hashed and Hierarchical Timing Wheels》标题中提到的,有⼀种分层时间轮,可以解决做到既节省空间,⼜节省时间:

分层时间轮

分层时间轮是这样⼀种思想:

  1. 针对时间复杂度的问题:不做遍历计算round,凡是任务列表中的都应该是应该被执⾏的,直接全部取出来执⾏。

  2. 针对空间复杂度的问题:分层,每个时间粒度对应⼀个时间轮,多个时间轮之间进⾏级联协作。

第一点很好理解,第二点有必要举个例子来说明:
比如我有三个任务:

  1. 任务⼀每周⼆上午九点。
  2. 任务⼆每周四上午九点。
  3. 任务三每个⽉12号上午九点。

三个任务涉及到四个时间单位:⼩时、天、星期、⽉份。

拿任务三来说,任务三得到执⾏的前提是,时间刻度先得来到12号这⼀天,然后才需要关注其更细⼀级的时间单位:上午9点。

基于这个思想,我们可以设置三个时间轮:⽉轮、周轮、天轮。

  • ⽉轮的时间刻度是天。
  • 周轮的时间刻度是天。
  • 天轮的时间刻度是⼩时。

初始添加任务时,任务⼀添加到天轮上,任务⼆添加到周轮上,任务三添加到⽉轮上。

三个时间轮以各⾃的时间刻度不停流转。

当周轮移动到刻度2(星期⼆)时,取出这个刻度下的任务,丢到天轮上,天轮接管该任务,到9点执⾏。

同理,当⽉轮移动到刻度12(12号)时,取出这个刻度下的任务,丢到天轮上,天轮接管该任务,到9点执⾏。

这样就可以做到既不浪费空间,又不浪费时间。

整体的⽰意图如下所⽰:

在这里插入图片描述

round时间轮和分层时间轮的一点比较

相比于round时间轮思想,采用分层时间轮算法的优点在于:只需要多耗费极少的空间(从1个时间轮到3个时间轮),就能实现多线程在效率上的提高(一个时间轮是一个线程去行走,3个时间轮可以3个线程行走)。当然这是相对的,若提交的任务都是每隔几个小时重复执行,那显然小时时间轮比月、周、小时时间轮组合的耗费空间少,且执行时间还相同。

时间轮的应⽤

时间轮的思想应⽤范围⾮常⼴泛,各种操作系统的定时任务调度,Crontab,还有基于java的通信框架Netty中也有时间轮的实现,⼏乎所有的时间任务调度系统采⽤的都是时间轮的思想。

⾄于采⽤round型的时间轮还是采⽤分层时间轮,看实际需要吧,时间复杂度和实现复杂度的取舍。


参考链接:
https://its301.com/article/qq_34039868/105384808
https://cloud.tencent.com/developer/article/1815722

  • 1
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值