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小时
-
...更高层级以此类推
运行机制 :
-
初始放置 :新条目根据过期时间放入对应层级的桶(如2小时后过期的放入第2层)
-
时间推进 : advance() 方法定期被调用,推进当前时间【桶是静止的,只是时间的指针移动,想一想 钟表的刻度 和 分针】
-
桶级联 :当某层的当前桶时间到达,会将桶内所有条目:
-
已过期的:调用 cache.evictEntry() 移除
-
未过期的:重新计算并放入更细粒度的下层桶
-
-
最终过期 :条目逐渐从高层级"下沉"到低层级,最终在秒针层过期
核心优势
-
时间复杂度 :所有操作(添加/移除/过期)均为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
简单来说,它们是 分工协作 的关系:
-
TimerWheel (时间轮) :扮演着一个 高效的定时器 或 调度器 的角色。它不存储缓存的完整键值对,只关心一件事:管理和追踪缓存条目( Node )的 到期时间 。它的设计目标是以 O(1) 的摊销时间复杂度处理大量的定时事件。
-
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;}
}
功能分析
- 更新时间:方法首先将内部时钟 nanos 更新为传入的 currentTimeNanos。
- 处理时钟回绕:System.nanoTime() 可能回绕(从正数变为负数)。代码通过将负数和正数都加上 Long.MAX_VALUE 来处理这种情况,确保时间差的计算是正确的。这是一个巧妙的设计,可以处理大约292年的连续运行时间而不会出错。
- 分层处理:TimerWheel 是分层的,每一层代表不同的时间粒度(由 SPANS 和 SHIFT 定义)。advance 方法会从最精确的轮子(i = 0)开始遍历到最粗糙的轮子。
- 计算时间差(Ticks):对于每一层轮子,它通过位移(>>> SHIFT[i])将纳秒时间转换为该层的"滴答数"(ticks)。然后计算从上次 advance 调用到现在的滴答数差 delta。
- 触发过期:如果 delta > 0,意味着时间至少前进了一个滴答,就需要处理这期间到期的条目。于是,它调用
expire
方法。 - 提前退出:如果某一层的 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 ...}}}
}
功能分析
- 确定范围:计算需要处理的桶的范围。start 是上次处理的最后一个桶的下一个,steps 是需要处理的桶的数量(等于 delta + 1,但不能超过整个轮子的大小)。
- 遍历到期桶:循环遍历从 start 到 end 的所有桶。
- 清空桶:对于每个桶,它首先将哨兵节点的 next 和 prev 指针都指向自己,从而在O(1)时间内将整个桶的链表"清空"。
- 处理节点:然后遍历刚刚从桶中取出的链表中的每一个节点(node)。
- 重新调度或驱逐:
- 检查是否真的过期:((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];
}
功能分析
- schedule:它的逻辑很简单,调用
findBucket
找到正确的桶,然后调用 link 将节点添加到该桶的双向链表的末尾。 - findBucket:这是决定节点存放位置的关键。
- 它首先计算出节点的过期时间与当前时间的差值 duration。
- 然后,它从最精确的轮子开始查找,if (duration < SPANS[i + 1]) 这个判断是为了找到能容纳这个 duration 的最精确的轮子。
- 一旦找到合适的轮子(i),它就会计算出在该轮子中的 ticks 和桶的索引 index。
- 如果一个事件的过期时间非常遥远,超过了除最后一个轮子之外所有轮子的最大跨度,它将被放入最后一个轮子的第一个桶中,作为一个"蓄水池"。
总结
advance 方法通过一个级联的、分层的过程来管理定时事件:
- advance 启动过程,根据当前时间计算出每个时间轮需要前进的"滴答数"。
- expire 负责处理这些到期的"滴答"对应的桶,将桶中的所有事件取出。
- 对于取出的每个事件,expire 会判断它是否真的到期。如果到期,则尝试驱逐;如果尚未到期(即"级联"),则调用 schedule。
- schedule 通过 findBucket 为事件计算出新的、更精确的位置,并将其放入相应的桶中。
这个设计确保了添加、删除和处理过期事件的平摊时间复杂度都是 O(1),因为处理一个桶的成本被分摊到了该桶所代表的整个时间跨度上。
getExpirationDelay()
返回 到下一个事件触发 的 时间
getExpirationDelay
和 peekAhead
协同实现高效延迟估算:
- 分层扫描:从细到粗,优先处理最紧急事件。
- 精确与保守结合:精确计算当前层级延迟,保守估计更高层级延迟。
- 立即返回:避免不必要计算,减少 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;
}
工作流程分析
-
分层遍历(外层循环):
for (int i = 0; i < SHIFT.length; i++)
- 从最精确的时间轮(
i=0
,通常为秒级)开始,逐级遍历到最粗糙的时间轮。 - 核心思想:找到所有层级中第一个非空桶,其条目是下一个最可能过期的。
- 从最精确的时间轮(
-
桶内扫描(内层循环):
for (int j = start; j < end; j++)
- 对当前层级
i
的时间轮,从当前时间nanos
对应的桶(start
)开始扫描一圈。 ticks = (nanos >>> SHIFT[i])
:计算当前时间在当前轮子下的“滴答数”。start = (int) (ticks & spanMask)
:通过位掩码计算当前“滴答数”对应的桶索引。
- 对当前层级
-
寻找非空桶:
if (next == sentinel)
- 检查哨兵节点
sentinel
的下一个节点是否为自身,若为空则继续扫描。
- 检查哨兵节点
-
计算延迟(核心逻辑):
buckets = (j - start)
:计算目标桶与当前桶的距离。delay = (buckets << SHIFT[i]) - (nanos & spanMask)
:(buckets << SHIFT[i])
:将桶距离转换为纳秒(目标桶起始时间 - 当前桶起始时间)。(nanos & spanMask)
:当前时间在当前桶内已经过的时间。
delay = (delay > 0) ? delay : SPANS[i]
:若delay ≤ 0
(目标桶为当前桶),则保守设置为SPANS[i]
。
-
展望更高层级(调用
peekAhead
):for (int k = i + 1; k < SHIFT.length; k++)
- 检查比当前轮更粗糙的层级(
k > i
),通过peekAhead
获取预估延迟。 delay = Math.min(delay, nextDelay)
:取最小延迟,确保不遗漏任何过期事件。
- 检查比当前轮更粗糙的层级(
-
返回结果:
- 找到非空桶后立即返回
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));
}
- 定位下一个桶:
probe = (int) ((ticks + 1) & mask)
- 仅检查下一个待处理的桶(
ticks + 1
)。
- 仅检查下一个待处理的桶(
- 检查是否为空:
next = sentinel.getNextInVariableOrder()
- 若下一个桶为空,返回
Long.MAX_VALUE
。
- 若下一个桶为空,返回
- 返回预估延迟:
- 若非空,返回
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)的基本特征和行为,是键值对及其元数据的"集装箱"。
核心职责与功能:
-
数据承载:
getKey()
:获取键(Key)getValue()
:获取值(Value)
这些是 Node 作为缓存条目的基本职责
-
生命周期管理:
isAlive()
:节点是否存活(在哈希表和淘汰策略中都有效)isRetired()
:节点是否已从哈希表中移除(等待从淘汰策略中清理)isDead()
:节点是否已彻底死亡(从哈希表和淘汰策略中都已移除)retire()
,die()
:改变节点生命周期状态的方法
这些状态精确追踪了缓存条目从创建到销毁的全过程
-
权重(Weight)管理:
getWeight()
,setWeight()
:支持基于权重的淘汰策略,允许为不同条目设置不同"成本"
-
多种排序/淘汰策略支持:
- 通过实现多个接口维护多套双向链表指针:
- 访问顺序 (AccessOrder):
get/setPrevious/NextInAccessOrder()
等方法,支持LRU/LIRS等策略 - 写入顺序 (WriteOrder):
get/setPrevious/NextInWriteOrder()
等方法,支持FIFO等策略 - 可变顺序 (Variable order):
get/setPrevious/NextInVariableOrder()
和get/setVariableTime()
等方法,支持基于时间的过期策略(如TimerWheel)
- 访问顺序 (AccessOrder):
- 通过实现多个接口维护多套双向链表指针:
实现特点:
Node 是抽象类,默认方法抛出UnsupportedOperationException
,具体子类根据缓存策略动态实现功能,体现高度灵活的设计。
Sentinel<K, V>:时间轮中的"哨兵"
Sentinel
是一个静态内部类,它继承自 Node
,但重写了所有方法,使其成为一个空操作或返回默认值。每个 Sentinel
实例代表一个桶内双向链表的头部。它的 next
和 prev
指针在初始化时都指向自己,形成一个空的循环链表。当新节点加入时,会被插入到 sentinel.prev
和 sentinel
之间,极大地简化了边界条件的处理。
定义:
TimerWheel
内部的static final
类,继承自Node,作为特殊的哨兵节点(非实际缓存条目)。
核心职责与功能:
-
链表头/尾节点:
- 在TimerWheel中,每个"桶"(Bucket)是一个由Sentinel引领的双向链表
- 构造函数
Sentinel() { prev = next = this; }
创建空循环链表(当桶为空时,哨兵节点的指针指向自身)
-
简化链表操作:
- 哨兵模式优势:链表永不"空"(至少含Sentinel节点),统一插入/删除操作逻辑,无需空指针判断
- 重写
get/setPrevious/NextInVariableOrder()
方法,使其能作为链表节点操作
-
无实际数据:
- 重写数据相关方法:
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>> 接口,但将具体的遍历方向(升序或降序)和移动逻辑交给了子类实现。
核心设计与功能:
-
并发修改检测:
final long expectedNanos;
:在构造时,它会记录下当前 TimerWheel 的时间戳 nanos。hasNext()
方法会在每次调用时检查nanos != expectedNanos
。如果时间轮的时间被advance()
方法改变了,就意味着其内部结构可能已经发生了变化(节点被移除或重新调度),此时迭代器会抛出ConcurrentModificationException
,这是一种快速失败(fail-fast)机制,保证了迭代的安全性。
-
迭代器基本逻辑:
- 它实现了标准的
hasNext()
和next()
方法。hasNext()
的核心是调用computeNext()
来预先查找并缓存下一个节点到next
字段。next()
方法则返回缓存的节点并将其清空。 computeNext()
是遍历的核心,它在一个循环中不断尝试:- 在当前桶内移动(
traverse(node)
)。 - 如果当前桶遍历完,就移动到下一个桶(
goToNextBucket()
)。 - 如果当前层级的轮子遍历完,就移动到下一个轮子(
goToNextWheel()
)。 - 如果所有轮子都遍历完,则返回
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 的抽象方法来定义其升序遍历行为:
-
状态管理:
int wheelIndex
:当前遍历到哪个时间轮(0 是最精确的,比如秒级)。int steps
:在当前时间轮上已经移动了多少个桶。
-
方法实现:
isDone()
:当wheelIndex
等于wheel.length
时,表示所有层级的时间轮都已遍历完毕,迭代结束。sentinel()
:根据当前的wheelIndex
和bucketIndex()
计算结果,返回对应桶的 Sentinel 节点。traverse(Node<K, V> node)
:核心的升序移动逻辑,它返回node.getNextInVariableOrder()
,即沿着链表的next
指针方向前进。goToNextBucket()
:将steps
加一,如果还没超出当前轮的桶数,就返回新位置的哨兵节点。goToNextWheel()
:将wheelIndex
加一,如果还没超出时间轮的总层数,就将steps
重置为 0,并返回新轮子中第一个桶的哨兵节点。
-
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(模板):定义了迭代的整体算法骨架(
hasNext
、next
、computeNext
),但将易变的部分(如遍历方向、如何切换桶和轮子)抽象成方法。 - AscendingIterator(具体实现):继承 Traverser 并实现了这些抽象方法,从而定义了具体的升序遍历行为。
这种结构使得代码逻辑清晰,复用性高。如果需要一个降序迭代器(DescendingIterator),只需再创建一个 Traverser 的子类并反向实现那些抽象方法即可,而无需重写整个迭代逻辑。