当前位置: 首页 > news >正文

Caffeine TimerWheel时间轮 深度解析:O(1)复杂度增删和触发时间事件

Kafka以另一种简洁方式实现了时间轮,见:

Kafka 时间轮深度解析

TimerWheel(时间轮)是一种高效的数据结构,专门用于管理大量的定时事件。它的核心思想源于现实世界中的时钟,通过将时间划分为不同的"格子"(buckets),并将定时事件放入对应时间点的格子里,从而避免了对所有事件进行频繁排序的开销。

在 Caffeine 中,TimerWheel 的主要职责是管理缓存条目(Node)的过期事件。当一个条目被设置为在未来某个时间点过期时,它就被"安排"到时间轮上。时间轮会随着时间的推移而"转动",当指针扫过某个格子时,这个格子里的所有事件就被触发(即条目过期)。

TimerWheel 是 Caffeine 实现高效、可伸缩的过期策略的基石。通过精巧的​​分层时间轮算法​​、​​位运算​​和​​哨兵节点​​等技术,它能够在 O(1) 的摊销时间内完成定时事件的调度和处理,完美支撑了 Caffeine 的高性能特性(处理高并发和大量缓存条目的场景)。

核心数据结构与分层设计

Caffeine 的 TimerWheel 采用了一种更高级的​​分层时间轮(Hierarchical Timer Wheel)​​设计,以应对跨度极大的过期时间(从几秒到几天)。这通过几个关键的静态常量和成员变量来实现:

  • wheel: final Node<K, V>[][]:这是一个二维数组,是时间轮的核心。第一维代表不同的"轮",每个轮具有不同的时间精度(例如,秒轮、分钟轮、小时轮)。第二维是该轮的"桶"(buckets),每个桶代表一个时间片。
  • BUCKETS: static final int[]:定义了每个轮有多少个桶。 { 64, 64, 32, 4, 1 } 表示第一个轮有64个桶,第二个轮有64个,以此类推。
  • SPANS: static final long[]:定义了每个轮的每个桶所代表的时间跨度(以纳秒为单位)。这些值被设计为2的幂,以便使用位运算进行快速计算。例如,SPANS[0] 大约是1.07秒,SPANS[1] 大约是1.14分钟。
  • SHIFT: static final long[]:预先计算好的位移量,用于通过位运算(>>>)快速确定一个给定的过期时间应该落在哪个轮的哪个桶里。
  static final long[] SPANS = {ceilingPowerOfTwo(TimeUnit.SECONDS.toNanos(1)), // 1.07sceilingPowerOfTwo(TimeUnit.MINUTES.toNanos(1)), // 1.14mceilingPowerOfTwo(TimeUnit.HOURS.toNanos(1)),   // 1.22hceilingPowerOfTwo(TimeUnit.DAYS.toNanos(1)),    // 1.63dBUCKETS[3] * ceilingPowerOfTwo(TimeUnit.DAYS.toNanos(1)), // 6.5dBUCKETS[3] * ceilingPowerOfTwo(TimeUnit.DAYS.toNanos(1)), // 6.5d};static final long[] SHIFT = {Long.numberOfTrailingZeros(SPANS[0]),Long.numberOfTrailingZeros(SPANS[1]),Long.numberOfTrailingZeros(SPANS[2]),Long.numberOfTrailingZeros(SPANS[3]),Long.numberOfTrailingZeros(SPANS[4]),};

​分层逻辑​​:
当一个事件被调度时,比如它将在80分钟后过期。直接把它放在秒级的轮上效率很低(需要转很多圈)。分层设计会根据过期时长,将其放入一个更粗粒度的轮上,比如"小时轮"。当时间推进,"小时轮"转动一格时,这一格里的所有事件会被取出,并根据它们剩余的精确时间,重新调度到更细粒度的"分钟轮"或"秒轮"中。这个过程称为​​层级降级(cascading)​​。

时间轮工作过程的直观类比

可以想象成 多层级时钟 :

  • 秒针层 (第0层):64个桶,每桶1.07秒,转一圈≈70秒

  • 分针层 (第1层):64个桶,每桶1.14分钟,转一圈≈73分钟

  • 时针层 (第2层):32个桶,每桶1.22小时,转一圈≈39小时

  • ...更高层级以此类推

运行机制 :

  1. 初始放置 :新条目根据过期时间放入对应层级的桶(如2小时后过期的放入第2层)

  2. 时间推进 : advance() 方法定期被调用,推进当前时间【桶是静止的,只是时间的指针移动,想一想 钟表的刻度 和 分针

  3. 桶级联 :当某层的当前桶时间到达,会将桶内所有条目:

    1. 已过期的:调用 cache.evictEntry() 移除

    2. 未过期的:重新计算并放入更细粒度的下层桶

  4. 最终过期 :条目逐渐从高层级"下沉"到低层级,最终在秒针层过期

核心优势

  • 时间复杂度 :所有操作(添加/移除/过期)均为O(1)摊销复杂度

  • 内存效率 :通过分层结构避免了单一时间轮的巨大空间开销

  • 精度平衡 :高层级粗粒度+低层级细粒度,兼顾长短期过期需求

  • 并发安全 :通过哨兵节点和双向链表实现无锁化桶操作

构造与初始化

TimerWheel() 的构造函数非常直接:

@SuppressWarnings({"rawtypes", "unchecked"})
TimerWheel() {wheel = new Node[BUCKETS.length][];for (int i = 0; i < wheel.length; i++) {wheel[i] = new Node[BUCKETS[i]];for (int j = 0; j < wheel[i].length; j++) {wheel[i][j] = new Sentinel<>();}}
}

它根据 BUCKETS 的定义创建了二维数组。最关键的一步是,每个桶(wheel[i][j])都被初始化为一个 Sentinel 对象。Sentinel 是一个特殊的 Node,它充当每个桶内双向链表的​​哨兵节点​​。使用哨兵节点可以极大地简化链表的插入和删除操作,避免了对头节点和尾节点的特殊处理。

TimerWheel 和 cache

简单来说,它们是 分工协作 的关系:

  1. TimerWheel (时间轮) :扮演着一个 高效的定时器 或 调度器 的角色。它不存储缓存的完整键值对,只关心一件事:管理和追踪缓存条目( Node )的 到期时间 。它的设计目标是以 O(1) 的摊销时间复杂度处理大量的定时事件。

  2. Cache (缓存实现) :这是 缓存的核心 。它负责存储实际的键值对数据(在一个 ConcurrentHashMap 中),并执行所有核心的缓存操作,比如读、写以及最终的驱逐(eviction)动作。

当缓存需要清理过期条目时(通常在写入操作后触发),它会调用 timerWheel.advance() 方法, advance 方法会根据流逝的时间计算出哪些时间桶(bucket)已经过期,然后调用 expire 方法来处理这些桶。

总结一下关系 :

  • Cache 是“老板”,它持有并管理着所有数据。

  • TimerWheel 是老板雇佣的“秘书”,专门负责管理日程表(条目何时过期)。

  • 到了预定时间,秘书( TimerWheel )会把到期的日程( Node )拿出来,并告诉老板( BoundedLocalCache ):“这个条目该处理了”。

  • 老板( cache.evictEntry() )接到通知后,亲自去执行驱逐操作。

  • 如果秘书发现某个日程还没到精确时间,或者老板处理失败,秘书会把这个日程重新放回日程表( schedule(node) ),等待下次检查。

通过这种方式,Caffeine 实现了一个高效、低锁竞争的过期策略。 TimerWheel 保证了时间管理的效率,而 BoundedLocalCache 则专注于数据的存储和最终一致性。

advance 方法

时间轮本身是被动的,它需要一个外部的"动力"来推进。这个动力就是 advance 方法。

public void advance(BoundedLocalCache<K, V> cache, @Var long currentTimeNanos) {@Var long previousTimeNanos = nanos;nanos = currentTimeNanos;// If wrapping then temporarily shift the clock for a positive comparison. We assume that the// advancements never exceed a total running time of Long.MAX_VALUE nanoseconds (292 years)// so that an overflow only occurs due to using an arbitrary origin time (System.nanoTime()).if ((previousTimeNanos < 0) && (currentTimeNanos > 0)) {previousTimeNanos += Long.MAX_VALUE;currentTimeNanos += Long.MAX_VALUE;}try {for (int i = 0; i < SHIFT.length; i++) {long previousTicks = (previousTimeNanos >>> SHIFT[i]);long currentTicks = (currentTimeNanos >>> SHIFT[i]);long delta = (currentTicks - previousTicks);if (delta <= 0L) {break;}expire(cache, i, previousTicks, delta);}} catch (Throwable t) {nanos = previousTimeNanos;throw t;}
}

功能分析

  1. ​更新时间​​:方法首先将内部时钟 nanos 更新为传入的 currentTimeNanos。
  2. ​处理时钟回绕​​:System.nanoTime() 可能回绕(从正数变为负数)。代码通过将负数和正数都加上 Long.MAX_VALUE 来处理这种情况,确保时间差的计算是正确的。这是一个巧妙的设计,可以处理大约292年的连续运行时间而不会出错。
  3. ​分层处理​​:TimerWheel 是分层的,每一层代表不同的时间粒度(由 SPANS 和 SHIFT 定义)。advance 方法会从最精确的轮子(i = 0)开始遍历到最粗糙的轮子。
  4. ​计算时间差(Ticks)​​:对于每一层轮子,它通过位移(>>> SHIFT[i])将纳秒时间转换为该层的"滴答数"(ticks)。然后计算从上次 advance 调用到现在的滴答数差 delta。
  5. ​触发过期​​:如果 delta > 0,意味着时间至少前进了一个滴答,就需要处理这期间到期的条目。于是,它调用 expire 方法。
  6. ​提前退出​​:如果某一层的 delta <= 0,说明在这一层及更粗粒度的层级上,时间并未前进一个滴答,因此可以直接 break 循环,优化性能。

expire

这是 advance 调用的核心子函数,负责处理指定轮子中到期的桶。

void expire(BoundedLocalCache<K, V> cache, int index, long previousTicks, long delta) {Node<K, V>[] timerWheel = wheel[index];int mask = timerWheel.length - 1;// We assume that the delta does not overflow an integer and cause negative steps. This can// occur only if the advancement exceeds 2^61 nanoseconds (73 years).int steps = Math.min(1 + (int) delta, timerWheel.length);int start = (int) (previousTicks & mask);int end = start + steps;for (int i = start; i < end; i++) {Node<K, V> sentinel = timerWheel[i & mask];Node<K, V> prev = sentinel.getPreviousInVariableOrder();@Var Node<K, V> node = sentinel.getNextInVariableOrder();sentinel.setPreviousInVariableOrder(sentinel);sentinel.setNextInVariableOrder(sentinel);while (node != sentinel) {Node<K, V> next = node.getNextInVariableOrder();node.setPreviousInVariableOrder(null);node.setNextInVariableOrder(null);try {if (((node.getVariableTime() - nanos) > 0)|| !cache.evictEntry(node, RemovalCause.EXPIRED, nanos)) {schedule(node);}node = next;} catch (Throwable t) {// ... existing code ...}}}
}

功能分析

  1. ​确定范围​​:计算需要处理的桶的范围。start 是上次处理的最后一个桶的下一个,steps 是需要处理的桶的数量(等于 delta + 1,但不能超过整个轮子的大小)。
  2. ​遍历到期桶​​:循环遍历从 start 到 end 的所有桶。
  3. ​清空桶​​:对于每个桶,它首先将哨兵节点的 next 和 prev 指针都指向自己,从而在O(1)时间内将整个桶的链表"清空"。
  4. ​处理节点​​:然后遍历刚刚从桶中取出的链表中的每一个节点(node)。
  5. ​重新调度或驱逐​​:
    • 检查是否真的过期:((node.getVariableTime() - nanos) > 0) 检查节点的精确过期时间是否仍然在未来。如果一个事件被放在一个粗粒度的轮子里,当这个轮子的桶到期时,事件的精确时间可能还未到。在这种情况下,需要将它重新调度到更精确的轮子中。
    • 尝试驱逐:如果节点确实到期了,就调用 cache.evictEntry() 来尝试驱逐它。evictEntry 返回 false 可能意味着该条目已被用户手动删除或因其他原因(如容量限制)被驱逐。
    • 重新调度:如果节点尚未到期,或者驱逐失败(意味着它仍然"存活"),则调用 schedule 方法将其重新放入时间轮的正确位置。

schedule

schedule 负责将一个节点放入时间轮。

public void schedule(Node<K, V> node) {Node<K, V> sentinel = findBucket(node.getVariableTime());link(sentinel, node);
}Node<K, V> findBucket(@Var long time) {long duration = Math.max(0L, time - nanos);if (duration == 0L) {time = nanos;}int length = wheel.length - 1;for (int i = 0; i < length; i++) {if (duration < SPANS[i + 1]) {long ticks = (time >>> SHIFT[i]);int index = (int) (ticks & (wheel[i].length - 1));return wheel[i][index];}}return wheel[length][0];
}

功能分析

  1. ​schedule​​:它的逻辑很简单,调用 findBucket 找到正确的桶,然后调用 link 将节点添加到该桶的双向链表的末尾。
  2. ​findBucket​​:这是决定节点存放位置的关键。
    • 它首先计算出节点的过期时间与当前时间的差值 duration。
    • 然后,它从最精确的轮子开始查找,if (duration < SPANS[i + 1]) 这个判断是为了找到能容纳这个 duration 的最精确的轮子。
    • 一旦找到合适的轮子(i),它就会计算出在该轮子中的 ticks 和桶的索引 index。
    • 如果一个事件的过期时间非常遥远,超过了除最后一个轮子之外所有轮子的最大跨度,它将被放入最后一个轮子的第一个桶中,作为一个"蓄水池"。

总结

advance 方法通过一个级联的、分层的过程来管理定时事件:

  1. advance 启动过程,根据当前时间计算出每个时间轮需要前进的"滴答数"。
  2. expire 负责处理这些到期的"滴答"对应的桶,将桶中的所有事件取出。
  3. 对于取出的每个事件,expire 会判断它是否真的到期。如果到期,则尝试驱逐;如果尚未到期(即"级联"),则调用 schedule。
  4. schedule 通过 findBucket 为事件计算出新的、更精确的位置,并将其放入相应的桶中。

这个设计确保了添加、删除和处理过期事件的平摊时间复杂度都是 O(1),因为处理一个桶的成本被分摊到了该桶所代表的整个时间跨度上。

getExpirationDelay() 

返回 到下一个事件触发 的 时间

getExpirationDelaypeekAhead 协同实现高效延迟估算:

  • ​分层扫描​​:从细到粗,优先处理最紧急事件。
  • ​精确与保守结合​​:精确计算当前层级延迟,保守估计更高层级延迟。
  • ​立即返回​​:避免不必要计算,减少 CPU 空转。

此机制使 Caffeine 的过期调度器能“智能”休眠,在保证及时性的同时最小化资源消耗。

@SuppressWarnings({"IntLongMath", "Varifier"})
public long getExpirationDelay() {for (int i = 0; i < SHIFT.length; i++) {Node<K, V>[] timerWheel = wheel[i];long ticks = (nanos >>> SHIFT[i]);long spanMask = SPANS[i] - 1;int start = (int) (ticks & spanMask);int end = start + timerWheel.length;int mask = timerWheel.length - 1;for (int j = start; j < end; j++) {Node<K, V> sentinel = timerWheel[(j & mask)];Node<K, V> next = sentinel.getNextInVariableOrder();if (next == sentinel) {continue;}long buckets = (j - start);@Var long delay = (buckets << SHIFT[i]) - (nanos & spanMask);delay = (delay > 0) ? delay : SPANS[i];for (int k = i + 1; k < SHIFT.length; k++) {long nextDelay = peekAhead(k);delay = Math.min(delay, nextDelay);}return delay;}}return Long.MAX_VALUE;
}

工作流程分析

  1. ​分层遍历(外层循环)​​:for (int i = 0; i < SHIFT.length; i++)

    • 从最精确的时间轮(i=0,通常为秒级)开始,逐级遍历到最粗糙的时间轮。
    • ​核心思想​​:找到所有层级中第一个非空桶,其条目是下一个最可能过期的。
  2. ​桶内扫描(内层循环)​​:for (int j = start; j < end; j++)

    • 对当前层级 i 的时间轮,从当前时间 nanos 对应的桶(start)开始扫描一圈。
    • ticks = (nanos >>> SHIFT[i]):计算当前时间在当前轮子下的“滴答数”。
    • start = (int) (ticks & spanMask):通过位掩码计算当前“滴答数”对应的桶索引。
  3. ​寻找非空桶​​:if (next == sentinel)

    • 检查哨兵节点 sentinel 的下一个节点是否为自身,若为空则继续扫描。
  4. ​计算延迟(核心逻辑)​​:

    • buckets = (j - start):计算目标桶与当前桶的距离。
    • delay = (buckets << SHIFT[i]) - (nanos & spanMask)
      • (buckets << SHIFT[i]):将桶距离转换为纳秒(目标桶起始时间 - 当前桶起始时间)。
      • (nanos & spanMask):当前时间在当前桶内已经过的时间。
    • delay = (delay > 0) ? delay : SPANS[i]:若 delay ≤ 0(目标桶为当前桶),则保守设置为 SPANS[i]
  5. ​展望更高层级(调用 peekAhead)​​:for (int k = i + 1; k < SHIFT.length; k++)

    • 检查比当前轮更粗糙的层级(k > i),通过 peekAhead 获取预估延迟。
    • delay = Math.min(delay, nextDelay):取最小延迟,确保不遗漏任何过期事件。
  6. ​返回结果​​:

    • 找到非空桶后立即返回 delay(最近的过期时间)。
    • 若所有桶为空,返回 Long.MAX_VALUE(无待过期条目)。

peekAhead() 

long peekAhead(int index) {long ticks = (nanos >>> SHIFT[index]);Node<K, V>[] timerWheel = wheel[index];long spanMask = SPANS[index] - 1;int mask = timerWheel.length - 1;int probe = (int) ((ticks + 1) & mask);Node<K, V> sentinel = timerWheel[probe];Node<K, V> next = sentinel.getNextInVariableOrder();return (next == sentinel) ? Long.MAX_VALUE : (SPANS[index] - (nanos & spanMask));
}
  1. ​定位下一个桶​​:probe = (int) ((ticks + 1) & mask)
    • 仅检查下一个待处理的桶(ticks + 1)。
  2. ​检查是否为空​​:next = sentinel.getNextInVariableOrder()
    • 若下一个桶为空,返回 Long.MAX_VALUE
  3. ​返回预估延迟​​:
    • 若非空,返回 SPANS[index] - (nanos & spanMask)(当前桶剩余时间)。
    • ​保守估计​​:假设下一个桶的条目会在当前桶结束后立刻过期。

为什么 peekAhead 只检查下一个桶?

peekAhead 的作用是为更高层级的轮( k > i )提供一个 快速且保守的延迟估算 。它只检查下一个桶是基于以下事实:

  • 精确轮次整个跨度不超过下一层级的一个Span。

  • 时间轮的级联机制 :当时间轮通过 advance() 方法向前推进时,如果一个高层级轮的桶过期了,里面的所有节点都会被取出,并根据它们精确的过期时间重新调度( schedule )到更低、更精确的层级轮中。

  • 有序性保证 :这个级联机制保证了,对于任何一个 k 层的轮,所有比“当前时间格”更早的节点都已经被移动到了 k-1 层或更低的层级。因此,在 k 层轮中,我们唯一需要担心的、可能比 i 层( i < k )结果更早的,就是 紧邻当前时间点的下一个桶 中的节点。

时间说明

首先,最关键的一点是, TimerWheel 内部的时间戳 nanos (通常来自 System.nanoTime() ) 是一个 单调递增 的值,就像汽车的里程表一样,它只会前进,不会倒退或循环。它不是我们日常生活中“今天下午3点”和“明天下午3点”那样的循环时间。

所以,即使最细粒度的秒轮盘(wheel[0])转了一圈又一圈,底层的 nanos 时间戳已经变得非常大了。系统通过比较节点自身的过期时间 node.getVariableTime() 和当前的 timerWheel.nanos 来判断是否到期,这是一个绝对时间的比较,因此不会混淆。

桶的索引是通过 (时间戳 >>> SHIFT) & mask 计算的,这确实是一个循环操作。一个桶会被一次又一次地重复使用。

那么,如何保证一个旧的、很久以前的事件不会和一个新的、即将到期的事件混在同一个桶里呢?

答案就在 advance 方法中。每次调用 advance(currentTimeNanos) 时,它会计算出从上次推进到的时间 previousTimeNanos 到 currentTimeNanos 之间经过了多少“滴答”(ticks)。然后,它会 遍历并处理这期间所有过期的桶 。

  • 处理过程 :对于每个过期的桶,它会清空里面的所有节点。

    • 如果节点真的到期了( node.getVariableTime() <= currentTimeNanos ),就执行驱逐操作。

    • 如果节点还没到期(这通常发生在从高层轮盘级联下来时),就调用 schedule 方法,根据它最新的过期时间把它重新放入更精确的、更底层的轮盘中。

这个“清空并级联”的机制保证了当一个桶即将被“循环使用”时,它里面所有陈旧的事件都已经被处理完毕了。因此,桶里永远只会有在当前时间周期内有效的事件。

理解了以上两点, peekAhead 的逻辑就清晰了。 peekAhead 的目的是为了快速、保守地估算一个上层(更粗粒度)时间轮中的下一个事件还有多久。

为什么它只检查“下一个”桶( (ticks + 1) & mask )就足够了呢?

因为 advance() 已经保证了,任何位于 当前桶或之前桶 的事件,如果它们的精确过期时间已经到来,那么它们早就被级联到下层轮盘了。换句话说,一个粗粒度轮盘(比如“小时”轮)的当前桶里,不可能存在一个“本分钟”就该过期的事件,它一定已经被移动到“分钟”或“秒”轮里了。

因此,当 getExpirationDelay 在检查完“秒”轮后,想知道“分钟”轮里会不会有更早的事件时,它只需要 peekAhead 一下“分钟”轮的下一个桶就够了。这提供了一个安全的、最差情况下的延迟估计。 getExpirationDelay 会取所有轮盘估算出的延迟中的最小值,从而确保找到全局的最小延迟,让调度器可以精确地休眠。

总结 :

  • 单调时间 : nanos 保证了时间的绝对前进性。

  • 循环索引 :桶通过 & mask 被循环使用。

  • advance() :是时间轮的核心驱动力,通过“级联”机制,确保了在桶被复用前,旧事件已被妥善处理,从而保证了时间的正确性。

  • peekAhead() :是一个基于 advance() 所建立的安全性的优化,它通过快速查看下一个可能的时间槽来提供一个保守的延迟估计,避免了全面搜索的高昂代价。

Node<K, V>:缓存条目的核心抽象

Node 是一个抽象类,定义了 Caffeine 缓存中条目(Entry)的基本特征和行为,是键值对及其元数据的"集装箱"。

​核心职责与功能​​:

  1. ​数据承载​​:

    • getKey():获取键(Key)
    • getValue():获取值(Value)
      这些是 Node 作为缓存条目的基本职责
  2. ​生命周期管理​​:

    • isAlive():节点是否存活(在哈希表和淘汰策略中都有效)
    • isRetired():节点是否已从哈希表中移除(等待从淘汰策略中清理)
    • isDead():节点是否已彻底死亡(从哈希表和淘汰策略中都已移除)
    • retire(), die():改变节点生命周期状态的方法
      这些状态精确追踪了缓存条目从创建到销毁的全过程
  3. ​权重(Weight)管理​​:

    • getWeight(), setWeight():支持基于权重的淘汰策略,允许为不同条目设置不同"成本"
  4. ​多种排序/淘汰策略支持​​:

    • 通过实现多个接口维护多套双向链表指针:
      • ​访问顺序 (AccessOrder)​​:get/setPrevious/NextInAccessOrder()等方法,支持LRU/LIRS等策略
      • ​写入顺序 (WriteOrder)​​:get/setPrevious/NextInWriteOrder()等方法,支持FIFO等策略
      • ​可变顺序 (Variable order)​​:get/setPrevious/NextInVariableOrder()get/setVariableTime()等方法,支持基于时间的过期策略(如TimerWheel)

​实现特点​​:
Node 是抽象类,默认方法抛出UnsupportedOperationException,具体子类根据缓存策略动态实现功能,体现高度灵活的设计。


Sentinel<K, V>:时间轮中的"哨兵"

Sentinel 是一个静态内部类,它继承自 Node,但重写了所有方法,使其成为一个空操作或返回默认值。每个 Sentinel 实例代表一个桶内双向链表的头部。它的 nextprev 指针在初始化时都指向自己,形成一个空的循环链表。当新节点加入时,会被插入到 sentinel.prevsentinel 之间,极大地简化了边界条件的处理。

​定义​​:
TimerWheel内部的static final类,继承自Node,作为特殊的哨兵节点(非实际缓存条目)。

​核心职责与功能​​:

  1. ​链表头/尾节点​​:

    • 在TimerWheel中,每个"桶"(Bucket)是一个由Sentinel引领的双向链表
    • 构造函数Sentinel() { prev = next = this; }创建空循环链表(当桶为空时,哨兵节点的指针指向自身)
  2. ​简化链表操作​​:

    • 哨兵模式优势:链表永不"空"(至少含Sentinel节点),统一插入/删除操作逻辑,无需空指针判断
    • 重写get/setPrevious/NextInVariableOrder()方法,使其能作为链表节点操作
  3. ​无实际数据​​:

    • 重写数据相关方法:
      • getKey()/getValue()返回null
      • getKeyReference()/getValueReference()抛出异常
      • isAlive()/isRetired()/isDead()固定返回false
        明确表明其仅作为结构性工具

Node 与 Sentinel 的关系

角色比喻功能设计意义
Node"士兵"存储实际缓存数据和元信息,参与淘汰策略实现缓存核心功能
Sentinel"哨兵"不存储数据,仅作为链表头节点简化操作(如TimerWheel中的桶管理)提升链表操作效率,降低边界条件处理


在TimerWheel中,wheel数组的每个槽位存放一个Sentinel实例。当Node因过期时间被调度时,会插入对应桶的Sentinel管理的链表中。

Traverser:抽象的遍历器基类

TimerWheel 实现了 Iterable<Node<K, V>> 接口,提供了两种遍历方式:

  • AscendingIterator​:从最快要过期的节点(在最细粒度的轮的当前桶中)开始,向最晚过期的节点遍历。它大致按照过期时间的升序进行。
  • DescendingIterator​:与前者相反,从最晚要过期的节点(在最粗粒度的轮中)开始,向最早要过期的节点遍历。

这两个迭代器都设计为​​快速失败(fail-fast)​​的,如果在遍历过程中时间轮被 advance 修改,会抛出 ConcurrentModificationException

Traverser 是一个抽象内部类,它定义了在 TimerWheel 的多层级结构中进行遍历所需要的通用逻辑和基本框架。它实现了 Iterator<Node<K, V>> 接口,但将具体的遍历方向(升序或降序)和移动逻辑交给了子类实现。

核心设计与功能:

  1. ​并发修改检测​​:

    • final long expectedNanos;:在构造时,它会记录下当前 TimerWheel 的时间戳 nanos。
    • hasNext() 方法会在每次调用时检查 nanos != expectedNanos。如果时间轮的时间被 advance() 方法改变了,就意味着其内部结构可能已经发生了变化(节点被移除或重新调度),此时迭代器会抛出 ConcurrentModificationException,这是一种快速失败(fail-fast)机制,保证了迭代的安全性。
  2. ​迭代器基本逻辑​​:

    • 它实现了标准的 hasNext()next() 方法。hasNext() 的核心是调用 computeNext() 来预先查找并缓存下一个节点到 next 字段。next() 方法则返回缓存的节点并将其清空。
    • computeNext() 是遍历的核心,它在一个循环中不断尝试:
      1. 在当前桶内移动( traverse(node))。
      2. 如果当前桶遍历完,就移动到下一个桶(goToNextBucket())。
      3. 如果当前层级的轮子遍历完,就移动到下一个轮子(goToNextWheel())。
      4. 如果所有轮子都遍历完,则返回 null,表示迭代结束。
    @Nullable Node<K, V> computeNext() {@Var var node = (current == null) ? sentinel() : current;for (;;) {node = traverse(node);if (node != sentinel()) {return node;} else if ((node = goToNextBucket()) != null) {continue;} else if ((node = goToNextWheel()) != null) {continue;}return null;}}

​抽象方法(留给子类实现)​​:Traverser 将具体的移动策略定义为一系列抽象方法,这使得它可以同时支持升序和降序两种遍历方式:

  • abstract boolean isDone();:判断整个迭代是否已经完成。
  • abstract Node<K, V> sentinel();:获取当前位置的哨兵节点,用于判断一个桶是否遍历完毕。
  • abstract Node<K, V> traverse(Node<K, V> node);:定义了在桶的链表中如何移动。升序迭代器会调用 node.getNextInVariableOrder(),而降序则会调用 getPreviousInVariableOrder()
  • abstract @Nullable Node<K, V> goToNextBucket();:移动到下一个桶。
  • abstract @Nullable Node<K, V> goToNextWheel();:移动到下一个时间轮。

AscendingIterator:具体的升序迭代器

AscendingIterator 是 Traverser 的一个具体实现,它提供了按时间​​升序​​(从最近到最远)遍历 TimerWheel 中所有节点的能力。TimerWheel 的 iterator() 方法返回的就是这个类的实例。

AscendingIterator 通过实现 Traverser 的抽象方法来定义其升序遍历行为:

  1. ​状态管理​​:

    • int wheelIndex:当前遍历到哪个时间轮(0 是最精确的,比如秒级)。
    • int steps:在当前时间轮上已经移动了多少个桶。
  2. ​方法实现​​:

    • isDone():当 wheelIndex 等于 wheel.length 时,表示所有层级的时间轮都已遍历完毕,迭代结束。
    • sentinel():根据当前的 wheelIndexbucketIndex() 计算结果,返回对应桶的 Sentinel 节点。
    • traverse(Node<K, V> node):核心的升序移动逻辑,它返回 node.getNextInVariableOrder(),即沿着链表的 next 指针方向前进。
    • goToNextBucket():将 steps 加一,如果还没超出当前轮的桶数,就返回新位置的哨兵节点。
    • goToNextWheel():将 wheelIndex 加一,如果还没超出时间轮的总层数,就将 steps 重置为 0,并返回新轮子中第一个桶的哨兵节点。
  3. ​bucketIndex() 的计算逻辑​​:
    这是 AscendingIterator 最复杂的逻辑之一。它需要计算出在当前时间 nanos 下,应该从哪个桶开始遍历。

    int ticks = (int) (nanos >>> SHIFT[wheelIndex]);
    int bucketMask = wheel[wheelIndex].length - 1;
    int bucketOffset = (ticks & bucketMask) + 1;
    return (bucketOffset + steps) & bucketMask;
    • 它首先计算出当前时间 nanos 在当前层级 wheelIndex 对应的“滴答数” ticks
    • bucketOffset 计算出当前时间所在的桶的下一个位置作为遍历的起点。
    • steps 记录了从起点开始已经走过的步数。
    • 通过 (bucketOffset + steps) & bucketMask 确保索引在桶数组的范围内循环。

int bucketOffset = (ticks & bucketMask) + 1;

  • ticks & bucketMask : 这行代码通过位运算计算出当前 ticks 对应的桶索引。例如,如果当前轮有 64 个桶,这个操作会返回 ticks % 64 的结果,范围是 0 到 63。

  • +1 : 这是 AscendingIterator (升序迭代器)的关键所在。迭代器需要遍历的是 未来 的事件。因此,它不从当前时间所在的桶开始,而是从 下一个桶 开始。 +1 操作就实现了这个“跳到下一个桶”的逻辑。

  • 目的 :计算出升序遍历的 起始桶索引 。

return (bucketOffset + steps) & bucketMask;

  • steps : 这是 AscendingIterator 的一个成员变量,记录了在当前时间轮中已经向前遍历了多少个桶。初始值为 0。

  • bucketOffset + steps : 将起始桶索引与已经走过的步数相加,得到当前应该访问的桶的逻辑位置。

  • & bucketMask : 再次使用位掩码进行取模运算。这确保了即使 bucketOffset + steps 的结果超过了桶的总数,也能正确地“回绕”(wrap around)到时间轮的开头,实现循环遍历。

  • 目的 :计算出本次迭代需要检查的 最终桶索引 。

DescendingIterator 的目标是 降序 遍历,即从最遥远的未来向当前时间回溯。它的 bucketOffset 计算没有 + 1 ,并且它是 - steps 。

- 它的起点就是“当前桶”。
- 它通过 - steps 从“当前桶”开始 向前 (即向时间戳更小的方向)回溯,实现降序遍历。

总结

Traverser 和 AscendingIterator 的设计体现了​​模板方法模式​​:

  • ​Traverser(模板)​​:定义了迭代的整体算法骨架(hasNextnextcomputeNext),但将易变的部分(如遍历方向、如何切换桶和轮子)抽象成方法。
  • ​AscendingIterator(具体实现)​​:继承 Traverser 并实现了这些抽象方法,从而定义了具体的升序遍历行为。

这种结构使得代码逻辑清晰,复用性高。如果需要一个降序迭代器(DescendingIterator),只需再创建一个 Traverser 的子类并反向实现那些抽象方法即可,而无需重写整个迭代逻辑。

http://www.xdnf.cn/news/1409311.html

相关文章:

  • 李宏毅NLP-13-Vocoder
  • html添加水印
  • 2025年- H103-Lc211--3090. 每个字符最多出现两次的最长子字符串(双指针)--Java版
  • leetcode 268 丢失的数字
  • AG32 Nano开发板的烧录与调试工具(二)
  • 【开题答辩全过程】以 基于vue+springboot的校园疫情管理系统的设计与实现为例,包含答辩的问题和答案
  • 异步编程与面向对象知识总结
  • 家庭全光组网高温故障深度分析与散热重构全记录
  • 【图论】Graph.jl 核心函数
  • 一种使用 Java / Kotlin 编写检测BT种子的磁力链接是否有可用 peers 的程序
  • 扩展:如何设计与实现一个微服务架构下的跨服务异常处理适配器?
  • linux修改权限命令chmod
  • sunset: twilight靶场
  • 利用ms-swift微调和百炼平台微调大模型
  • FTP - 学习/实践
  • 【学习笔记】LLM Interview(Agent相关)
  • (附源码)基于Vue的教师档案管理系统的设计与实现
  • 安装Android Studio
  • centos 7 安装docker、docker-compose教程
  • SketchUp Pro 2024 Mac 3D建模 草图设计大师
  • Redis八股小记
  • 【了解下TJ、TC、TB、TT、TA、qJA、qJC、qJB、YJB、YJT】
  • Asible——将文件部署到受管主机和管理复杂的Play和Playbook
  • [linux仓库]解剖Linux内核:文件描述符(fd)的‘前世今生’与内核数据结构探秘
  • 编写一个用scala写的spark程序从本地读取数据,写到本地
  • 【ArcGIS微课1000例】0150:如何根据地名获取经纬度坐标
  • openssl使用SM2进行数据加密和数据解密
  • 科普:requirements.txt 和 environment.yml
  • Labview使用modbus或S7与PLC通信
  • Machine Learning HW3 report:图像分类(Hongyi Lee)