计时器Timing Wheel 时间轮算法

news2024/12/25 8:55:55

文章目录

  • 1. 前言
  • 2. 什么是时间轮算法?
    • 2.1 单层时间轮
    • 2.2 多层时间轮
      • 2.2.1 增加轮次的概念
      • 2.2.2 多层次时间轮
    • 2.3 小结
  • 3. 实现案例
    • 3.1 Kafka中的时间轮
      • 3.1.1 任务的添加
      • 3.1.2 时间轮的推进
        • SystemTimer
      • 3.1.3 小结

1. 前言

计时器对于故障恢复、基于速率的流量控制、调度算法、控制网络中的数据包生命周期至关重要。

而一般计时器的实现维护成本比较高,比如JDK自带的 Timer、DelayQueue对于任务的进出其时间复杂度为O(logN)。

对于要求高性能且需要保证高频繁大量操作任务的优先级框架,比如Kafka、Netty等框架,重排序的时间复杂度O(logN)是不能满足其要求的。而基于一种时间轮的算法可以实现将这种重排序的时间复杂度降为O(1)。

2. 什么是时间轮算法?

算法来自于生活,我们日常看时间使用手表,一个表盘就可以无限的去循环每一天,通过同样的一个表盘不同的指针来指向不同维度的时间(时分秒),日常中如果我们由大量任务需要进行提醒,可以进行备忘与时钟里的时间进行指定按时提醒。

同样的时间轮算法数据结构其实抽象于手表时钟,时间轮是用环形数组抽象表盘,数组里面的每一个元素就是一个bucket(刻度之间的间隔,也可以指代时间的精度)。bucket内部用双向链表存这待执行的任务,此时添加和删除的链表操作时间复杂度都是o(1)。

2.1 单层时间轮

在这里插入图片描述

从图中可以看到此时指针指向的是第一个bucket,一共有八个bucket0~7,假设bucket的时间单位为 1 秒,现在要加入一个延时 6秒的任务,计算方式就是 6 % 8 = 6,即放在下标为6 的那个bucket中,具体的操作只要直接添加到bucket双向链表的tail尾部就行了。

2.2 多层时间轮

比如当我们加一个延迟9s后执行的任务,此时超出表盘的范围时,如何解决呢?同样借鉴于表盘中循环以及多个指针代表不同时间维度的思想,时间轮算法有两种解决方案。

2.2.1 增加轮次的概念

延迟9s任务存放的bucket 下标 = 9%8 = 1,轮数记为 9/8 = 1。
意思就是当循环1轮后,指针指向下表为1的bucket就会触发这个任务。Netty 中的 HashedWheelTimer 使用的就是这种方式。

2.2.2 多层次时间轮

在这里插入图片描述
这种概念,就和我们手表里的时分秒不同指针代表不同维度时间概念一样了,只不过这里是分层的设计。
实现的方式如同手表里的指针转动,当秒针走一圈,分针走一格,分针走一圈,时针走一格。
这里三层的时间轮,一共是38=24格bucket, 最多可以延迟88*8=512秒。

多层时间轮,任务存放的位置还会随着时间进行降层变动,比如一个延迟65秒的任务,刚才是放在第三层,时间过了1s后,此时只需要64s就会执行,那么这个任务就会被降层到第二层,随着时间的不断的进行,这个任务最终会降层到第一层等待执行。

为什么要进行降层操作呢? 这是为了保证时间精度的一致性,Kakfa内部用的就是多层次时间轮算法。

2.3 小结

时间轮是一种实现延迟功能(定时器)的高效调度模型算法。其设计思想类似于手表时钟的设计,主要的数据结构为数组+链表,多层时间轮有两种实现方案,一种是轮次时间轮,一种是多层时间轮方案。

3. 实现案例

3.1 Kafka中的时间轮

Kafka的时间轮(TimingWheel)是一个存储定时任务的环形队列,底层采用数组实现,数组中的每个元素可以存放一个定时任务链表(TimerTaskList),或者称之为任务槽。TimerTaskList是一个环形的双向链表,链表中的每一项表示的均是定时任务(TimerTaskEntry),其中封装了真正的定时任务(TimerTask)。

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

3.1.1 任务的添加

// TimerTaskEntry的就是包装了任务,并且记录任务的执行时间 = 延时+当前时间
def add(timerTaskEntry: TimerTaskEntry): Boolean = {
	val expiration = timerTaskEntry.expirationMs
	if (timerTaskEntry.cancelled) {
	  // Cancelled
	  false
	} else if (expiration < currentTime + tickMs) { // 如果到期
	  // Already expired
	  false
	} else if (expiration < currentTime + interval) { // 如果还在本层
	  // Put in its own bucket
	  val virtualId = expiration / tickMs
	  val bucket = buckets((virtualId % wheelSize.toLong).toInt) // 计算bucket
	  bucket.add(timerTaskEntry) // 添加到bucket中的双向链表中

	  // Set the bucket expiration time
	  if (bucket.setExpiration(virtualId * tickMs)) { // 更新bucket过期时间
		// The bucket needs to be enqueued because it was an expired bucket
		// We only need to enqueue the bucket when its expiration time has changed, i.e. the wheel has advanced
		// and the previous buckets gets reused; further calls to set the expiration within the same wheel cycle
		// will pass in the same value and hence return false, thus the bucket with the same expiration will not
		// be enqueued multiple times.
		queue.offer(bucket) // 将bucket加入delayQueue
	  }
	  true
	} else {
	  // Out of the interval. Put it into the parent timer
	  if (overflowWheel == null) addOverflowWheel()
	  overflowWheel.add(timerTaskEntry)
	}
}

从上面的 add 方法我们知道每次对比都是根据expiration < currentTime + interval 来进行对比的,那currentTime 如何进行推进的呢?

3.1.2 时间轮的推进

Netty 中是通过固定的时间间隔扫描,时候未到就等待来进行时间轮的推动。
而 Kafka 就利用了空间换时间的思想,通过 DelayQueue,来保存每个槽,通过每个槽的过期时间排序。这样拥有最早需要执行任务的槽会有优先获取。如果时候未到,那么 delayQueue.poll 就会阻塞着,这样就不会有空推进的情况发送。

SystemTimer

我们先看下SystemTimer构造器如下:

@threadsafe
class SystemTimer(executorName: String,
                  tickMs: Long = 1,
                  wheelSize: Int = 20,
                  startMs: Long = Time.SYSTEM.hiResClockMs) extends Timer {

  // timeout timer
  private[this] val taskExecutor = Executors.newFixedThreadPool(1,
    (runnable: Runnable) => KafkaThread.nonDaemon("executor-" + executorName, runnable))

  private[this] val delayQueue = new DelayQueue[TimerTaskList]()
  private[this] val taskCounter = new AtomicInteger(0)
  private[this] val timingWheel = new TimingWheel(
    tickMs = tickMs,
    wheelSize = wheelSize,
    startMs = startMs,
    taskCounter = taskCounter,
    delayQueue
  )

其中SystemTimer.advanceClock即为推进的方法

在这里插入图片描述

3.1.3 小结

Kafka 用了多层次时间轮来实现,并且是按需创建时间轮,采用任务的绝对时间来判断延期,并且对于每个bucket槽(槽内存放的也是任务的双向链表)都会维护一个过期时间,利用 DelayQueue 来对每个槽的过期时间排序,来进行时间的推进,防止空推进的存在。
每次推进都会更新 currentTime 为当前时间戳,当然做了点微调使得 currentTime 是 tickMs 的整数倍。并且每次推进都会把能降级的任务重新插入降级。
可以看到这里的 DelayQueue 的元素是每个槽,而不是任务,因此数量就少很多了,这应该是权衡了对于槽操作的延时队列的时间复杂度与空推进的影响。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/58655.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[yolov5] yolo的数据标签格式

yolov5 的标签格式 参考链接&#xff1a; https://github.com/ultralytics/yolov5/issues/9816 翻译内容 你好!。感谢您询问YOLOv5&#x1f680;数据集格式。用于分割的XY坐标与用于长方体中心的标准坐标相同。 为了正确训练&#xff0c;您的数据必须为YOLOv5格式。有关数…

数据结构与算法的基本概念

前言 技术学得再多&#xff0c;再好还不如将基础学扎实。欠下的迟早是要还的。与其在以后后悔当初不好好学&#xff0c;还不如在大学期间将该学的知识学透。《数据结构与算法》确实不好学&#xff0c;但大学四年下来&#xff0c;还怕啃不下一本书&#xff1f;&#xff1f;正是基…

自动驾驶技术

高精地图&#xff08;HD Maps&#xff09;&#xff1a;支持其他模块 定位&#xff08;Localization&#xff09;&#xff1a;讨论汽车如何确定他所处的位置&#xff0c;汽车利用激光和雷达数据&#xff0c;将这些传感器感知内容与高分辨地图进行对比&#xff0c;这种对比使得汽…

Scanpy plot umap的color编码, Scanpy 的color map 如何设置?

Scanpy plot umap的color编码&#xff0c; Scanpy 的color map 如何设置&#xff1f;关键词palette&#xff08;调色板&#xff09; https://scanpy.readthedocs.io/en/stable/generated/scanpy.pl.umap.html?highlightpl.umap#scanpy.pl.umap https://scanpy.discourse.grou…

JavaScript -- 08. 数组介绍

文章目录数组1 数组简介1.1 创数组1.2 向数组添加元素1.3 读取数组中的元素1.4 获取数组长度2 数组的遍历2.1 使用for循环遍历2.2 for-of遍历3 数组的方法3.1 Array.isArray()3.2 at()3.3 concat()3.4 indexOf()3.5 lastIndexOf()3.6 join()3.7 slice()3.8 push()3.9 pop()3.10…

Android Jetpack-Compose相关

Android Jetpack-Compose相关 一、什么是Compose&#xff1f; Jetpack Compose 是用于构建原生 Android 界面的新工具包。它使用更少的代码、强大的工具和直观的 Kotlin API&#xff0c;可以帮助您简化并加快 Android 界面开发&#xff0c;打造生动而精彩的应用。它可让您更快…

Go学习之路-环境搭建

默认运行设备系统&#xff1a;MacOS 安装 安装包下载地址&#xff08;下面3个都可以&#xff09;&#xff1a; https://studygolang.com/dl https://golang.google.cn/dl/ https://golang.org/dl/ 我这里选择 pkg包、一键安装、默认的安装路径 /usr/local/go 环境 设置go语言…

[附源码]计算机毕业设计springboot在线图书销售系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

mmap 创建共享内存映射

所谓内存映射指的是 让一个磁盘文件与内存中的一个缓冲区相映射&#xff0c;进程访问这块内存时&#xff0c;就等同于访问文件对应映射部分&#xff0c;不必再调用 read / write 。 我们可以使用mmap函数来建立内存和文件某一部分的映射关系。 目录 一、共享内存映射的创建 /…

代码审计-3 文件包含漏洞

文章目录代码审计-文件包含漏洞文件包含漏洞种类当检测到目标存在文件包含漏洞时可以怎么利用文件包含中php可使用的协议PHPCMS V9.6.3后台文件包含漏洞后台路由分析漏洞寻找代码审计-文件包含漏洞 文件包含漏洞种类 当检测到目标存在文件包含漏洞时可以怎么利用 文件包含中php…

npm run dev和npm run serve

npm run dev和npm run serve目录概述需求&#xff1a;设计思路实现思路分析1.npm install命令2.package.json3.npm run serve参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make …

redis基础4——RDB持久化、AOF持久化全面深入解读

文章目录一、redis持久化机制1.1 持久化的背景1.2 两种持久化概念1.2.1 快照方式&#xff08;RDB&#xff09;1.2.2 文件追加方式&#xff08;AOF&#xff09;1.3 rdb持久化&#xff08;Redis Database&#xff09;1.3.1 快照原理1.3.2 触发机制1.3.2.1 手动触发1.3.2.1.1 save…

最常用的python开发工具

有哪些值得推荐的 Python 开发工具 推荐5个非常适合Python小白的开发工具&#xff1a;1、Python TutorPython Tutor是由Philip Guo开发的一个免费教育工具&#xff0c;可帮助开发者攻克编程学习中的基础障碍&#xff0c;理解每一行源代码在程序执行时在计算机中的过程。 通过…

CentOS8克隆虚拟机修改IP,错误:未知的连接 “ens160“

关于CentOS8该如何克隆与修改IP&#xff0c;并设置成静态IP的方法&#xff0c;可以参考我的另一篇文章&#xff0c;我也是一直这么做的。 CentOS8拷贝虚拟机、修改ip、主机 但是最近我再使用我的这篇文章克隆虚拟机的时候&#xff0c;竟然报错了&#xff0c;本来想删除掉克隆…

2023年天津农学院专升本专业课报名、确认缴费及准考证打印流程

天津农学院2023年高职升本科专业课考试 报名、确认缴费及准考证打印操作流程 一、报名操作流程 1. 阅读报名注意事项 请考生于2022年12月5日9点—12月10日12点登录报名系统 http://gzsb.tjau.edu.cn&#xff0c;请认真阅读报名注意事项。2.报名登录 点击“报名”菜单后进入报名…

LeetCode 374. 猜数字大小

&#x1f308;&#x1f308;&#x1f604;&#x1f604; 欢迎来到茶色岛独家岛屿&#xff0c;本期将为大家揭晓LeetCode 374. 猜数字大小&#xff0c;做好准备了么&#xff0c;那么开始吧。 &#x1f332;&#x1f332;&#x1f434;&#x1f434; 一、题目名称 LeetCode 374.…

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java高校辅导员工作管理系统82aub

对于即将毕业或者即将做课设的同学而言&#xff0c;由于经验的欠缺&#xff0c;面临的第一个难题就是选题&#xff0c;确定好题目之后便是开题报告&#xff0c;如果选题首先看自己学习那些技术&#xff0c;不同技术适合做不同的产品&#xff0c;比如自己会些简单的Java语言&…

【圣诞文】用python带你体验多重花样圣诞树

前言 大家早好、午好、晚好吖 ❤ ~ 在圣诞要来临的前夕&#xff0c;我们来出一起圣诞特辑吧 &#xff08;虽说可能有一丢丢早&#xff0c;但是等要来了在准备就来不及了呀~&#xff09; 圣诞节要素 1、圣诞袜&#xff0c;最早以前是一对红色的大袜子&#xff0c;大小不拘。 …

Spring异步任务async介绍与案例实战

关于spring异步任务 简单地说&#xff0c;用Async注释bean的方法将使其在单独的线程中执行。换句话说&#xff0c;调用者不会等待被调用方法的完成。利用spring提供的注解即可简单轻松的实现异步任务处理。 默认线程池问题 Spring 异步任务默认使用 Spring 内部线程池 Simpl…

网页自动点赞

进入网页版QQ空间 ,在开发者模式下创建脚本 var x5,y10;function autoClick(){yy5;var zandocument.getElementsByClassName(item qz_like_btn_v3);for(var i0;i<zan.length;i){if(zan[i].attributes[6].valuelike){zan[i].firstChild.click();}};window.scrollBy(x,y);}win…