基于 golang 从零到一实现时间轮算法 (一)

前言

时间轮是用来解决海量百万级定时器(或延时)任务的最佳方案,linux 的内核定时器就是采用该数据结构实现。


应用场景

  1. 自动删除缓存中过期的 Key:缓存中设置了 TTL 的 kv,通过把该 key 对应的 TTL 以及回调方法注册到 timewheel,到期直接删除
  2. 延时任务,将任务注册到 timewheel,过期自动触发执行
  3. 在 TcpServer 中,用来管理海量 Tcp 连接的超时定时器,如 zinx 的定时器 实现

简单时间轮

如上图,一个普通的时间轮,类似于时钟表盘,指针(pointer)每隔一段时间前进一格(interval,tick 一次),一圈代表一个周期(circle),定时任务以链表(双向)方式置放在表盘的刻度处,当指针前进到当前位置时,遍历任务链表,执行相应的任务。

基本模型构成

  1. tickMs(基本时间跨度):时间轮由多个时间格组成,每个时间格代表当前时间轮的基本时间跨度(tickMs)。
  2. wheelSize(时间单位个数):时间轮的时间格个数是固定的,可用(wheelSize)来表示,那么整个时间轮的总体时间跨度(interval)可以通过公式tickMs × wheelSize计算得出。
  3. currentTime(当前所处时间):时间轮还有一个表盘指针(currentTime),用来表示时间轮当前所处的时间,currentTime是 tickMs 的整数倍。currentTime 可以将整个时间轮划分为到期部分和未到期部分,currentTime当前指向的时间格也属于到期部分,表示刚好到期,需要处理此时间格所对应的 TimerTaskList 的所有任务。

从开发角度而言,实现一个时间轮:

  1. 时间轮是一个由固定长度 length 的数组(本例子中就是 [1,12])构造而成的环形队列
  2. 时间轮的长度决定了延时任务的刻度,假设上面的刻度为 1s(即时间轮 1s 前进一格),那么该时间轮只能表达延时任务在 1s 至 12s内的任务;时间轮的长度也即时间轮的周期(12s)
  3. 注册任务按照 当前刻度 + 延时时长 % 时间轮周期 计算得出,假设当前指针在 5s的位置,此时添加一个延时周期为 5s 的任务,那么该任务需要注册到刻度为 10s 的格子对应的任务链表中
  4. 数组中的每个元素都指向一个双向链表,用于存储对应的延时任务
  5. 时间轮的插入复杂度是 O(1),删除指定节点的复杂度是O(n),因为需要遍历双向链表以查找到要删除的节点
  6. 当时间轮指针转动到对应的单元格时,顺序执行双向链表中存储的任务基础时间轮的缺点是无法注册延时超过时间轮周期的任务,如何解决呢?

基础时间轮的缺点是无法注册延时超过时间轮周期的任务,如何解决呢?

解决方法 1:加 circle 计数器

此方法相当于给双向链表中存储的任务加多一个 "圈数" 的维度

• 假设时间轮有8个刻度

• 计时器值 = 17

• 走两圈时间轮+多走1个刻度

• 在"0"这个桶中设置计时器

• 与计时器一同保留#圈数

• 在到期处理时,如果#圈数 > 0,则重新插入计时器

注意,此策略中,时间轮指针每前进一格,需要把此格对应的任务链表中,所有的任务的 circle 计数器都减 1,如果 circle==0,那么说明,任务时间已到期,执行该任务

解决方法 2:层级时间轮

这是一种典型的 "空间换时间" 的思路,按照时间轮周期的倍数进行合理分层,有两个优点:

  1. 避免任务堆积在某个 slot 上
  2. 支持任意长时间的延时任务注册

    复用之前的案例,第一层的时间轮 tickMs=1ms, wheelSize=20, interval=20ms。第二层的时间轮的 tickMs 为第一层时间轮的 interval,即为 20ms。每一层时间轮的 wheelSize 是固定的,都是 20,那么第二层的时间轮的总体时间跨度 interval 为 400ms。以此类推,这个 400ms 也是第三层的 tickMs 的大小,第三层的时间轮的总体时间跨度为 8000ms。

关于层级时间轮,可以参考普通的时间。多层时间轮的概念也非常清晰,将时间轮分为多个,每两个轮之间是进位的关系,例如最普遍的秒,分,时.即:

  • 秒级时间轮上,设有60个槽, 每两个槽之间的时间为1s.
  • 分钟级时间轮上,设有60s个槽,每两个槽之间的时间为1min
  • 小时级时间轮上,设有24个槽,每两个槽之间的时间为1h

    这样,秒级每走60个槽,时间过去一分钟,秒级时间轮归零,分级时间轮走一个槽; 分级时间轮每走过60个槽,时间过去一小时,分级时间轮归零,小时级时间轮走一个槽.

通过三级时间轮,只需要遍历60+60+60 =180个槽,就可以成为一个精度为1s, 周期为60x60x60 = 216000s的定时调度器.


参考

https://zhuanlan.zhihu.com/p/658079556,

https://pandaychen.github.io/2022/05/28/A-TIMEWHEEL-ANALYSIS/

https://juejin.cn/post/7083795682313633822

属于在以上博客基础上记录的个人笔记。

相关推荐
菜鸟求带飞_3 分钟前
算法打卡:第十一章 图论part01
java·数据结构·算法
浅念同学4 分钟前
算法.图论-建图/拓扑排序及其拓展
算法·图论
骆晨学长19 分钟前
基于springboot的智慧社区微信小程序
java·数据库·spring boot·后端·微信小程序·小程序
是小Y啦21 分钟前
leetcode 106.从中序与后续遍历序列构造二叉树
数据结构·算法·leetcode
AskHarries24 分钟前
利用反射实现动态代理
java·后端·reflect
liuyang-neu31 分钟前
力扣 42.接雨水
java·算法·leetcode
y_dd38 分钟前
【machine learning-12-多元线性回归】
算法·机器学习·线性回归
m0_6312704038 分钟前
标准c语言(一)
c语言·开发语言·算法
万河归海42838 分钟前
C语言——二分法搜索数组中特定元素并返回下标
c语言·开发语言·数据结构·经验分享·笔记·算法·visualstudio
小周的C语言学习笔记43 分钟前
鹏哥C语言36-37---循环/分支语句练习(折半查找算法)
c语言·算法·visual studio