sentinel滑动时间窗口算法详解
1 背景介绍
sentinel
流控规则高级选择中的流控效果有三种,快速失败
、 Warm Up
和 排队等待
,其中 快速失败
的统计利用的就是滑动窗口算法。
2 快速失败介绍
Sentinel 的 快速失败(Fast Fail) 是流控策略中的一种默认处理方式,用于在系统压力过大时,立即拒绝超出阈值的请求,以保护系统资源,避免雪崩。
2.1 快速失败简介
- 策略值:
controlBehavior = 0
- 含义:当请求速率(如 QPS)达到设定的阈值时,立即拒绝后续请求,不排队、不等待。
- 适用场景:响应时间敏感、系统资源紧张的业务场景。
2.2 示例配置(JSON)
{"resource": "activity_draw","limitApp": "default","grade": 1,"count": 100,"strategy": 0,"controlBehavior": 0,"clusterMode": false
}
说明:
- 对资源
activity_draw
进行 QPS 控制(grade=1
); - 阈值为 100;
- 一旦超过 100 QPS,就立即拒绝超出的请求;
- 不做排队或缓冲处理。
2.3 快速失败的特点
特性 | 描述 |
---|---|
⏱️ 实时判断 | 每次请求时立即判断是否超限 |
🚫 拒绝请求 | 超过阈值即抛出 BlockException |
✅ 实现简单 | 不涉及队列、等待等复杂逻辑 |
⚠️ 用户体验 | 请求压力大时,部分请求会被快速拒绝 |
2.4 使用场景
- 秒杀接口 / 抽奖接口:超过阈值直接失败,避免资源争抢耗尽;
- 非关键服务:可以被快速降级、容错;
- RT 敏感服务:不允许延迟或排队的业务场景。
2.5 总结
快速失败是 Sentinel 中最轻量、最直接的限流控制方式:触发即拒绝,保护系统优先级最高。
如果你有更复杂的场景(如希望缓冲排队),可以考虑:
controlBehavior = 1
:预热模式controlBehavior = 2
:排队等待模式
是否需要我对比这三种控制行为的特点和适用场景?
3 固定窗口算法
如果需要实现接口限流,在对于精确度要求不高,并且请求分布较为平均的场景下,常规的计数器法即可满足要求:
- 在一个固定的时间窗口(比如1分钟)内允许最多 N 个请求(例如 100 个);
- 记录第一个请求的时间,维护一个计数器;
- 如果在时间窗口内计数器超过阈值,则拒绝请求;
- 如果时间窗口过了,重置时间戳和计数器。
3.1 代码示例
package com.scheme.sentinel;public class FixedWindowRateLimiter {/*** 限流阈值*/protected int sampleCount;/*** 时间窗口 ms*/protected long intervalInMs;/*** 请求次数计数器*/private int counter = 0;/*** 当前窗口开始时间*/private long windowStart;public FixedWindowRateLimiter(int sampleCount, long intervalInMs) {this.sampleCount = sampleCount;this.intervalInMs = intervalInMs;this.windowStart = System.currentTimeMillis();}/*** 尝试请求一次,如果允许返回true,否则返回false*/public synchronized boolean tryRequest() {long now = System.currentTimeMillis();if (now - windowStart >= intervalInMs) {// 超出当前时间窗口,重置计数器和窗口时间counter = 1;windowStart = now;return true;}if (counter < sampleCount) {counter++;return true;} else {// 超过限制,拒绝请求return false;}}public static void main(String[] args) throws InterruptedException {// 10s内最多3个请求FixedWindowRateLimiter fixedWindowRateLimiter = new FixedWindowRateLimiter(3, 10000);for (int i = 1; i <= 10; i++) {boolean allowed = fixedWindowRateLimiter.tryRequest();System.out.println("请求 " + i + ": " + (allowed ? "通过" : "被拒绝"));Thread.sleep(1000); // 每秒一个请求}}
}
请求 1: 通过
请求 2: 通过
请求 3: 通过
请求 4: 被拒绝
请求 5: 被拒绝
请求 6: 被拒绝
请求 7: 被拒绝
请求 8: 被拒绝
请求 9: 被拒绝
请求 10: 被拒绝
你这段代码实现的是一种 固定窗口限流算法(Fixed Window Rate Limiting),原理简单、易实现,但存在明显的缺点,尤其在高并发场景下容易出现“突刺效应”(突发请求集中通过),下面是它的简单总结和主要 弊端分析:
3.2 主要弊端
弊端 | 说明 |
---|---|
🕳️ 窗口边界问题(突刺效应) | 在两个时间窗口交界处,可能短时间内通过大量请求,导致 实际 QPS 超过阈值。例如:前一个窗口的最后 1ms 接 3 个请求,后一个窗口的第 1ms 又接 3 个请求 —— 实际 2ms 内来了 6 个请求。 |
📉 精度不高 | 统计单位是一个完整窗口(如 10 秒),无法反映窗口内的分布趋势,比如是否集中、是否均匀。 |
❎ 不适用于平滑限流 | 固定窗口不具备滑动窗口的平滑特性,导致限流不够灵敏或过于粗暴。 |
⚠️ 不支持突发容忍 | 无法灵活配置如“突发 + 平稳速率”这样的组合限流策略。 |
示例场景下的问题复现
你的配置是:
new FixedWindowRateLimiter(3, 10000); // 10秒内最多3次请求
如果用户在:
- 第 9.9 秒请求 3 次 ✅
- 第 10.1 秒再请求 3 次 ✅
实际只有 0.2 秒间隔,就通过了 6 次请求,超过了设定的平均速率目标(3次/10s)。
固定窗口算法适合对实时性要求不高、实现简单的场景,但在高并发环境中,容易因窗口边界问题导致 流量突刺与限流不准确,建议替代为滑动窗口或令牌桶算法。
4 滑动窗口算法
4.1 代码示例
package com.scheme.sentinel;import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedList;public class SlidingWindowLimiterWithDeque {/*** 限流阈值*/protected int sampleCount;/*** 时间窗口 ms*/protected long intervalInMs;/*** 每个请求的时间*/private final Deque<Long> requestTimestamps = new ArrayDeque<>();public SlidingWindowLimiterWithDeque(int sampleCount, long intervalInMs) {this.sampleCount = sampleCount;this.intervalInMs = intervalInMs;}/*** 尝试请求,返回是否允许*/public boolean tryRequest() {long now = System.currentTimeMillis();synchronized (requestTimestamps) {// 清除窗口外的时间戳while (!requestTimestamps.isEmpty() && now - requestTimestamps.peek() >= intervalInMs) {requestTimestamps.pollFirst();}if(requestTimestamps.size() < sampleCount) {requestTimestamps.offerLast(now);return true;} else {return false;}}}/*** 测试逻辑*/public static void main(String[] args) throws InterruptedException {SlidingWindowLimiterWithDeque limiter = new SlidingWindowLimiterWithDeque(5, 10000);for (int i = 1; i <= 10; i++) {boolean allowed = limiter.tryRequest();System.out.println("请求 " + i + ": " + (allowed ? "通过" : "被拒绝"));Thread.sleep(1500); // 每 1.5 秒一个请求}}
}
请求 1: 通过
请求 2: 通过
请求 3: 通过
请求 4: 通过
请求 5: 通过
请求 6: 被拒绝
请求 7: 被拒绝
请求 8: 通过
请求 9: 通过
请求 10: 通过
4.2 代码分析
这段 SlidingWindowLimiterWithDeque
实现了一个经典的 滑动时间窗口限流算法(Sliding Log Window),用于限制在任意时间窗口内通过的请求数不超过指定阈值。下面从结构、流程、优缺点几个方面做个简明分析:
4.2.1 核心逻辑(滑动窗口思想)
该实现的目标是:在最近 intervalInMs
毫秒内,允许最多 sampleCount
个请求。
4.2.2 关键流程解析:
while (!requestTimestamps.isEmpty() && now - requestTimestamps.peek() >= intervalInMs) {requestTimestamps.pollFirst();
}
-
滑动窗口核心在于这一段:
- 每来一个请求,先判断窗口左边界以外的请求是否已过期(
now - peek >= windowSize
),如果是就剔除; - 然后检查队列中剩下的请求数量(即当前滑动窗口内的请求数)是否超过限流阈值。
- 每来一个请求,先判断窗口左边界以外的请求是否已过期(
if(requestTimestamps.size() < sampleCount) {requestTimestamps.offerLast(now); // 记录当前请求时间return true;
}
- 如果窗口内请求数没有超限,记录当前请求并放行。
4.2.3 滑动窗口的意义
与“固定窗口”相比:
模型 | 固定窗口 | 滑动窗口(本例) |
---|---|---|
精度 | 时间对齐,每秒等间隔 | 精确到请求时间戳,滑动式 |
抖动 | 抖动明显(边界集中触发) | 抖动极小,统计更平滑 |
内存消耗 | 低 | 中(保留每个请求时间戳) |
实现复杂度 | 简单 | 稍复杂 |
常见使用场景 | 低并发限流、日志 | 实时限流、滑动窗口统计 |
4.2.4 代码结构优势
- 使用
Deque<Long>
(双端队列),配合pollFirst
和offerLast
实现 FIFO 队列,适合记录时间戳; - 使用
synchronized (requestTimestamps)
保证线程安全; - 粒度精确到毫秒级,适合高精度限流场景;
- 易于理解和扩展(如添加统计接口、QPS 可视化等);
4.2.5 潜在问题(在高并发下)
问题类型 | 描述 |
---|---|
性能瓶颈 | 每次都需要遍历清除过期时间戳,随着请求增多,性能会下降 |
内存增长 | 短期内请求大量积压,队列增长可能引发 OOM |
全局锁阻塞 | synchronized 整体锁队列,在高并发场景下阻塞严重 |
4.2.6 优化建议(如需进阶)
目标 | 优化方案 |
---|---|
提高性能 | 改为基于时间桶的滑动窗口(如 Sentinel 的 LeapArray ) |
降低内存占用 | 使用数组或固定大小队列 + 时间桶 |
减少锁冲突 | 用 ReentrantLock 替代 synchronized ,或分段锁 |
线程安全但无锁 | 使用 ConcurrentLinkedQueue + 原子变量优化 |
5 源码分析
很好!下面我们详细讲解 Sentinel 的滑动窗口限流实现(基于 LeapArray
),这是 Sentinel 中核心的高性能统计机制,适用于 QPS/线程数限流、熔断、热点规则等。
5.1 问题背景
在传统滑动窗口中,我们通常用链表或队列保存时间戳来统计某个时间段内的请求数,但在高并发场景中,存在如下问题:
问题 | 描述 |
---|---|
查询慢 | 每次请求都需要遍历清理过期数据 |
加锁多 | 队列操作需要加锁,影响性能 |
内存多 | 保存每个请求的时间戳,量大时内存开销大 |
5.2 Sentinel 如何解决的?
Sentinel 使用了 分段滑动窗口算法,本质是:
把一个大时间窗口(比如 1 秒)划分为若干个小的时间格子(如 50ms),每个格子只记录统计数据(如 QPS),不记录请求时间戳。
这种设计就是 Sentinel 的核心组件:
LeapArray<T>
+ WindowWrap<T>
5.3 核心结构解析
LeapArray<T>
和 WindowWrap<T>
是阿里巴巴 Sentinel(一个用于服务容错限流的框架)中的核心滑动窗口实现类,用于统计滑动时间窗口内的数据,比如 QPS、响应时间、异常数等。
这两个类实现了高性能的滑动窗口限流/熔断机制,下面是它们的详细介绍:
5.3.1 WindowWrap<T>
WindowWrap<T>
是一个窗口包装器类,表示时间轴上的一个窗口格子。
作用:
- 存储一个固定时间区间内的数据(如 1s 的 QPS 统计)。
- 每个
WindowWrap<T>
持有一个T value
,该T
可以是统计结构(比如MetricBucket
,记录请求数等)。
核心属性:
public class WindowWrap<T> {private long windowStart; // 窗口的起始时间戳(单位:毫秒)private long windowLength; // 窗口的长度(单位:毫秒)private T value; // 窗口中存储的数据,如 MetricBucket
}
举例说明:
假设我们设置窗口长度为 1 秒,当前时间是 12:00:00,则这个窗口的 windowStart = 12:00:00.000
,windowLength = 1000ms
,这个窗口就统计了从 12:00:00.000
到 12:00:00.999
的数据。
5.3.2 LeapArray<T>
LeapArray<T>
是一个环形数组,用于实现滑动时间窗口统计。它维护了多个 WindowWrap<T>
,通过时间对这些窗口进行复用、重置,实现“滑动窗口”。
核心作用:
- 管理并复用固定数量的时间窗口。
- 提供方法查找、重置窗口。
- 实现高性能、高精度的滑动窗口。
核心属性:
public abstract class LeapArray<T> {protected int sampleCount; // 窗口格子的数量protected int intervalInMs; // 总窗口时间(例如 1000ms)protected long windowLengthInMs; // 每个窗口格子的时间(intervalInMs / sampleCount)protected final AtomicReferenceArray<WindowWrap<T>> array; // 环形数组,存放窗口
}
核心方法:
currentWindow(timeMillis)
:根据当前时间返回对应的窗口,如果窗口过期则重置。values()
:返回所有有效窗口中的值。isWindowDeprecated(windowStart, time)
:判断窗口是否过期。
举例说明:
如果设置:
intervalInMs = 1000ms
sampleCount = 10
- 则每个小窗口为
100ms
在这 1 秒的滑动窗口中,LeapArray
会创建 10 个 WindowWrap
,每个统计 100ms 内的数据。通过当前时间戳与窗口起始时间计算“时间索引”,找到当前应该落在哪个窗口中。
5.3.3 使用示意图:
时间轴(1秒区间,sampleCount=10):
|----|----|----|----|----|----|----|----|----|----|
0ms 100ms 200ms ... 1000ms每个格子是一个 WindowWrap<T>
这些格子由 LeapArray<T> 管理,形成滑动窗口
随着时间推移,LeapArray 会不断移动滑动窗口,淘汰旧的窗口,统计新的时间片数据,从而实现流量控制、熔断、限速等操作。
5.3.4 应用场景
- 限流:统计滑动窗口内的请求总量,判断是否超限。
- 熔断:统计某段时间内异常率是否超阈值。
- 降级:根据 RT(响应时间)滑动平均判断是否触发降级。
5.3.5 总结
类名 | 作用 |
---|---|
WindowWrap<T> | 表示一个窗口时间片,记录某段时间的数据 |
LeapArray<T> | 管理多个 WindowWrap ,形成滑动窗口模型 |
这些类实现了 Sentinel 高性能滑动窗口统计的基础,是其流控/熔断功能的核心支撑。
如需源码分析或具体使用代码示例,也可以继续问我。
5.4 工作机制
以 QPS 为例,假设统计窗口 1 秒,分成 10 个格子,每个格子 100ms:
5.4.1 请求到来:
- 当前时间
now
,根据now % intervalInMs
定位到一个格子 index; - 如果这个格子过期了(与当前时间不在同一“轮回”),则重置这个格子;
- 然后将该格子里的 QPS +1;
5.4.2 获取统计值:
- 遍历所有格子;
- 只统计当前时间范围内的格子(即没过期的);
- 求和得到滑动窗口内的总请求数或其他指标;
5.5 示例图解
总时间窗口 1000ms,格子数量 10:
|----|----|----|----|----|----|----|----|----|----|
0 100 200 300 400 500 600 700 800 900 1000
如果现在时间为 920ms,当前格子为第 9 个,QPS 更新第 9 个格子。
当统计 QPS 时,会跳过 0~800ms 的格子(已经过期)。
5.6 Sentinel 的优势
特性 | 优势 |
---|---|
数组结构 | 几乎无锁(通过原子引用数组 + CAS) |
定位快 | 时间戳直接定位格子,无需遍历 |
内存少 | 只记录每个格子的汇总数 |
高并发 | 线程安全设计,适用于高 QPS 场景 |
5.7 总结一句话
Sentinel 的滑动窗口限流器基于高性能的
LeapArray
,通过将整个统计窗口划分为多个时间格子,并用数组加原子操作实现高效的实时统计,是面向高并发系统设计的利器。
5.8 源码体现
以下是对Spring Cloud Alibaba Sentinel滑动窗口算法的源码分析(基于核心实现逻辑,结合2023.0.3版本相关类及方法):
5.8.1 核心类与数据结构
5.8.1.1 LeapArray<T>
(环形数组)
- 作用:存储时间窗口的环形数组,实现滑动窗口的核心逻辑。
- 关键属性 :
protected int windowLengthInMs; // 单个样本窗口时长(毫秒) protected int sampleCount; // 总样本窗口数(如秒级窗口默认2个) protected int intervalInMs; // 总统计周期(如秒级为1000ms) protected AtomicReferenceArray<WindowWrap<T>> array; // 存储窗口的环形数组
- 构造方法:通过
sampleCount
和intervalInMs
计算窗口长度:public LeapArray(int sampleCount, int intervalInMs) {this.windowLengthInMs = intervalInMs / sampleCount;this.array = new AtomicReferenceArray<>(sampleCount); }
5.8.1.2 WindowWrap<T>
(窗口包装类)
- 作用:封装单个时间窗口的元数据及统计值。
- 关键属性 :
private long windowStart; // 窗口起始时间戳 private long windowLengthInMs; // 窗口时长 private T value; // 统计值(如MetricBucket)
5.8.1.3 MetricBucket
(指标桶)
- 作用:存储单个窗口内的统计指标(如通过数、异常数):
private final LongAdder[] counters; // 使用LongAdder高并发计数 public void addPass(int count) {counters[MetricEvent.PASS.ordinal()].add(count); }
5.8.2 核心方法流程
5.8.2.1 获取当前窗口:LeapArray.currentWindow()
-
入口:
ArrayMetric.addPass()
→data.currentWindow()
。 -
实现逻辑 :
public WindowWrap<T> currentWindow(long timeMillis) {int idx = calculateTimeIdx(timeMillis); // 计算数组索引long windowStart = calculateWindowStart(timeMillis); // 窗口起始时间while (true) {WindowWrap<T> old = array.get(idx);if (old == null) { // 1. 窗口未初始化WindowWrap<T> window = new WindowWrap<>(...);if (CAS设置成功) return window;} else if (windowStart == old.windowStart()) { // 2. 命中当前窗口return old;} else if (windowStart > old.windowStart()) { // 3. 窗口过期,重置if (获取锁) return resetWindowTo(old, windowStart);} else { // 4. 时间回退(理论上不可能)return new WindowWrap(...);}} }
-
计算索引:
calculateTimeIdx
基于时间戳与窗口长度的模运算:private int calculateTimeIdx(long timeMillis) {// 根据时间戳找到对应的映射窗口return (int)((timeMillis / windowLengthInMs) % array.length()); }
-
计算窗口起始时间:
calculateWindowStart
基于时间戳与窗口长度的模运算:protected long calculateWindowStart(/*@Valid*/ long timeMillis) {return timeMillis - timeMillis % windowLengthInMs;}
5.8.2.2 统计指标更新:ArrayMetric.addPass()
- 调用链:
StatisticNode.addPassRequest()
→ArrayMetric.addPass()
:public void addPass(int count) {WindowWrap<MetricBucket> wrap = data.currentWindow(); // 获取当前窗口wrap.value().addPass(count); // 更新MetricBucket中的计数器 }
5.8.3 滑动窗口实现细节
5.8.3.1 环形数组复用
- 设计目的:通过模运算复用数组,避免频繁内存分配。
- 示例:秒级统计(
intervalInMs=1000
,sampleCount=2
)时,每500ms一个窗口,数组长度固定为2。
5.8.3.2 线程安全
- CAS操作:通过
AtomicReferenceArray.compareAndSet()
保证并发安全。 - 锁机制:过期窗口重置时使用
ReentrantLock
避免竞态条件。
5.8.3.3 抢占机制(OccupiableBucketLeapArray)
- 实现类:
OccupiableBucketLeapArray
支持占用未来窗口的配额:public class OccupiableBucketLeapArray extends LeapArray<MetricBucket> {private FutureBucketLeapArray borrowArray; // 预占用的未来窗口 }
5.8.4 统计维度与初始化
** 5.8.4.1 秒级与分钟级统计**
- StatisticNode初始化 :
// 秒级:2个窗口,每个500ms rollingCounterInSecond = new ArrayMetric(2, 1000); // 分钟级:60个窗口,每个1s rollingCounterInMinute = new ArrayMetric(60, 60000, false);
** 5.8.4.2 入口调用链**
- StatisticSlot.entry():请求入口触发统计更新:
public void entry(...) {fireEntry(...); // 执行后续Slotnode.addPassRequest(count); // 更新通过数 }
5.8.5 性能优化
- LongAdder替代AtomicLong:减少CAS竞争,适合高频写场景。
- 时间窗口懒加载:仅在需要时创建窗口对象,减少内存占用。
5.8.6 总结
Sentinel的滑动时间窗口算法通过LeapArray环形数组管理时间窗口,每个窗口(WindowWrap)记录特定时间段的统计指标(如通过数、异常数),核心利用模运算计算窗口索引,复用数组实现滑动效果;采用CAS与锁机制保证高并发下线程安全,结合LongAdder优化计数器性能,支持秒级(如2窗口/500ms)和分钟级(60窗口/1s)等多维度实时统计,确保流量控制的精准性与低开销。
Sentinel 的滑动时间窗口算法通过 LeapArray
环形数组管理时间窗口,每个窗口(WindowWrap
)通过MetricBucket
记录特定时间段的统计指标(如通过数、异常数)。核心原理是通过 时间戳除以窗口长度并取模 来计算窗口索引,实现滑动统计效果:index = (timestamp / windowLength) % arrayLength
,从而高效地在固定数组中复用时间窗口,降低内存开销。
同时,为确保每个窗口是“时间对齐”的,Sentinel 会通过 windowStart = timestamp - (timestamp % windowLength)
计算窗口的起始时间,保证所有统计窗口严格按照时间段划分,避免数据重叠。
算法在并发环境下采用 CAS 操作结合锁机制(如 ReentrantLock)保证窗口初始化与切换的线程安全;计数器使用 LongAdder
进行高性能累加,减少竞争开销。支持秒级(如 2 个窗口/500ms)和分钟级(如 60 个窗口/1s)等多维度实时统计,保证流控的实时性、准确性与低资源占用。
Sentiel 的滑动时间窗口算法主要通过 LeapArray
环形数组管理时间窗口,每个窗口(WindowWrap
)通过MetricBucket
记录特定时间段的统计指标(如通过数、异常数)。LeapArray
主要属性有时间窗口总长度、样本窗口个数、样本窗口长度、WindowWrap
数组,当需要添加统计指标时,先通过currentWindow()
方法获取当前时间窗口,这个方法是实现滑动时间窗口的核心所在,通过时间戳除以窗口长度并与窗口长度取模来计算窗口索引,实现滑动效果,复用固定数组中的时间窗口,同时通过时间戳减去时间戳与窗口长度的模来计算当前窗口此时的起始时间,然后和时间窗口的起始时间对比,如果等于直接返回旧的时间窗口,如果计算时间大于时间窗口的起始时间就重置窗口。获取完时间窗口后就是给时间窗口添加统计指标,通过获取到的时间窗口拿到指标统计类 MetricBucket
, MetricBucket
里面使用了 JDK 提供的 LongAdder 来优化计数器性能,避免传统的锁和 CAS 自旋开销,从而实现高并发场景下的无锁安全累加。
优化后的版本:
Sentinel 的滑动时间窗口算法主要通过
LeapArray
环形数组来管理时间窗口,每个时间窗口由WindowWrap
表示,用于封装该时间段的统计指标,实际的指标数据则由内部的MetricBucket
记录,比如通过数、异常数、响应时间等。
LeapArray
的核心属性包括时间窗口总长度、样本窗口数量、每个窗口的时间长度以及WindowWrap[]
数组。滑动窗口的实现关键在于其currentWindow()
方法:
- 首先,通过
timestamp / windowLength % arrayLength
计算窗口索引,实现环形数组的复用。- 然后,通过
windowStart = timestamp - (timestamp % windowLength)
对时间进行对齐,获取该时间点所对应窗口的标准起始时间。- 若计算出的时间等于当前槽位的窗口起始时间,则直接复用已有窗口;若时间更大,则说明窗口已过期,需重置窗口内容。
获取到窗口后,便可通过其持有的
MetricBucket
添加统计指标。MetricBucket
内部使用了 JDK 的LongAdder
作为计数器,这是一种高性能的线程安全累加器,通过分段累加机制避免传统锁与 CAS 自旋所带来的性能瓶颈,非常适合 Sentinel 这类高并发统计场景。综上,Sentinel 滑动时间窗口通过数组复用、时间对齐、窗口重置和无锁累加等机制,实现了低开销、高精度的实时流量统计能力。
优点:
- 结构清晰:数据结构 → 核心方法 → 算法原理 → 线程安全,层层递进;
- 术语准确:环形数组、窗口对齐、窗口重置、分段累加;
- 用语专业:如“高性能的线程安全累加器”、“低开销、高精度的统计能力”;
- 面试风格浓厚:用词正式、表达逻辑清晰,给人“深入理解源码”的印象。
如果你希望我再帮你扩展为一分钟口述版、图示说明版、或加入源码片段讲解版,也可以告诉我。
5.9 参考文章
Sentinel核心算法解析の滑动窗口算法
5.10
很好,下面为你详细补充 Sentinel 滑动时间窗口算法的核心源码类、类之间关系、关键字段与方法,以便在面试中更系统地展示你的理解。
5.10.1 核心类图与职责说明
+-------------------------+
| SlidingWindowMetric | <-- 对外暴露的统计接口
+-------------------------+
| - BucketLeapArray data |
| - addCount(), pass(), ... |
+-------------------------+|▼
+-------------------------+
| BucketLeapArray | <-- 管理滑动窗口数组(继承 LeapArray)
+-------------------------+
| - WindowWrap<MetricBucket>[] |
| - currentWindow(), list() |
+-------------------------+|▼
+-------------------------+
| WindowWrap<T> | <-- 封装单个时间窗口的数据 + 起始时间
+-------------------------+
| - long windowStart |
| - T value |
+-------------------------+|▼
+-------------------------+
| MetricBucket | <-- 存储窗口内的统计指标(如通过数、异常数等)
+-------------------------+
| - LongAdder[] counters |
+-------------------------+
5.10.2 关键类与源码解析(基于 sentinel-core
源码)
SlidingWindowMetric
- 主要对外提供统计接口。
- 内部持有一个
BucketLeapArray
对象,封装了滑动窗口的核心逻辑。
public class SlidingWindowMetric {private final BucketLeapArray data;public SlidingWindowMetric(int sampleCount, int intervalInMs) {this.data = new BucketLeapArray(sampleCount, intervalInMs);}public void addPass(int count) {data.currentWindow().value().addPass(count);}public long pass() {long pass = 0;List<MetricBucket> list = data.values();for (MetricBucket bucket : list) {pass += bucket.pass();}return pass;}
}
BucketLeapArray
- 实际是
LeapArray<MetricBucket>
的实现类。 - 内部通过固定长度的
WindowWrap<MetricBucket>[]
数组管理时间窗口。
public class BucketLeapArray extends LeapArray<MetricBucket> {public BucketLeapArray(int sampleCount, int intervalInMs) {super(sampleCount, intervalInMs);}@Overrideprotected MetricBucket newEmptyBucket(long time) {return new MetricBucket();}@Overrideprotected void resetWindowTo(WindowWrap<MetricBucket> w, long startTime) {w.resetTo(startTime);w.value().reset();}
}
LeapArray<T>
- 通用滑动窗口抽象类。
- 关键算法在
currentWindow(long time)
方法中:
public WindowWrap<T> currentWindow(long timeMillis) {long timeId = timeMillis / windowLength;int idx = (int)(timeId % array.length);long windowStart = timeMillis - (timeMillis % windowLength);// 取当前位置窗口WindowWrap<T> old = array.get(idx);if (old == null) {// 创建新窗口(CAS 保证线程安全)WindowWrap<T> newWrap = new WindowWrap<T>(windowLength, windowStart, newEmptyBucket(timeMillis));if (array.compareAndSet(idx, null, newWrap)) {return newWrap;} else {// 并发失败重试Thread.yield();return array.get(idx);}} else if (old.windowStart() == windowStart) {return old;} else if (windowStart > old.windowStart()) {// 过期重置窗口if (updateLock.tryLock()) {try {old.resetTo(windowStart);old.value().reset();} finally {updateLock.unlock();}}return old;} else {// 当前窗口时间小于已有窗口,说明是旧数据return new WindowWrap<T>(windowLength, windowStart, newEmptyBucket(timeMillis));}
}
WindowWrap<T>
- 表示一个固定时间长度的时间片。
- 包含窗口的起始时间
windowStart
和数据内容T
(如MetricBucket
)
public class WindowWrap<T> {private long windowStart;private final long windowLength;private final T value;public void resetTo(long startTime) {this.windowStart = startTime;}public long windowStart() {return windowStart;}public T value() {return value;}
}
MetricBucket
- 使用
LongAdder
存储多种指标值(高并发性能更优于 AtomicLong)。
public class MetricBucket {private final LongAdder[] counters;public void addPass(int count) {counters[PASS].add(count);}public long pass() {return counters[PASS].sum();}public void reset() {for (LongAdder adder : counters) {adder.reset();}}
}
5.10.3 总结面试答题要点(提纲式)
-
核心结构:基于
LeapArray
+WindowWrap
+MetricBucket
构建环形滑动窗口。 -
窗口索引计算:
index = (timestamp / windowLength) % arrayLength
-
窗口起始对齐:
startTime = timestamp - (timestamp % windowLength)
-
高并发优化:
- CAS 创建窗口;
ReentrantLock
保证重置线程安全;- 使用
LongAdder
优化计数性能。
-
支持多维度粒度:
- 秒级(如 2 个窗口 / 500ms);
- 分钟级(如 60 个窗口 / 1s);
- 精准实时统计流量指标。
如果你要面试的是高级开发或中台、网关类岗位,也可以进一步延伸:
- Sentinel 滑动窗口和令牌桶、漏桶的比较;
- 与 Nginx 或 Envoy 的限流策略对比;
- 在 Spring Cloud Alibaba 中使用 Sentinel 的最佳实践。