【Netty】时间轮

https://www.modb.pro/db/131799

https://cloud.tencent.com/developer/article/2185938

https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/Netty%20%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86%E5%89%96%E6%9E%90%E4%B8%8E%20RPC%20%E5%AE%9E%E8%B7%B5-%E5%AE%8C/21%20%20%E6%8A%80%E5%B7%A7%E7%AF%87%EF%BC%9A%E5%BB%B6%E8%BF%9F%E4%BB%BB%E5%8A%A1%E5%A4%84%E7%90%86%E7%A5%9E%E5%99%A8%E4%B9%8B%E6%97%B6%E9%97%B4%E8%BD%AE%20HashedWheelTimer.md

 

时间轮是一种高效利用线程资源进行批量化调度的一种调度模型。把大批量的调度任务全部绑定到同一个调度器上,使用这一个调度器来进行所有任务的管理、触发、以及运行。

时间轮的模型能够高效管理各种任务: 延时任务、周期任务、通知任务。

 

基本知识

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

应用场景

应用:在kafka、zookeeper、Netty、Dubbo等高性能组件中都有时间轮使用的方式。

适用于对时效性不高的,可快速执行的,大量这样的“小”任务,能够做到高性能,低消耗。非准实时

应用场景大致有: 心跳检测(客户端探活)、会话、请求是否超时、消息延迟推送、业务场景超时取消(订单、退款单等)

Netty:
比如Netty动辄管理100w+的连接,每一个连接都会有很多超时任务。比如发送超时、心跳检测间隔等,如果每一个定时任务都启动一个Timer,不仅低效,而且会消耗大量的资源。
在Netty中的一个典型应用场景是判断某个连接是否idle,如果idle(如客户端由于网络原因导致到服务器的心跳无法送达),则服务器会主动断开连接,释放资源。
得益于Netty NIO的优异性能,基于Netty开发的服务器可以维持大量的长连接,单台8核16G的云主机可以同时维持几十万长连接,及时掐掉不活跃的连接就显得尤其重要。

 

基本模型构成

时间轮是以时间作为刻度, 组成的一个环形队列,这个环形队列采用数组来实现,数组的每个元素称为槽 Bucket,

时间轮是由多个时间格组成,每个时间格代表当前时间轮的基本时间跨度(tickDuration),其中时间轮的时间格的个数是固定的。它以恒定的速率顺时针转动。没转动一步就指向下一个槽,每次转动称之为一个tick。

每个槽位可以放一个定时任务列表,叫HashedWheelBucket;可以是一个双向链表,其中可以设置一个 sentinel 哨兵节点, 作为添加任务和删除任务的起始节点。

槽位链表的每一项表示一个定时任务项(HashedWhellTimeout),其中封装了真正的定时任务TimerTask。

计算槽位: ts = (cs + (ti / si)) % N   // 现在指针指向槽cs,我们要添加一个定时时间为ti的定时器,则该定时器将被插入槽ts(timerslot)对应的链表中

  

时间刻度不够用怎么办?

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

  • 增大时间轮的刻度
  • 列表中的任务中添加round属性
  • 分层时间轮

方案:增大时间轮的刻度

一天24个小时,一周168个小时,为了解决上面的问题,我可以把时间轮的刻度(槽)从12个增加到168个,

仔细思考一下,会发现这中方式存在几个缺陷:
1. 时间刻度太多会导致时间轮走到的多数刻度没有任务执行,比如一个月就2个任务,我得移动720次,其中718次是无用功。
2. 时间刻度太多会导致存储空间变大,利用率变低,比如一个月就2个任务,我得需要大小是720的数组,如果我的执行时间的粒度精确到秒,那就更恐怖了。

 

方案:列表中的任务中添加round属性

时间轮每移动到一个刻度时,遍历任务列表,把round值-1,然后取出所有round=0的任务执行。

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

 

方案:分层时间轮

分层时间轮是这样一种思想:
1. 针对时间复杂度的问题:不做遍历计算round,凡是任务列表中的都应该是应该被执行的,直接全部取出来执行。

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

第一点很好理解,第二点有必要举个例子来说明。
比如我有三个任务:任务一每周二上午九点。任务二每周四上午九点。任务三每个月12号上午九点。三个任务涉及到四个时间单位:小时、天、星期、月份。
拿任务三来说,任务三得到执行的前提是,时间刻度先得来到12号这一天,然后才需要关注其更细一级的时间单位:上午9点。
基于这个思想,我们可以设置三个时间轮:月轮、周轮、天轮。月轮的时间刻度是天。周轮的时间刻度是天。天轮的时间刻度是小时。
初始添加任务时:任务一添加到周轮上,任务二添加到周轮上任务三添加到月轮上。

三个时间轮以各自的时间刻度不停流转。
当周轮移动到刻度2(星期二)时,取出这个刻度下的任务1,丢到天轮上,天轮接管该任务,到9点执行。
当周轮移动到刻度4(周四)时,取出这个刻度下的任务2,丢到天轮上,天轮接管该任务,到9点执行。
当月轮移动到刻度12(12号)时,取出这个刻度下的任务,丢到天轮上,天轮接管该任务,到9点执行。


Netty时间轮定义

 
使用:
1、创建时间轮:
HashedWheelTimer timer = new HashedWheelTimer(1, TimeUnit.SECONDS, 8);  // 支持多种构造器
2、提交任务

timer.newTimeout(timeout -> {log.info("running task 2..."); }, 5, TimeUnit.SECONDS);   // 内部会调用HashedWheelTimer::start()  触发workerThread.start()启动进行时间轮转动

3、停止时间轮

 timer.stop();   == 会返回未被执行的timeout

 

HashedWheelTimer 内部关键实现

private final Queue<HashedWheelTimeout> timeouts = PlatformDependent.newMpscQueue();    -- newMpscQueue最终来自jcTools的高性能队列

private final Queue<HashedWheelTimeout> cancelledTimeouts = PlatformDependent.newMpscQueue();

 

newTimeOut(): 
1、启动时间轮的转动处理
2、提交新定时任务提交到上述timeouts队列,等待worker线程的扫描处理
 
 
内部Worker类:扫描timeouts任务加入时间轮(transferTimeoutsToBuckets计算具体位置)
class Worker implements Runnable
1、transferTimeoutsToBuckets  从timeouts(mpsc)队列poll定时任务,计算在时间轮具体位置并插入
2、waitForNextTick(): 负责时间轮的转动
 
内部HashedWheelTimeout类:一个待提交到时间轮的定时任务
class HashedWheelTimeout implements Timeout, Runnable
1、TimerTask:待执行的任务
2、HashedWheelTimeout next;
3、HashedWheelTimeout prev;
 
内部HashedWheelBucket类:时间轮bucket

1、HashedWheelTimeout head;
2、HashedWheelTimeout tail; 

 
 
posted @   飞翔在天  阅读(340)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示