欢迎光临
我们一直在努力

孤舟笔记 分布式与微服务篇十九 Sentinel的滑动窗口算法怎么实现的?从原理到源码一次讲透

文章目录

    • 先说结论
    • LeapArray 数据结构
    • 格子定位:时间到索引的映射
    • 数据写入:CAS 更新无锁
    • 数据统计:累加所有有效格子
    • 回答技巧与点评
      • 加分回答
      • 面试官点评

个人网站

前面讲了限流算法,知道 Sentinel 用滑动窗口——但它的滑动窗口和教科书上的不一样。Sentinel 的 LeapArray 是一个高性能、低内存的滑动窗口实现。面试官问这题,他想听的是:你能不能讲清 LeapArray 的数据结构、滑动机制、和指标统计的流程?

先说结论

| 维度 | 说明 || | ——|——|| | 数据结构 | 环形数组(LeapArray)+ 每个格子(WindowWrap)存统计数据 || | 窗口配置 | 默认 1 秒窗口分 2 格,每格 500ms || | 滑动方式 | 根据当前时间计算格子索引,覆盖过期格子 || | 并发安全 | 用 CAS 更新格子起始时间,避免加锁 || | 统计指标 | 通过 QPS、异常数、RT 等 || | 内存优化 | 固定大小数组,不创建新对象 ||

|一句话记住:Sentinel的滑动窗口像"环形跑道"——跑完一圈回到起点,旧的记录自动被覆盖"

LeapArray 数据结构

// Sentinel 核心数据结构(简化)
public class LeapArray<T> {
int windowLengthInMs; // 每格时长(默认500ms)
int sampleCount; // 格子数(默认2)
int intervalInMs; // 窗口总时长(默认1000ms)

// 环形数组 —— 核心存储 👈
AtomicReferenceArray<WindowWrap<T>> array;
}

// 每个格子
public class WindowWrap<T> {
long windowStart; // 格子起始时间
T value; // 统计数据(MetricBucket)
}

默认配置:1秒窗口,2个格子

array[0]: WindowWrap{start=1000ms, value=Bucket{pass=5, block=1}}
array[1]: WindowWrap{start=1500ms, value=Bucket{pass=3, block=0}}
↑ ↑
0-500ms的统计 500-1000ms的统计

总QPS = array[0].value.pass + array[1].value.pass = 8

格子定位:时间到索引的映射

// 根据当前时间计算格子索引
int calculateTimeIdx(long timeMillis) {
return (int)(timeMillis / windowLengthInMs) % sampleCount;
}

// 计算格子应该的起始时间
long calculateWindowStart(long timeMillis) {
return timeMillis timeMillis % windowLengthInMs;
}

示例:windowLengthInMs=500, sampleCount=2

timeMillis=1200 → idx = 1200/500 % 2 = 0, start = 1000
timeMillis=1700 → idx = 1700/500 % 2 = 1, start = 1500
timeMillis=2200 → idx = 2200/500 % 2 = 0, start = 2000 👈 复用array[0]

当时间推进到新格子时,旧的格子会被覆盖——这就是"滑动":不是物理移动数组,而是时间推进后旧格子自然过期。

数据写入:CAS 更新无锁

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) {
// 格子为空,创建新的
WindowWrap<T> wrap = new WindowWrap<>(windowStart, newBucket());
if (array.compareAndSet(idx, null, wrap)) { // 👈 CAS
return wrap;
}
} else if (windowStart == old.windowStart) {
// 时间匹配,直接用
return old;
} else if (windowStart > old.windowStart) {
// 时间已过期,覆盖旧格子 👈
if (old.tryLock()) {
try {
old.resetTo(windowStart); // 重置起始时间+清空数据
return old;
} finally {
old.unlock();
}
}
}
// CAS 失败或锁竞争,自旋重试
}
}

关键点:

  • 格子为空时用 CAS 创建,避免加锁
  • 格子过期时用锁覆盖,保证数据重置的原子性
  • 格子时间匹配时直接返回,零开销

数据统计:累加所有有效格子

public List<WindowWrap<T>> list(long timeMillis) {
int idx = calculateTimeIdx(timeMillis);
long windowStart = calculateWindowStart(timeMillis);

List<WindowWrap<T>> result = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
WindowWrap<T> wrap = array.get(i);
if (wrap == null) continue;

// 只统计窗口内的格子(过期的不算) 👈
if (isWindowDeprecated(wrap, timeMillis)) continue;
result.add(wrap);
}
return result;
}

// 判断格子是否过期
boolean isWindowDeprecated(WindowWrap wrap, long timeMillis) {
return timeMillis wrap.windowStart > intervalInMs;
}

示例:当前时间 = 2000ms

array[0]: start=1000ms → 2000-1000=1000ms = intervalInMs → 刚好过期 👈
array[1]: start=1500ms → 2000-1500=500ms < intervalInMs → 有效 ✅

统计QPS = array[1].pass = 3(只算有效格子)

Sentinel 滑动窗口全景

数据结构
├── LeapArray —— 环形数组
├── WindowWrap —— 格子(起始时间+统计数据)
└── MetricBucket —— 统计值(pass/block/exception/rt)

核心机制
├── 格子定位 —— 时间取模算索引
├── 数据写入 —— CAS无锁 + 过期覆盖
├── 数据统计 —— 遍历有效格子累加
└── 滑动本质 —— 不是物理移动,是时间推进后旧格子自然过期

配置
├── 默认1秒窗口分2格
├── 统计精度500ms
└── 可配置:sampleCount越大精度越高

口诀:环形数组存格子,时间取模算索引;
CAS写入无锁快,过期格子直接覆盖;
统计只算窗口内,旧数据自然淘汰;
不创建新对象省内存,高并发下性能好

回答技巧与点评

标准回答:Sentinel 用 LeapArray 实现滑动窗口:环形数组存 WindowWrap 格子,每个格子记录起始时间和统计数据。格子索引通过时间取模计算,写入时用 CAS 无锁更新,格子过期后覆盖重置。统计时遍历所有格子,跳过超出窗口时间的过期格子。默认 1 秒窗口分 2 格(500ms 精度),环形数组固定大小,不创建新对象。

加分回答

  • OccupiableBucketLeapArray:Sentinel 还有一种可抢占的滑动窗口,当当前格子被限流时,可以"借用"未来格子的配额,实现更平滑的限流效果
  • 滑动窗口 vs 令牌桶的选择:Sentinel 的 QPS 限流用滑动窗口统计,而 WarmUp(预热)限流用令牌桶(Guava RateLimiter)。滑动窗口更精确,令牌桶更适合预热场景
  • LeapArray 的内存优化:固定大小数组,对象复用(覆盖而非新建),每个格子只存少量计数器。1 秒 2 格的配置下,每个资源只占 2 个 WindowWrap 对象,非常省内存
  • 面试官点评

    这道题考的是你对 流量统计实现 的理解深度。最忌讳的回答是只知道"滑动窗口"四个字——面试官想听的是 LeapArray 的数据结构(环形数组+WindowWrap)和 滑动机制(时间取模定位+过期覆盖)。能画出环形数组示意图,再讲清 CAS 写入,就是高分回答。

    原文阅读


    内容有帮助?点赞、收藏、关注三连!评论区等你 💪

    赞(0)
    未经允许不得转载:171主机测评 » 孤舟笔记 分布式与微服务篇十九 Sentinel的滑动窗口算法怎么实现的?从原理到源码一次讲透
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址