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>(双端队列),配合 pollFirstofferLast 实现 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.000windowLength = 1000ms,这个窗口就统计了从 12:00:00.00012: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; // 存储窗口的环形数组
    
  • 构造方法:通过sampleCountintervalInMs计算窗口长度:
    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(...); // 执行后续Slot
        node.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()方法获取当前时间窗口,这个方法是实现滑动时间窗口的核心所在,通过时间戳除以窗口长度并与窗口长度取模来计算窗口索引,实现滑动效果,复用固定数组中的时间窗口,同时通过时间戳减去时间戳与窗口长度的模来计算当前窗口此时的起始时间,然后和时间窗口的起始时间对比,如果等于直接返回旧的时间窗口,如果计算时间大于时间窗口的起始时间就重置窗口。获取完时间窗口后就是给时间窗口添加统计指标,通过获取到的时间窗口拿到指标统计类 MetricBucketMetricBucket里面使用了 JDK 提供的 LongAdder 来优化计数器性能,避免传统的锁和 CAS 自旋开销,从而实现高并发场景下的无锁安全累加。

优化后的版本:

Sentinel 的滑动时间窗口算法主要通过 LeapArray 环形数组来管理时间窗口,每个时间窗口由 WindowWrap 表示,用于封装该时间段的统计指标,实际的指标数据则由内部的 MetricBucket 记录,比如通过数、异常数、响应时间等。

LeapArray 的核心属性包括时间窗口总长度、样本窗口数量、每个窗口的时间长度以及 WindowWrap[] 数组。滑动窗口的实现关键在于其 currentWindow() 方法:

  1. 首先,通过 timestamp / windowLength % arrayLength 计算窗口索引,实现环形数组的复用。
  2. 然后,通过 windowStart = timestamp - (timestamp % windowLength) 对时间进行对齐,获取该时间点所对应窗口的标准起始时间。
  3. 若计算出的时间等于当前槽位的窗口起始时间,则直接复用已有窗口;若时间更大,则说明窗口已过期,需重置窗口内容。

获取到窗口后,便可通过其持有的 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);
    }

    @Override
    protected MetricBucket newEmptyBucket(long time) {
        return new MetricBucket();
    }

    @Override
    protected 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 总结面试答题要点(提纲式)

  1. 核心结构:基于 LeapArray + WindowWrap + MetricBucket 构建环形滑动窗口。

  2. 窗口索引计算index = (timestamp / windowLength) % arrayLength

  3. 窗口起始对齐startTime = timestamp - (timestamp % windowLength)

  4. 高并发优化

    • CAS 创建窗口;
    • ReentrantLock 保证重置线程安全;
    • 使用 LongAdder 优化计数性能。
  5. 支持多维度粒度

    • 秒级(如 2 个窗口 / 500ms);
    • 分钟级(如 60 个窗口 / 1s);
    • 精准实时统计流量指标。

如果你要面试的是高级开发或中台、网关类岗位,也可以进一步延伸:

  • Sentinel 滑动窗口和令牌桶、漏桶的比较;
  • 与 Nginx 或 Envoy 的限流策略对比;
  • 在 Spring Cloud Alibaba 中使用 Sentinel 的最佳实践。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

路上阡陌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值