后端开发四种层式结构

个人博客:https://www.yangjicong.top/

  1. B树 / B+树:关系型数据库核心存储结构(mysql、mogodb)
  2. 时间轮:海量定时任务检测(linux、skynet、kafka)
  3. 跳表:高并发有序存储(lucune、redis、rocksdb)
  4. LSM-Tree:更高性能以及更高空间利用率的数据存储结构(rocksdb)

一、B树和B+树

MySQL数据库的索引的数据结构主要是Hash表或B+树.

先引入问题, 数据库索引为什么使用树结构存储?
因为树的查询效率高, 而且可以保持有序. 二叉树的时间复杂度是O(logN), 查找和比较次数都是最小的, 但是并没有使用二叉树作为索引的数据结构.

没有使用二叉树的原因是因为磁盘IO, 数据库引擎是存储在磁盘上的, 当数据量比较大的时候, 索引的大小可能有几个G甚至更多. 当我们利用索引查询的时候, 不可能将整个索引全部加载到内存中去, 只能逐一加载每一个磁盘页, 这里的磁盘页对应的索引树的节点.

B树

在计算机科学中,B树是一种自平衡的树能够保持数据有序。这种数据结构能够让查找数据顺序访问、插入数据及删除的动作,都在对数量级的时间复杂度内完成。B树,其实是一颗特殊的二叉查找树(binary search tree),可以拥有多于2个子节点。与自平衡二叉查找树不同,B树为系统大块数据的读写操作做了优化。B树减少定位记录时所经历的中间过程,从而加快存取速度,其实B树主要解决的就是数据IO的问题。B树这种数据结构可以用来描述外部存储。这种数据结构常被应用在数据库和文件系统的实现上。

B树特性

这里的B树,也就是英文中的B-Tree,一个 m 阶的B树满足以下条件:

  1. `每个结点至多拥有m棵子树;

  2. `根结点至少拥有两颗子树(存在子树的情况下),根结点至少有一个关键字;

  3. `除了根结点以外,其余每个分支结点至少拥有 m/2 棵子树;

  4. `所有的叶结点都在同一层上,B树的叶子结点可以看成是一种外部节点,不包含任何信息;

  5. `有 k 棵子树的分支结点则存在 k-1 个关键码,关键码按照递增次序进行排列;

  6. `关键字数量需要满足ceil(m/2)-1 <= n <= m-1;

典型的B树(4阶)如下图所示:

为什么要B树

磁盘中有两个机械运动的部分,分别是盘片旋转和磁臂移动。盘片旋转就是我们市面上所提到的多少转每分钟,而磁盘移动则是在盘片旋转到指定位置以后,移动磁臂后开始进行数据的读写。那么这就存在一个定位到磁盘中的块的过程,而定位是磁盘的存取中花费时间比较大的一块,毕竟机械运动花费的时候要远远大于电子运动的时间。当大规模数据存储到磁盘中的时候,显然定位是一个非常花费时间的过程,但是我们可以通过B树进行优化,提高磁盘读取时定位的效率。

为什么B类树可以进行优化呢?我们可以根据B类树的特点,构造一个多阶的B类树,然后在尽量多的在结点上存储相关的信息,保证层数尽量的少,在B树中可以检查多个子结点,由于在一棵树中检查任意一个结点都需要一次磁盘访问,所以B树避免了大量的磁盘访问;而且B类树是平衡树,每个结点到叶子结点的高度都是相同,这也保证了每个查询是稳定的,查询的时间复杂度是O(log2N)。

总的来说就是利用平衡树的优势,保证了查询的稳定性和加快了查询的速度。

B树的操作

既然是树,那么必不可少的操作就是插入和删除,这也是B树和其它数据结构不同的地方,当然了,还有必不可少的搜索,分享一个对B树的操作进行可视化的网址,它是由usfca提供的。

假定对高度为h的m阶B树进行操作。

插入

通过搜索找到对应的结点进行插入,那么根据即将插入的结点的数量又分为下面几种情况。

  • 如果该结点的关键字个数没有到达到m-1个,那么直接插入即可;

  • 如果该结点的关键字个数已经到达了m-1个,那么根据B树的性质显然无法满足,需要将其进行分裂。分裂的规则是该结点分成两半,将中间的关键字进行提升,加入到父亲结点中,但是这又可能存在父亲结点也满员的情况,则不得不向上进行回溯,甚至是要对根结点进行分裂,那么整棵树都加了一层。

例子如下
将元素7插入下图中的B树

步骤一:自顶向下查找元素7应该在的位置,即在6和8之间

步骤二:三阶B树中的节点最多有两个元素,把6 7 8里面的中间元素上移(中间元素上移是插入操作的关键)

步骤三:上移之后,上一层节点元素也超载了,5 7 9中间元素上移,现在根节点变为了 7 15

步骤四:要对B树进行调整,使其满足B树的特性,最终如下图

下面是往B树中依次插入
6 10 4 14 5 11 15 3 2 12 1 7 8 8 6 3 6 21 5 15 15 6 32 23 45 65 7 8 6 5 4

删除

B树的删除相对于插入来说较为复杂。

删除分为两种情况:

  1. 删除的是非终端结点
  2. 删除的是终端结点

当我们删除的是非终端结点时,将用该结点的直接前驱或直接后继代替其位置,因此对于非终端结点的删除最后还是转化到了对终端结点的删除。(关键字左子树的最右下结点为直接前驱,右子树的最左下结点为直接后继)
因此我们主要还是讨论对终端结点的删除。

对于终端结点的删除我们又分为了几种情况:

  1. 删除后结点的关键字个数未低于下限
    直接删除即可

  2. 删除后结点的关键字个数低于下限
    该种情况下还需要分为以下三种情况
    (1). 右兄弟有足够的关键字

有足够关键字的意思就是借给自己一个关键字后还能够保证B树的性质,即关键字个数大于[m/2⌉-1]个。
此时右兄弟的最左关键字上浮到父亲结点,而原来的父亲元素则下沉到被删除关键字的结点中。如下图所示

(2). 右兄弟没有足够的关键字,左兄弟有足够的关键字
此时左兄弟的最右关键字上浮到父亲结点,而原来的父亲元素则下沉到被删除关键字的结点中。如下图所示

(3) 左右兄弟都没有足够关键字
此时由于左右兄弟的结点都只有[m/2]-1个,因此,当关键字删除后,该结点与任意兄弟结点的关键字个数的总和必然不大于一个结点所能容纳的上限。
举个例子,对于一个5阶B树来说,每个结点最多有4个关键字,最少要有2个关键字,当该B树删除结点时符合本条情况时,左右兄弟必然都只有2个关键字,而被删除关键字的结点由于删除后自身结点不足,则只剩下1个关键字,因此该结点删除后与任一兄弟的合并都只有3个关键字,完全符合B树性质。
合并过程中,合并的两个结点的父关键字则会下沉,与这个两个合并结点一起合并。具体如下图所示

在合并过程中,双亲结点中的关键字会减1.若其双亲结点是根节点,且个数少至0,则直接删除根结点,合并后的新结点成为根。
若双亲结点不是根结点,且关键字数量少于[m/2]-1个,则又要重复之前的步骤进行调整,直至符合B树所有条件。

B+树

我们的B树可以拥有非常高效率的查找速度,那么为什么我们还会需要B+树这个数据结构呢?

事实上,B树的查找效率虽然非常优秀,但是它也有一个自身的缺陷。我们都知道,数据库中的数据都是按照记录存放的,每条记录都是由多个数据项组成。因此我们的每条数据记录通常也不会太短,甚至可能非常之长。同样,如果将这些数据记录按照我们的B树进行组织,那么每个结点将存储的内容就是记录本身,B树是将记录本身作为单位存放的。比如我们用一个5阶B树组织在校学生的数据记录,那么我们每个结点都至少存储2条学生记录。这样乍一看好像没什么问题,但是在检索过程中,由于每个结点所占据的存储很大,我们实际的检索速度很难像前面展示的那些B树一样实现快速的检索(前面的树中每个结点的数据记录都是关键字本身,非常小)。

因此我们引入了B+树,B+树的特殊之处在于它将我们的记录的内容放在了叶子结点上,其他分支节点和终端结点只存放关键字。同时所有的结点的关键字都会再次出现在该关键字对应的子结点上即所有的关键字都会出现在终端结点上,这样保证了每个叶子结点上的数据记录都能够有一个关键字于其对应。在这样的调整下我们每个结点只需要存放记录对应关键字,由此相较于B树,在同样大小的结点约束下,我们的B+树的每个结点可以存放更多的关键字,从而大幅降低我们的树高,提升检索速度。

且在B树的基础上将从m个关键字对应m+1个分支变成了m个关键字对应m个分支,即B+树的结点最大关键字数与B+数的阶相同.

B+树的基本性质

一棵m阶的B+树需要满足下列条件:

  1. 每个分支结点最多有m棵树和m个关键字
  2. 根结点至少两棵子树,其他每个分支结点至少有[m/2]棵子树
  3. 结点的子树个数与关键字个数相等
  4. 每个关键字都应该出现在其对应子结点中,且每个结点都按照从小到大的顺序排列
  5. 所有终端结点包含全部关键字及指向相应记录的指针。同时终端结点将关键字从小到大顺序排列,并且相终端结点按大小顺序相互链接起来。
  6. 同样是是绝对平衡的
    其树形(4阶B+树)如下图所示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GAK9qcuJ-1683615013626)(https://wx3.sinaimg.cn/large/007FyU7Tgy1g1931ail52j30gm09rn2i.jpg)]
B+树的优势
B+树的层级更少:相较于B树B+每个非叶子节点存储的关键字数更多,树的层级更少所以查询数据更快;
B+树查询速度更稳定:B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定;
B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。
B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描。

操作

其操作和B树的操作是类似的,不过需要注意的是,在增加值的时候,如果存在满员的情况,将选择结点中的值作为新的索引,还有在删除值的时候,索引中的关键字并不会删除,也不会存在父亲结点的关键字下沉的情况,因为那只是索引。

B树和B+树的区别

这都是由于B+树和B树具有不同的存储结构所造成的区别,以一个m阶树为例。

  1. 关键字的数量不同;B+树中分支结点有m个关键字,其叶子结点也有m个,其关键字只是起到了一个索引的作用,虽然B树也有m个子结点,但是其只拥有m-1个关键字。

  2. 存储的位置不同;B+树中的数据都存储在叶子结点上,也就是其所有叶子结点的数据组合起来就是完整的数据,但是B树的数据存储在每一个结点中,并不仅仅存储在叶子结点上。

  3. 分支结点的构造不同;B+树的分支结点存储着关键字信息和儿子的指针(这里的指针指的是磁盘块的偏移量),也就是说内部结点仅仅包含着索引信息。

  4. 查询不同;B树在找到具体的数值以后就结束,而B+树则需要通过索引找到叶子结点中的数据才结束,也就是说B+树的搜索过程中走了一条从根结点到叶子结点的路径,其高度是相同的,相对来说更加的稳定;

  5. 区间访问:B+树的叶子结点会按照顺序建立起链状指针,可以进行区间访问;

B+树的优势

B+树的层级更少:相较于B树B+每个非叶子节点存储的关键字数更多,树的层级更少所以查询数据更快;
B+树查询速度更稳定:B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定;
B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。
B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描。
打个比方:B+树就有点像我们的目录,是索引的一个形式。若一个目录中,除了每个章节的名称外还需包含每章的大致内容,那么本来一页就可以看完的目录就会变成很多也,这并不方便我们从中去找到我们所需要的内容。

B树的优势

B树相对于B+树的优点是,如果经常访问的数据离根节点很近,而B树的非叶子节点本身存有关键字其数据的地址,所以这种数据检索的时候会要比B+树快。

`但是不可否认,B树依旧是一种优秀的算法

两者的细节对比

- m阶B树 m阶B+树
类比 二叉查找树的进化→m叉查找树 分块查找的进化→多级分块查找
关键字和分叉 n个关键字对应n+1个分叉 n个关键字对应n个分叉
结点包含的信息 所有结点都包含记录本身 分支结点只放关键字,只有终端结点会存放指向记录的指针
查找方式 不支持顺序查找。且查找速度不稳定,可能停留在任何一层 支持顺序查找,且查找速度稳定,每次查找都会到达最下层
平衡 绝对平衡 绝对平衡
结点最少关键字数 ⌈m/2⌉-1 ⌈m/2⌉

B树与B+树在实际代码中的应用

在我们的MySql数据库中我们经常能够看见数据库引擎这个词,而数据库引擎的选择就决定了我们数据库记录的组织和查找方式。我们最常使用的就是我们的MyISAM和InnoDB两个数据库引擎。事实上,这两种数据库引擎所用的都是B+树但是又有所不同,属于B+树的变体,这个要另外去理解。

而我们的B树呢则是在MongoDB中被使用到了


原文链接:https://blog.csdn.net/qq_33905217/article/details/121827393

二、时间轮

从定时任务说起

自然界中定时任务无处不在,太阳每天东升西落,候鸟的迁徙,树木的年轮,人们每天按时上班,每个月按时发工资、交房租,四季轮换,潮涨潮落,等等,从某种意义上说,都可以认为是定时任务。
大概很少有人想过,这些“定时”是怎样做到的。当然,计算机领域的同学们可能对此比较熟悉,毕竟工作中的定时任务也是无处不在的:每天凌晨更新一波数据库,每天9点发一波邮件,每隔10秒钟抢一次火车票。。。
  至于怎么实现的?很简单啊,操作系统的crontab,spring框架的quartz,实在不行Java自带的ScheduledThreadPool都可以很方便的做到定时任务的管理调度。
  当你熟练的敲下“* * 9 * * ?”等着神奇的事情发生时,你是否想过背后的“玄机”?

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

初识时间轮

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

绝对时间和相对时间

定时任务一般有两种:
  1. 约定一段时间后执行。
  2. 约定某个时间点执行。

聪明的你会很快发现,这两者之间可以相互转换,比如给你个任务,要求12点执行,你看了一眼时间,发现现在是9点钟,那么你可以认为这个任务三个小时候执行。
  同样的,给你个任务让你3个小时后执行,你看了一眼现在是9点钟,那么你当然可以认为这个任务12点钟执行。
  我们先来考虑一个简单的情况,你接到三个任务A、B、C(都转换成绝对时间),分别需要再3点钟,4点钟和9点钟执行,正当百思不得其解时,不经意间你瞅了一眼墙上的钟表,瞬间来了灵感,如醍醐灌顶,茅塞顿开:

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

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

  • 比如现在是下午4点钟,我把这个任务加入到时间轮,并设定当时针转到明天上午九点(该任务下次执行的时间)时执行。
  • 时间来到了第二天上午九点,时间轮也转到了9点钟的位置,发现该位置有一个生成报表的任务,拿出来执行。
  • 同时时间轮发现这是一个循环执行的任务,于是把该任务重新放回到9点钟的位置。
  • 重复步骤2和步骤3。

如果哪一天这个任务不需要再执行了,那么直接通知时间轮,找到这个任务的位置删除掉就可以了。 由上面的过程我们可以看到,时间轮至少需要提供4个功能:

  • 加入任务
  • 执行任务
  • 删除任务
  • 沿着时间刻度前进
    上面说的是同一个时刻只有一个任务需要执行的情况,更通用的情况显然是同一时刻可能需要执行多个任务,比如每天上午九点除了生成报表之外,还需要执行发送邮件的任务,需要执行创建文件的任务,还需执行数据分析的任务等等,于是你刚才可能就比较好奇的时间轮的数据结构到现在可能更加好奇了,那我们先来说说时间轮的数据结构吧。

时间轮的数据结构

首先,时钟可以用数组或者循环链表表示,这个每个时钟的刻度就是一个槽,槽用来存放该刻度需要执行的任务,如果有多个任务需要执行呢?每个槽里面放一个链表就可以了,就像下面图中这样:

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

时间刻度不够用怎么办?

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

增大时间轮的刻度

一天24个小时,一周168个小时,为了解决上面的问题,我可以把时间轮的刻度(槽)从12个增加到168个,比如现在是星期二上午10点钟,那么下周一上午九点就是时间轮的第9个刻度,这周三上午九点就是时间轮的第57个刻度,示意图如下:

仔细思考一下,会发现这中方式存在几个缺陷:

  • 时间刻度太多会导致时间轮走到的多数刻度没有任务执行,比如一个月就2个任务,我得移动720次,其中718次是无用功。
  • 时间刻度太多会导致存储空间变大,利用率变低,比如一个月就2个任务,我得需要大小是720的数组,如果我的执行时间的粒度精确到秒,那就更恐怖了。
    于是乎,聪明的你脑袋一转,想到另一个办法:

round 的时间轮算法

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

  • 任务一每周二上午九点。
  • 任务二每周四上午九点。
  • 任务三每个月12号上午九点。

比如现在是9月11号星期二上午10点,时间轮转一圈是24小时,
任务一下次执行(下周二上午九点),需要时间轮转过6圈后,到第7圈的第9个刻度开始执行。
任务二下次执行第3圈的第9个刻度,任务三是第2圈的第9个刻度。

为了处理上面提到的问题,我们可以在每个定时任务中增设一个 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 字段,只要在最后一级遍历到的任务队列,必然是可提交执行的,进而避免了空轮询的问题,提高了轮询的效率。每个时间轮的遍历由不同的轮询线程实现,虽然引入的线程并发,但是线程数仅仅跟时间轮的级数有关,并不会随着任务数量的增加的增加。

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

  • 针对时间复杂度的问题:不做遍历计算round,凡是任务列表中的都应该是应该被执行的,直接全部取出来执行。
  • 针对空间复杂度的问题:分层,每个时间粒度对应一个时间轮,多个时间轮之间进行级联协作。

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

  • 任务一每周二上午九点。
  • 任务二每周四上午九点。
  • 任务三每个月12号上午九点。

三个任务涉及到四个时间单位:小时、天、星期、月份。
拿任务三来说,任务三得到执行的前提是,时间刻度先得来到12号这一天,然后才需要关注其更细一级的时间单位:上午9点。
基于这个思想,我们可以设置三个时间轮:月轮、周轮、天轮。

  • 月轮的时间刻度是天。
  • 周轮的时间刻度是天。
  • 天轮的时间刻度是小时。

初始添加任务时,任务一添加到天轮上,任务二添加到周轮上,任务三添加到月轮上。
三个时间轮以各自的时间刻度不停流转。
当周轮移动到刻度2(星期二)时,取出这个刻度下的任务,丢到天轮上,天轮接管该任务,到9点执行。
同理,当月轮移动到刻度12(12号)时,取出这个刻度下的任务,丢到天轮上,天轮接管该任务,到9点执行。
这样就可以做到既不浪费空间,有不浪费时间。

时间轮算法的进一步优化

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

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

性能分析

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

定义:

  • n - 任务数量
  • k - 多线程轮询的线程数
  • 常数 M - 全时段时间轮刻度数量(空间单位数)
  • 常数 L - 单 round 时间轮刻度数量(空间单位数)
  • li​ - 第 i 层时间轮刻度数量(空间单位数)
  • T - 存在任务队列的空间单位数

多级时间轮算法的简单实现

package timeWheel;  
  
import java.util.Date;  
import java.util.concurrent.DelayQueue;  
import java.util.concurrent.Delayed;  
import java.util.concurrent.TimeUnit;  
  
public class MultiLevelTimeWheel {
     
  
    // 时间轮大小,每一格代表1秒  
    private final int WHEEL_SIZE = 60;  
  
    // 时间轮的每一格,用来存储定时任务  
    private TimeSlot[] timeSlots;  
  
    // 当前指针指向的时间槽  
    private int currentSlotIndex = 0;  
  
    // 下一级时间轮  
    private MultiLevelTimeWheel nextLevelWheel;  
  
    
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值