限流算法深度与 Guava/Sentinel 源码:从单机令牌桶到分布式滑动窗口的流量防护体系

概述

系列定位与文章概述

本文是 高并发与稳定性工程 系列的第 1 篇。在总纲系列(《分布式系统架构认知与设计》)确立了"故障是常态"与"优雅降级"的核心原则,并深入拆解了超时公式、退避算法与跨层故障阻断策略之后,本文正式进入稳定性工程的第一道防线------限流。限流是整个高并发防御体系的基石,后续的熔断、降级、隔离、压测等机制,均建立在"先限住流量,再谈如何更优雅地处理被限流量"这一前提之上。

大促秒杀,订单服务的 QPS 从平日的 2000 瞬间飙到 15000。如果没有任何防护,数据库连接池将首先被打满,随后所有依赖服务连锁超时,最终导致全站雪崩。限流,就是在这种场景下说"不"的艺术------直接拒绝超出系统承载能力的请求,保护核心链路不被冲垮。然而,限流远不止"QPS 超过 10000 就拒"这么简单:令牌桶与漏桶选哪个?突发流量是允许还是丢弃?预热阶段如何渐进式放量?单机限流与分布式限流的偏差如何处理?Sentinel 的滑动窗口如何做到无锁又精确?Gateway 的 Redis 令牌桶在 Lua 脚本里是如何原子执行的?Redis 主从切换时令牌数据丢失了怎么办?本文将以 电商订单服务的秒杀限流 为实战场景,从 Guava RateLimiter 的 SmoothBurstySmoothWarmingUp 源码出发(深入解析 synchronized(mutex()) 的线程安全机制),到 Sentinel LeapArray 的无锁环形数组与 CAS 循环,再到 Gateway 的 RedisRateLimiter Lua 脚本(含 Redis 故障时降级为本地 Guava 限流的完整容错策略),逐层拆解限流算法的数学原理和源码实现,最终输出一套 "网关层 → 应用层 → 单机层"三层限流协同 的生产级配置方案。

核心要点

  • 令牌桶(Guava)SmoothBursty 恒定速率与突发容量,SmoothWarmingUp 预热梯形积分公式与 coldFactorsynchronized(mutex()) 线程安全。
  • 滑动窗口(Sentinel)LeapArrayAtomicReferenceArray 无锁环形数组,currentWindow 的 CAS 循环,LongAdder 分散竞争累加。
  • 控制效果(Sentinel) :直接拒绝、Warm Up 预热、匀速排队(漏桶)的三种 controlBehavior 源码实现。
  • 网关限流(Gateway)RedisRateLimiter 的 Lua 脚本原子令牌桶,replenishRateburstCapacity 调优,Redis 故障时降级为本地 Guava 兜底。
  • 三层限流协同:网关 RedisRateLimiter(集群入口)→ 应用 Sentinel(服务级)→ 单机 Guava(单机预热),含降级容错路径。

文章组织架构图

flowchart TD subgraph "限流本质与算法对比" A1["令牌桶算法"] A2["漏桶算法"] A3["滑动窗口算法"] end subgraph "Guava单机限流源码分析" B1["SmoothBursty 恒定速率"] B2["SmoothWarmingUp 预热"] B3["synchronized(mutex()) 线程安全"] end subgraph "Sentinel分布式滑动窗口与FlowRule" C1["LeapArray 无锁环形数组"] C2["LongAdder 指标累加"] C3["controlBehavior 控制效果"] end subgraph "Sentinel DegradeRule 熔断" D1["慢调用比例/异常比例"] D2["熔断状态机"] end subgraph "Gateway RedisRateLimiter" E1["KeyResolver 解析限流Key"] E2["Lua 脚本令牌桶"] E3["Redis故障降级本地Guava"] end subgraph "多层限流协同与生产调优" F1["三层架构部署"] F2["调优公式速查"] F3["监控指标"] end subgraph "贯穿案例与前后衔接" G1["电商秒杀三层限流配置"] G2["故障演练"] G3["系列文章缝合"] end H["面试高频专题"] A1 & A2 & A3 --> B1 & B2 & B3 B1 & B2 & B3 --> C1 & C2 & C3 C1 & C2 & C3 --> D1 & D2 D1 & D2 --> E1 & E2 & E3 E1 & E2 & E3 --> F1 & F2 & F3 F1 & F2 & F3 --> G1 & G2 & G3 G3 --> H classDef default fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a

架构图说明:

  • 总览说明:全文从算法原理出发,经由三个主流实现(Guava → Sentinel → Gateway),加入容错降级策略,最后以多层协防和调优收尾,构建完整的限流知识体系。
  • 逐模块说明:模块 1 建立数学基础与算法选型;模块 2-4 拆解单机和分布式限流的源码实现;模块 5-6 将限流能力前移到网关层并补齐容错策略;模块 7 形成多层协同的全局视角;模块 8 用电商实战案例贯穿所有知识点;模块 9 缝合系列;模块 10 面试巩固。
  • 关键结论 :限流的本质不是"拦住更多请求",而是"用最小的拒绝代价保护系统容量"。从单机令牌桶到分布式滑动窗口再到网关层统一限流,每一层都有其适用边界和性能特征。理解源码是为了在生产中能精确调出 burstCapacitycoldFactor,以及在 Redis 故障时知道降级路径在哪里,让限流器成为保护系统的最可靠防线------即使依赖的中间件故障,限流本身也不能失效。

1. 限流本质与算法对比:令牌桶、漏桶、滑动窗口

限流的数学本质是 控制到达速率 ≤ 系统处理速率。当请求速率超过处理速率时,超出部分必须被丢弃或排队,以保护系统不被过载冲垮。经典算法主要有令牌桶、漏桶和滑动窗口三种。

1.1 令牌桶算法

令牌桶以恒定速率(rate)生成令牌并存入桶中,桶的容量(capacity)有限。请求到达时必须先获取令牌,若能获取则通过,否则被拒绝或等待。由于桶具备一定容量,令牌桶允许在短时间内突发等于桶容量的流量(burst = capacity),这是其与漏桶的最大区别。

1.2 漏桶算法

漏桶模型是一个固定容量的桶,请求以任意速率流入桶中,若桶满则溢出丢弃。桶底部有一个恒定速率(rate)的出水口,请求以固定速率流出并被处理。漏桶强制流量整形为恒定速率,不允许任何突发,适合对流量进行严格平滑的场景。

1.3 滑动窗口算法

滑动窗口将时间划分为多个小窗口(如1秒分为2个500ms窗口),每次统计当前时间所在窗口周期的请求计数,随着时间推移窗口不断滑动,统计窗口内的请求总数。精度取决于窗口数量:窗口越多越平滑,但存储和计算开销越大。Sentinel 的滑动窗口基于 LeapArray 实现,在性能和精度之间取得平衡。

令牌桶与漏桶算法对比示意图

flowchart TD subgraph TokenBucket [令牌桶算法] direction TB TB_Gen[恒定速率生成令牌] --> TB_Bucket[(令牌桶
容量=burst)] TB_Req[请求到达] --> TB_Get{获取令牌?} TB_Get -- 成功 --> TB_Pass[通过] TB_Get -- 失败 --> TB_Reject[拒绝/等待] end subgraph LeakyBucket [漏桶算法] direction TB LB_Req[请求到达] --> LB_Bucket[(漏桶
容量固定)] LB_Bucket -- 桶满 --> LB_Drop[溢出丢弃] LB_Bucket --> LB_Out[恒定速率流出] --> LB_Process[处理] end

图表主旨概括:对比令牌桶与漏桶的流量整形机制,令牌桶允许突发而漏桶严格平滑。

逐层/逐元素分解

  • 令牌桶:生成器以恒定速率填充桶,桶容量为 burst。请求获取令牌,成功即通过。突发流量可消耗桶内积攒的令牌。
  • 漏桶:请求进入桶,若桶满则丢弃。桶底以恒定速率流出请求,无论流入速率如何,流出速率保持恒定。

设计原理映射:令牌桶体现了"为突发留有余量"的弹性设计,漏桶则贯彻"绝对平滑"的刚性约束。在互联网场景中,业务通常具备一定的弹性,允许短时突发,因此令牌桶及其变种(如预热令牌桶)应用更广。

工程联系与关键结论Guava RateLimiter 实现的是令牌桶算法(含预热变种),而 Sentinel 的匀速排队模式(CONTROL_BEHAVIOR_RATE_LIMITER)实现的是漏桶算法。 选型时,对于允许短期流量突增的服务(如秒杀下单的瞬时点击),优先选择令牌桶;对于下游数据库等严格限制并发的资源,漏桶更能提供平滑保护。


2. Guava RateLimiter 单机限流源码分析

Guava 的 RateLimiter 提供了两种令牌桶实现:SmoothBursty(恒定速率)和 SmoothWarmingUp(预热模式)。两者均继承自抽象类 SmoothRateLimiter,核心字段包括:

  • storedPermits:当前存储的令牌数。
  • maxPermits:最大存储令牌数,由 maxBurstSeconds 或预热公式决定。
  • stableIntervalMicros:稳定速率下生成一个令牌所需的微秒数。
  • nextFreeTicketMicros:下一次请求可以无等待获取令牌的时间点(微秒)。

2.1 SmoothBursty:恒定速率与突发容量

SmoothBursty 的存储令牌数理论上无上限(实际由 maxBurstSeconds * permitsPerSecond 决定),允许的最大突发流量等于桶的最大容量。

令牌补充逻辑:resync(long nowMicros)

java 复制代码
void resync(long nowMicros) {
  if (nowMicros > nextFreeTicketMicros) {
    double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
    storedPermits = min(maxPermits, storedPermits + newPermits);
    nextFreeTicketMicros = nowMicros;
  }
}

该方法在每次 acquire 时首先调用,根据时间差生成新令牌,并将 nextFreeTicketMicros 更新为当前时间。coolDownIntervalMicros()SmoothBursty 中返回 stableIntervalMicros,即生成令牌的间隔固定。

获取令牌核心逻辑:reserveEarliestAvailable

java 复制代码
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
  resync(nowMicros);
  long returnValue = nextFreeTicketMicros;
  double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
  double freshPermits = requiredPermits - storedPermitsToSpend;
  long waitMicros = (long) (freshPermits * stableIntervalMicros);

  this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
  this.storedPermits -= storedPermitsToSpend;
  return returnValue;
}

该方法计算等待时间:先用存储令牌抵扣,不足部分通过将来生成的新令牌补齐,新令牌的生成需要花费时间(freshPermits * stableIntervalMicros),从而导致后续请求的等待。返回值 returnValue 是本次请求预计可以获取令牌的时间点,acquire 方法据此调用 sleep

线程安全机制 :整个 acquiretryAcquire 方法体都由 synchronized (mutex()) 包裹。mutex()RateLimiter 的实例方法,返回 this(或构造时传入的自定义锁对象)。这种设计将锁对象置于对象内部,简单可靠,避免外部代码错误使用锁。由于临界区主要是几个算术运算和赋值操作,执行时间极短,synchronized 性能表现优异,无需 ReentrantLock 的高级特性。

java 复制代码
public double acquire(int permits) {
  long microsToWait = reserve(permits);
  sleepUninterruptibly(microsToWait);
  return 1.0 * microsToWait / SECONDS.toMicros(1L);
}

final long reserve(int permits) {
  checkPermits(permits);
  synchronized (mutex()) {
    return reserveAndGetWaitLength(permits, stopwatch.readMicros());
  }
}

2.2 SmoothWarmingUp:预热梯形积分公式

SmoothWarmingUp 适用于系统冷启动或长时间闲置后突然流量涌入的场景。它定义了一个预热期 warmupPeriod,在此期间生成令牌的速率从冷启动速率(stableIntervalMicros * coldFactor)逐渐上升到稳定速率(stableIntervalMicros)。

关键参数:

  • coldFactor:冷启动因子,默认 3.0,即冷启动速率是稳定速率的 1/3。
  • thresholdPermits:预热期令牌阈值,存储令牌数大于此值即进入预热区。
  • maxPermits:桶最大令牌数,由预热公式决定。

预热梯形积分公式:

ini 复制代码
warmupPeriodMicros = (maxPermits - thresholdPermits) * stableIntervalMicros * coldFactor / 2
                    + thresholdPermits * stableIntervalMicros

该公式将预热期划分为两个区域:存储令牌在 0 ~ thresholdPermits 区间时,令牌产生速率恒定,等待时间对应稳定速率;在 thresholdPermits ~ maxPermits 区间时,速率线性降低,等待时间随令牌数增加而线性增大,体现了预热期令牌生成慢的特性。

storedPermitsToWaitTime 方法计算给定存储令牌数下,请求需要等待的时间:

java 复制代码
long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
  double availablePermitsAboveThreshold = storedPermits - thresholdPermits;
  long micros = 0;
  if (availablePermitsAboveThreshold > 0.0) {
    double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake);
    double length = permitsToTime(availablePermitsAboveThreshold)
                   + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake);
    micros = (long) (permitsAboveThresholdToTake * length / 2.0);
    permitsToTake -= permitsAboveThresholdToTake;
  }
  micros += (long) (stableIntervalMicros * permitsToTake);
  return micros;
}

当存储令牌数超过 thresholdPermits 时,对高出部分进行梯形积分求取等待时间,剩余部分按稳定速率计算。这样就实现了"令牌越满,获取越慢"的预热效果。

SmoothBursty vs SmoothWarmingUp 存储令牌曲线图

flowchart TB subgraph "SmoothBursty" direction TB SB_Time["时间"] --> SB_Stored["storedPermits 线性增长至 maxPermits
突发容量= maxBurstSeconds * rate"] end subgraph "SmoothWarmingUp" direction TB SW_Time["时间"] --> SW_Area1["稳定区:0 ~ thresholdPermits
生成速率=稳定速率"] SW_Area1 --> SW_Area2["预热区:thresholdPermits ~ maxPermits
生成速率线性降低,coldFactor=3"] SW_Area2 --> SW_Max["maxPermits 上限"] end classDef bursty fill:#f1f5f9,stroke:#334155,stroke-width:2px,color:#0f172a classDef warming fill:#ede9fe,stroke:#8b5cf6,stroke-width:2px,color:#3b2f4b class SB_Time,SB_Stored bursty class SW_Time,SW_Area1,SW_Area2,SW_Max warming

图表主旨概括 :展示两种模式下存储令牌数与令牌生成速率的关系,SmoothWarmingUp 通过预热区实现渐进式放量。

逐层/逐元素分解

  • SmoothBursty:令牌数线性增长直至 maxPermits,生成速率始终为恒定速率,突发由 maxBurstSeconds 控制。
  • SmoothWarmingUp:分为稳定区(thresholdPermits 以下)和预热区(thresholdPermits ~ maxPermits),预热区令牌生成速率从 coldFactor 倍稳定间隔逐渐减小,梯形积分公式决定等待时间曲线。

设计原理映射:预热模式反映了系统资源的"热身"特性------数据库连接池、缓存、JIT编译等在初始阶段处理能力较低,必须渐进式放量以避免瞬时高负载击垮系统。

工程联系与关键结论单机预热限流是保护 Pod 冷启动的第一道屏障。 在秒杀场景下,应用服务可能因弹性扩容新增 Pod,若不加预热直接承受满负载流量,极易导致新 Pod 被瞬间打垮。通过 warmupPeriod=10s, permitsPerSecond=500 配置,可让新节点在 10 秒内从约 167 QPS 平滑过渡到 500 QPS。


3. Sentinel 分布式滑动窗口与 FlowRule 源码

Sentinel 的限流能力基于滑动窗口统计实时 QPS/线程数,其核心数据结构是 LeapArray

3.1 LeapArray:无锁环形数组与 CAS 滑动

LeapArray 是一个固定时间窗口(如 1 秒)内划分若干个样本窗口的环形数组,通过 AtomicReferenceArray<WindowWrap<T>> 存储。例如 intervalInMs=1000, sampleCount=2,则每个小窗口长度为 500ms。

WindowWrap<T> 包含:

  • windowStart:窗口开始时间戳。
  • windowLength:窗口长度(如500ms)。
  • value:存储统计数据,限流场景下为 MetricBucket,内部使用 LongAdder 累加 pass/block 等指标。

currentWindow(long timeMillis) 方法实现无锁窗口滑动:

java 复制代码
public WindowWrap<T> currentWindow(long timeMillis) {
    long timeId = timeMillis / windowLengthInMs;
    int idx = (int)(timeId % array.length());
    timeMillis = timeMillis - timeMillis % windowLengthInMs;

    while (true) {
        WindowWrap<T> old = array.get(idx);
        if (old == null) {
            WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, timeMillis, newEmptyBucket(timeMillis));
            if (array.compareAndSet(idx, null, window)) {
                return window;
            } else {
                Thread.yield();
            }
        } else if (timeMillis == old.windowStart()) {
            return old;
        } else if (timeMillis > old.windowStart()) {
            if (updateLock.tryLock()) {
                try {
                    return resetWindowTo(old, timeMillis);
                } finally {
                    updateLock.unlock();
                }
            } else {
                Thread.yield();
            }
        } else {
            return new WindowWrap<T>(windowLengthInMs, timeMillis, newEmptyBucket(timeMillis));
        }
    }
}

CAS 循环逻辑

  1. 计算时间窗口 ID 和数组下标。
  2. 获取当前位置的 WindowWrap 引用。
  3. 若为 null,则 CAS 新建窗口放入;若时间戳匹配,直接返回;若窗口过期,则尝试获取更新锁(updateLock,内部使用 ReentrantLock)进行窗口重置,重置时更新 windowStart 和清空数据。
  4. 若竞争失败,通过 Thread.yield() 让出 CPU,稍后重试,避免长时间自旋。

这种设计使得大部分统计操作(判断窗口是否匹配)无需锁,只在窗口跨过临界点时才会锁竞争,且竞争窗口极短。LongAdderCell 数组进一步减少了并发累加时的 CAS 冲突,高并发下性能远超 AtomicLong

StatisticNode 持有两个 LeapArray<MetricBucket>

  • rollingCounterInSecond:秒级滑动窗口(1s,sampleCount 默认 2)。
  • rollingCounterInMinute:分钟级滑动窗口(60s,sampleCount 默认 12)。

当需要查询当前 QPS 时,调用 MetricBucketpass() 方法累加当前窗口所有小窗口的值,即可获得精确的通过数量。

3.2 FlowRule 控制效果实现

FlowRuleChecker.checkFlow 方法根据 FlowRule.controlBehavior 选择不同的限流逻辑:

  • CONTROL_BEHAVIOR_DEFAULT(直接拒绝) :若 passQps + acquireCount > count,则抛出 FlowException,快速失败。适用于对响应时间敏感的核心接口。
  • CONTROL_BEHAVIOR_WARM_UP(预热) :采用类似 Guava SmoothWarmingUp 的预热算法,内部维护一个令牌桶,根据 warmUpPeriodSeccoldFactor 计算当前速率。随着系统运行,桶内令牌逐渐消耗,通过速率从冷启动速率升至设定 QPS。
  • CONTROL_BEHAVIOR_RATE_LIMITER(匀速排队) :实现漏桶算法,请求必须通过一个虚拟队列,以固定间隔通过。若队列等待时间超过 maxQueueingTimeMs,则拒绝。适用于处理突发流量但对延迟有一定容忍度的场景,如消息队列消费。

Sentinel LeapArray 环形数组滑动窗口机制图

flowchart LR Time["时间轴"] --> WinA["窗口 A
windowStart=T0"] Time --> WinB["窗口 B
windowStart=T0+500ms"] Time --> WinC["..."] Time --> WinNew["新窗口
windowStart=T1"] subgraph "AtomicReferenceArray" Idx0["索引0"] --> Wrap0["WindowWrap A"] Idx1["索引1"] --> Wrap1["WindowWrap B"] end Wrap0 -->|"时间匹配"| Use["直接返回"] Wrap0 -->|"过期"| CAS_Reset["CAS重置窗口
更新windowStart,清空MetricBucket"] Wrap1 -->|"null"| CAS_New["CAS创建新窗口"]

图表主旨概括 :展示 LeapArray 如何通过 AtomicReferenceArray 和 CAS 实现时间窗口的无锁滑动和统计。

逐层/逐元素分解

  • 时间轴:每个小窗口具有固定的 windowStartwindowLength
  • AtomicReferenceArray:存储两个窗口引用,下标由时间计算得出。
  • 窗口匹配:当前时间对应的窗口若已存在且未过期,直接复用。
  • 窗口过期或缺失:通过 CAS 操作创建或重置窗口,失败则重试或 yield。

设计原理映射:无锁滑动窗口结合了"时间窗口"的统计精确性和"CAS"的高并发性能,避免了全局锁带来的瓶颈,是 Sentinel 高性能限流的基础。

工程联系与关键结论sampleCount 决定了统计平滑度与内存/CPU开销的平衡。 默认 2 个窗口在秒级统计中已经足够精确,若需要更细粒度监控,可增加样本数。每个 MetricBucketLongAdder 保证了高并发下 pass/block 指标的累加几乎无竞争。

Sentinel FlowRule 三种 controlBehavior 控制流程图

flowchart TD Req[请求到达] --> Check{controlBehavior} Check -- DEFAULT --> Direct{passQps + permits > count?} Direct -- Yes --> Reject[抛出 FlowException 拒绝] Direct -- No --> Pass[通过] Check -- WARM_UP --> WarmUp[预热令牌桶
动态计算当前速率] WarmUp --> WarmCheck{令牌足够?} WarmCheck -- Yes --> Pass WarmCheck -- No --> Reject Check -- RATE_LIMITER --> Leaky[漏桶排队] Leaky --> QueueTime{排队时间 < maxQueueingTimeMs?} QueueTime -- Yes --> Sleep[等待排队] --> Pass QueueTime -- No --> Reject

图表主旨概括:三种控制效果处理请求的决策树,体现不同的流量整形策略。

逐层/逐元素分解

  • 直接拒绝:瞬时判断是否超过阈值,立即拒绝,性能最高。
  • Warm Up:令牌桶维持当前预热状态,根据令牌存量决定通过或拒绝,阈值随时间动态调整。
  • 匀速排队:请求进入虚拟队列,按固定间隔消费,超出排队时长则拒绝。

设计原理映射 :Sentinel 通过 controlBehavior 将限流策略与具体算法解耦,允许运维根据接口特性灵活选择。直接拒绝适合对延迟零容忍的核心接口;预热适合启动缓慢的服务;匀速排队适合需要削峰填谷的场景。

工程联系与关键结论匀速排队模式实现的是漏桶算法 ,其内部通过 maxQueueingTimeMs 控制最大等待时间,该参数必须小于上游 RPC 超时时间(遵循总纲第4篇的超时传递链公式),否则会导致大量请求在队列中等待最终超时,进而引发连锁故障。


4. Sentinel DegradeRule 熔断与限流的组合

限流解决的是"流量过大"的问题,熔断解决的是"服务不可用"的问题。两者组合使用才能构建完整的稳定性防线。

Sentinel 的 DegradeRule 支持三种熔断策略:

  • 慢调用比例(DEGRADE_GRADE_RT :统计窗口内请求的响应时间,若 慢调用比例 > slowRatioThreshold,则熔断。慢调用标准为 rt > maxAllowedRt
  • 异常比例(DEGRADE_GRADE_EXCEPTION_RATIO:统计窗口内异常比例超过阈值则熔断。
  • 异常数(DEGRADE_GRADE_EXCEPTION_COUNT:统计窗口内异常数量超过阈值则熔断。

熔断状态机:CLOSED → OPEN → HALF_OPEN → CLOSED。当熔断触发进入 OPEN 状态后,经过 timeWindow 时长转入 HALF_OPEN 状态,此时放行一个探测请求,若成功则关闭熔断,若失败则重新进入 OPEN 状态。

限流与熔断的组合防御策略:先限流防冲垮,熔断兜底防雪崩。当流量突发时,限流器在第一层直接拒绝超量请求,防止系统进入过载状态。若系统内部出现依赖超时或异常,熔断器立即切断故障调用链,避免线程资源被耗尽。在秒杀订单服务中,对数据库查询接口配置 QPS 限流,同时对慢查询配置慢调用比例熔断,当 1 秒内慢调用比例超过 50% 且最小请求数达到 10 时,触发熔断,10 秒后进入半开探测。


5. Gateway RequestRateLimiter 与 RedisRateLimiter 集成

Spring Cloud Gateway 通过 RequestRateLimiter GatewayFilter 实现网关层限流,核心组件是 KeyResolverRedisRateLimiter

5.1 KeyResolver 与限流 Key 提取

KeyResolver 负责从请求中解析限流维度,常见实现:

  • PrincipalNameKeyResolver:按用户身份限流。
  • RemoteAddrKeyResolver:按客户端 IP 限流。
  • 自定义:如根据 Header 中的租户 ID。

5.2 RedisRateLimiter Lua 脚本令牌桶

RedisRateLimiter 通过执行 Lua 脚本原子性地实现令牌桶算法。脚本全文如下:

lua 复制代码
local tokens_key = KEYS[1]          -- 令牌桶key
local timestamp_key = KEYS[2]       -- 上次刷新时间戳key
local rate = tonumber(ARGV[1])      -- 补充速率(令牌/秒)
local capacity = tonumber(ARGV[2])  -- 桶容量
local now = tonumber(ARGV[3])       -- 当前时间(毫秒,由客户端传入)
local requested = tonumber(ARGV[4]) -- 请求的令牌数

local fill_time = capacity / rate * 1000  -- 桶完全填满所需毫秒
local ttl = math.floor(fill_time * 2)     -- Key 过期时间

-- 获取上次令牌存量,若无记录则初始化为满桶
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
  last_tokens = capacity
end

-- 获取上次刷新时间,若无则为0
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
  last_refreshed = 0
end

-- 计算时间差,补充令牌
local delta = math.max(0, now - last_refreshed)
local filled_tokens = math.min(capacity, last_tokens + (delta * rate / 1000))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
  new_tokens = filled_tokens - requested
  allowed_num = 1
end

-- 写回Redis
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)

return { allowed_num, new_tokens }

脚本分析

  • 令牌补充公式:filled_tokens = min(capacity, last_tokens + delta * rate / 1000),精确实现了令牌桶的时间线性补充。
  • 原子性:由于 Redis 单线程执行 Lua 脚本,整个检查、补充、消耗过程无并发竞争,保证了分布式环境下的限流准确性。
  • 客户端传入 now:规避了 Redis Cluster 多节点间的时钟漂移问题。如果依赖 Redis 服务器时间,主从或不同分片的时间差异会导致令牌补充速率不一致,而从客户端统一传入时间戳则消除了这种偏差。

Gateway RedisRateLimiter Lua 脚本执行时序图

sequenceDiagram participant Client as 客户端 participant Gateway as Spring Cloud Gateway participant Redis as Redis participant Guava as 本地Guava降级 Client->>Gateway: HTTP请求 Gateway->>Gateway: KeyResolver 解析限流Key Gateway->>Redis: EVAL Lua脚本
(tokens_key, timestamp_key, rate, capacity, now, requested) alt Redis正常 Redis-->>Gateway: {allowed, remaining} alt allowed=1 Gateway->>Client: 转发请求到后端 else allowed=0 Gateway-->>Client: 429 Too Many Requests end else Redis异常/超时 Gateway->>Guava: fallback到本地RateLimiter
按Key缓存实例(TTL 60s) alt 本地获取令牌成功 Gateway->>Client: 转发请求 else 失败 Gateway-->>Client: 429 Too Many Requests end end

图表主旨概括:展示正常限流流程与 Redis 故障时的降级路径,确保限流能力始终在线。

逐层/逐元素分解

  • 正常路径:Gateway 解析 Key 后调用 Redis 执行 Lua 脚本,根据返回值决定放行或拒绝。
  • 异常路径:捕获 Redis 访问异常(连接超时、命令失败等),降级为本地 Guava RateLimiter,按照 Key 维度缓存限流器实例。
  • 状态恢复:Redis 恢复后,后续请求将自动切回 Redis 令牌桶,本地 Guava 实例随 TTL 过期自然清理。

设计原理映射:这是典型的"降级+兜底"模式,体现了总纲第2篇的"故障是常态"原则。将强一致性的分布式限流交给 Redis,在故障时回退到弱一致性的本地限流,牺牲部分全局精度换取系统的持续防护能力。

工程联系与关键结论Redis 主从切换时令牌 Key 丢失不是灾难 ,脚本中 or capacity 保证了首次访问初始化满桶。但短暂的精度丢失可能导致全局流量超过预期,因此应用层 Sentinel 必须作为第二道防线兜底。


6. Redis 令牌桶容错与降级策略

6.1 Redis 主从切换与网络分区场景

Redis 主从切换或 Cluster 分片迁移时,tokens_key 可能因未持久化到新主而丢失。Lua 脚本中的 if last_tokens == nil then last_tokens = capacity end 将令牌数重置为满桶,避免了无限拒绝。但这意味着瞬间桶被重置为满容量,可能导致流量尖峰。

6.2 Gateway 层降级为本地 Guava 实现

当 Redis 调用抛出异常或超时时,Gateway 的 ReactiveRedisRateLimiter 需捕获异常并降级。通过扩展 RedisRateLimiter 或自定义过滤器,可加入本地缓存逻辑:

java 复制代码
@Component
public class FallbackRateLimiter {
    private final Cache<String, RateLimiter> cache = CacheBuilder.newBuilder()
            .expireAfterAccess(60, TimeUnit.SECONDS)
            .build();

    public Mono<Response> isAllowed(String key, double permitsPerSecond) {
        return Mono.fromSupplier(() -> {
            try {
                RateLimiter limiter = cache.get(key, () -> RateLimiter.create(permitsPerSecond));
                if (limiter.tryAcquire()) {
                    return new Response(true, 0);
                } else {
                    return new Response(false, 0);
                }
            } catch (Exception e) {
                return new Response(true, 0); // 极端兜底:按需可放行或拒绝
            }
        });
    }
}

在实际 Gateway 过滤器中,先尝试 Redis 限流,onErrorResume 捕获异常后调用 FallbackRateLimiter.isAllowed()。Key 的生成规则与 KeyResolver 一致,保证降级前后限流维度不变。TTL 设置为 60s,避免内存泄漏。

6.3 恢复与自动切回

一旦 Redis 恢复可用,后续请求将正常进入 Redis 限流分支,本地 Guava 不再被调用。Cache 中的实例在 TTL 过期后自动回收,不会长期占用内存。这种设计保证了限流系统的自愈能力,无需人工干预。


7. 多层限流协同与生产调优公式

7.1 三层限流架构

flowchart TD Internet[外部流量] --> GW[Spring Cloud Gateway
RedisRateLimiter
集群总限流] GW -- Redis正常 --> Redis[(Redis Cluster
令牌桶)] GW -- Redis故障 --> LocalGW[本地Guava降级
单网关兜底] GW --> App1[订单服务 Pod-1
Sentinel FlowRule QPS=500
Guava SmoothWarmingUp 500] GW --> App2[订单服务 Pod-2
同上] GW --> AppN[订单服务 Pod-N] subgraph 单Pod内部 Sentinel[Sentinel 分布式限流] --> GuavaSingle[Guava 单机预热限流] GuavaSingle --> Controller[业务Controller] end

图表主旨概括:展示三层限流的部署位置与流量路径,网关层、应用层、单机层各司其职。

逐层/逐元素分解

  • 网关层(RedisRateLimiter):对入口总流量进行全局限流,例如 10000 QPS,保证集群整体不被冲垮。降级路径指向本地 Guava。
  • 应用层(Sentinel):每个 Pod 通过 Sentinel 的分布式滑动窗口统计自身流量,控制单 Pod 流量不超过预设阈值(如 500 QPS),同时可通过控制台动态调整。
  • 单机层(Guava):对每个 Pod 内部进行预热和精细化控制,防止冷启动过载。

设计原理映射:分层限流体现了纵深防御思想。网关层防止洪峰冲垮整个集群;应用层解决单 Pod 过载和热点资源保护;单机层应对 JVM 冷启动和瞬时流量毛刺。

工程联系与关键结论Redis 故障时,网关降级为本地 Guava 后,单网关实际只能控制自身节点流量,集群总限流失效。因此,应用层的 Sentinel 在每个 Pod 上依然精确限流,保证不会因网关降级而导致整体过载。这种"降级不失守"的设计是高可用限流体系的关键。

7.2 生产调优公式

  • 网关层 RedisRateLimiter
    • replenishRate = 目标总 QPS
    • burstCapacity = replenishRate × burstSeconds (建议 burstSeconds=2~5s,允许合理突发)
    • 每秒补充令牌数 = replenishRate
  • Sentinel FlowRule
    • 单 Pod 限流 QPS = 目标总 QPS / Pod 数量,并预留 10%~20% 余量。
    • warmUpPeriodSec 需大于 JVM 预热时长(通常 10~30s)。
  • Guava SmoothWarmingUp
    • permitsPerSecond = 单 Pod 限流 QPS
    • warmupPeriod 建议与 Sentinel 的 warmUpPeriodSec 保持一致或略小,形成双重预热保护。

监控指标:

  • Sentinel Dashboard:passQps, blockQps, avgRt
  • Redis:通过 RedisRateLimiter.remaining 返回值可暴露为 Micrometer Gauge,用于告警。
  • Guava:可通过 RateLimiter.getRate() 和自定义埋点监控突发使用率。

8. 贯穿案例:电商秒杀订单的三层限流配置

场景:秒杀活动预估 QPS 10000,订单服务部署 20 个 Pod,使用 Redis Cluster 作为网关限流存储。

8.1 网关层配置

application-gateway.yml:

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: order-seckill
          uri: lb://order-service
          predicates:
            - Path=/order/seckill
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter:
                  replenishRate: 10000
                  burstCapacity: 20000
                key-resolver: "#{@remoteAddrKeyResolver}"

Redis 故障降级配置(使用自定义 Filter 实现 fallback,参考第6节)。

8.2 应用层 Sentinel 配置

java 复制代码
public void initFlowRules() {
    List<FlowRule> rules = new ArrayList<>();
    FlowRule rule = new FlowRule("seckillOrder");
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    rule.setCount(500);  // 单机500
    rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
    rule.setWarmUpPeriodSec(15);
    rule.setMaxQueueingTimeMs(200);
    rules.add(rule);
    FlowRuleManager.loadRules(rules);
}

8.3 单机层 Guava 配置

java 复制代码
@Bean
public RateLimiter seckillRateLimiter() {
    return RateLimiter.create(500, 10, TimeUnit.SECONDS); // 500 QPS, 10s预热
}

在 Controller 中先经过 Guava 限流,再由 Sentinel 注解 @SentinelResource 保护。

8.4 故障演练与效果验证

sequenceDiagram participant Load as JMeter(12000 QPS) participant GW as Gateway participant Redis as Redis Cluster participant Order as Order Service Pod(x20) participant Sentinel as Sentinel Dashboard Load->>GW: 大量秒杀请求 GW->>Redis: EVAL 限流 Lua Redis-->>GW: 正常返回 allowed=1/0 Note over GW,Sentinel: 正常阶段:网关通过10000 QPS,Sentinel各Pod限流500 Redis--xGW: 主从切换,连接超时 GW->>GW: onErrorResume 降级到本地Guava(rate=10000) GW->>Order: 转发(本地限流放行) Note over Order,Sentinel: Sentinel Dashboard显示 blockQps 略升,整体无异常 Redis-->>GW: 恢复可用 GW->>Redis: EVAL 正常 Note over GW: 自动切回Redis,降级Guava TTL过期回收

故障演练效果:Redis 故障期间,全局限流精度下降(各网关节点独立限流),但应用层 Sentinel 兜底,单 Pod 过载风险可控。系统吞吐量略有下降(约 5%),无错误率飙升,达到高可用设计目标。


9. 与前后系列的衔接

  • 总纲第 4 篇(时间维度与故障传播) :匀速排队模式的 maxQueueingTimeMs 必须小于上游 RPC 超时时间,遵循 上游超时 > 下游超时之和 的总纲公式,避免排队超时引发级联重试。
  • 总纲第 2 篇(设计原则):限流是"故障是常态"原则的直接体现------超过系统容量就快速失败。多层降级策略(Redis→Guava)诠释了"优雅降级"。
  • 本系列第 2 篇(熔断降级):Sentinel DegradeRule 是限流的下一道防线,当限流漏过的流量导致慢调用或异常时,熔断器介入阻止雪崩。
  • 本系列第 6 篇(秒杀架构):Redis 预减库存与令牌桶限流可组合使用,限流器保证进入库存扣减的流量可控。
  • 本系列第 9 篇(Gateway 深度):将深入探讨 RedisRateLimiter 的连接池配置、超时设置及在高负载 Gateway 中的性能优化。
  • 微服务与云原生架构系列第 5 篇(服务治理):Sentinel 规则通过 Nacos 配置中心持久化与动态推送的详细实现。

10. 面试高频专题

10.1 令牌桶和漏桶算法的区别是什么?各自的适用场景?

一句话回答:令牌桶允许突发流量,漏桶则强制流量平滑,前者适合互联网弹性业务,后者适合严格资源保护。

详细解释:令牌桶以恒定速率生成令牌,桶容量决定突发大小;漏桶固定流出速率,流入过快则丢弃。令牌桶适用于允许短期流量突增的微服务接口,如秒杀开始瞬间的点击,利用桶内积攒的令牌吸收突发;漏桶则适合下游数据库连接池等严格线性消费的资源,防止突发请求耗尽连接。Sentinel 的匀速排队模式是漏桶实现,Guava 的 SmoothBursty/SmoothWarmingUp 是令牌桶变种。

多角度追问

  • 在秒杀场景下,为什么网关层用令牌桶(允许突发)而数据库层适合漏桶?
  • 如何动态调整令牌生成速率以适应系统负载?
  • 令牌桶的 burstCapacity 设置过大会有什么风险?

加分回答 :Guava SmoothWarmingUp 通过冷启动因子 coldFactor(默认3)和预热梯形积分公式,使令牌生成速率渐进增长,防止冷系统被突发流量打垮。令牌桶与漏桶可以组合使用,例如在网关层用令牌桶吸收突发,在应用层用漏桶整形后发给下游,实现两级防护。


10.2 Guava SmoothBursty 和 SmoothWarmingUp 的源码实现有何不同?预热公式是如何推导的?synchronized(mutex()) 如何保证线程安全?

一句话回答SmoothBursty 令牌生成速率恒定;SmoothWarmingUp 通过预热区梯形积分使生成速率与存储令牌数负相关;synchronized(mutex()) 将令牌操作串行化,锁为 RateLimiter 实例。

详细解释SmoothBursty.coolDownIntervalMicros() 返回恒定 stableIntervalMicros,而 SmoothWarmingUp 覆写该方法,依据存储令牌所处区间返回不同间隔。预热公式 warmupPeriodMicros = (maxPermits - thresholdPermits) * stableInterval * coldFactor / 2 + thresholdPermits * stableInterval 保证了预热期总生成令牌数符合平均速率。acquire 入口被 synchronized(mutex()) 包裹,mutex() 默认返回 this,临界区只做算术和赋值,性能优异。

多角度追问

  • synchronized(this) 在高并发下有什么影响?为什么不用 ReentrantLock
  • coldFactor 默认3,调整后对系统有何影响?
  • resyncSmoothBursty 中如何允许突发?

加分回答RateLimiter 设计哲学是"尽可能快的路径",synchronized 在短临界区下偏向锁/轻量级锁优化充分,无额外内存开销。预热公式本质是线性函数在梯形面积下的积分,反映了冷启动处理能力线性增长。可通过自定义 RateLimiterSleepingStopwatch 实现更细粒度的等待控制。


10.3 Sentinel LeapArray 的滑动窗口是如何实现无锁的?currentWindow 的 CAS 逻辑是怎样的?LongAdder 相比 AtomicLong 有何优势?

一句话回答LeapArray 通过 AtomicReferenceArray 和 CAS 更新窗口引用实现无锁滑动;LongAdder 通过 Cell 数组分散竞争,写性能远高于 AtomicLong

详细解释currentWindow 在循环中检查当前窗口,若不匹配则 CAS 创建/重置窗口,失败 yield。过期重置使用 updateLock(ReentrantLock)防止 ABA 问题。LongAdder 内部 Cell[] 通过线程 probe 哈希到不同 Cell 累加,求和时才合并,适合高并发统计场景。

多角度追问

  • 为什么窗口重置需要锁而新建窗口用 CAS?能否全用 CAS?
  • sampleCount 设置较大时对性能有何影响?
  • LongAdder.sum() 在统计时是否精确?

加分回答 :过期重置是复合操作,用轻量锁简化实现且竞争极低。LongAdder 在并发写时 sum() 非快照一致,但限流场景可接受短暂不一致,相比 AtomicLong 减少了缓存行争用(false sharing),其 Cell 通过 @Contended 注解填充缓存行。


10.4 Sentinel FlowRule 的三种 controlBehavior 在源码层面是如何实现的?

一句话回答 :直接拒绝比较 passQps;Warm Up 维护预热令牌桶;匀速排队用漏桶算法控制通过间隔。

详细解释DefaultController 从当前窗口取 passQps 比较;WarmUpController 模拟 Guava 预热,通过 storedTokenslastFilledTime 动态计算阈值;RateLimiterController 记录 latestPassedTime,要求 当前时间 >= latestPassedTime + costTime,且等待时间不超过 maxQueueingTimeMs,否则拒绝。

多角度追问

  • Warm Up 模式与 Guava 预热有何细节差异?
  • 匀速排队模式如何保证公平?
  • 直接拒绝模式为何性能最高?

加分回答 :Sentinel 的 Warm Up 无全局锁,结合滑动窗口实现无锁预热桶。匀速排队是"虚拟队列",只有时间戳计算,没有真实队列,内存占用极小。直接拒绝只需一次 Metric 查询和比较,路径最短。


10.5 Gateway RedisRateLimiter 的 Lua 脚本令牌桶算法是什么?如何保证原子性?Redis 故障时如何容错?

一句话回答:Lua 脚本原子计算时间差补充令牌、判断并消耗;Redis 异常时 Gateway 降级为本地 Guava RateLimiter。

详细解释 :脚本使用 delta * rate / 1000 补充令牌,min 限幅,通过 redis.call('setex') 写回。Redis 单线程执行 Lua 保证原子性。容错方面,Gateway 捕获 Redis 异常,降级到 Cache<Key, RateLimiter> 本地 Guava,TTL 60s,恢复后自动切回。

多角度追问

  • 为什么客户端传入时间戳而不使用 Redis 的 TIME 命令?
  • 本地 Guava 降级后多 Gateway 实例如何协同?
  • Key 丢失时 or capacity 是否安全?

加分回答 :客户端传时解决 Redis Cluster 时钟漂移。降级后各实例独立限流,短时失准但应用层 Sentinel 兜底。or capacity 避免无限拒绝,但可能导致瞬问流量高于阈值,可通过设置 burstCapacity 适当留有余地。


10.6 如何设计多层限流体系?网关层、应用层、单机层的限流阈值如何分配?Redis 故障时降级路径是怎样的?

一句话回答:三层分别控制入口总流量、单服务实例流量和 Pod 内部冷启动流量;阈值按"总限流/实例数"分配并留有余量;Redis 故障时网关降级本地 Guava,Sentinel 继续兜底。

详细解释 :网关层 RedisRateLimiter 按总目标 QPS 设置;应用层 Sentinel 单 Pod 限流 count = 总QPS/Pod数 * 1.2;单机 Guava 同应用层但加入预热。降级路径:Redis 异常→本地 Guava(按 Key 缓存,单网关限总 QPS)→若 Guava 也崩,Sentinel 仍能保护单 Pod。Sentinel 规则持久化到 Nacos,重启不丢。

多角度追问

  • 单机预热是否可仅放在应用层?
  • 分布式限流和网关限流同时存在的必要性?
  • Pod 动态扩缩时阈值如何自适应?

加分回答:多层均设预热是纵深防御,网关预热可保护新 Gateway 实例。阈值自适应可通过监听注册中心(如 Nacos)实例数变化,动态推送 Sentinel 规则,属于弹性伸缩的高级话题。


10.7 系统设计题:设计电商秒杀系统完整限流方案(QPS 50000,50 Pod)

要求:(1) 各层参数与选型;(2) 各层超时与排队配合;(3) RedisRateLimiter Lua 调优;(4) Redis 故障降级策略;(5) 画出含故障降级路径的架构图。

(1) 限流参数与工具选型

  • 网关层RedisRateLimiterreplenishRate=50000burstCapacity=150000(突发3秒)。KeyResolver 使用 RemoteAddrKeyResolver 或自定义 token 限流。
  • 应用层 :Sentinel FlowRule,单 Pod count=1200(50*1200=60000,留有 20% 余量),controlBehavior=WARM_UPwarmUpPeriodSec=20maxQueueingTimeMs=300 用于内部保护。
  • 单机层 :Guava RateLimiter.create(1200, 20, TimeUnit.SECONDS),预热 20 秒。

(2) 超时与排队配合

  • 上游调用方超时:总体超时 500ms。
  • Sentinel 匀速排队(如有使用)maxQueueingTimeMs=300ms,加上网络和处理时间,确保总和 < 500ms,遵循 上游超时 > 下游超时之和
  • Gateway 层不排队,直接拒绝或降级。

(3) RedisRateLimiter Lua 调优

  • 客户端传入 now 为统一 UTC 毫秒。
  • 使用 Redis Cluster 的 hash tag {order_seckill} 保证同一 Key 落到相同分片。
  • TTL 设置为 (capacity/rate)*1000 * 2,平衡内存与 Key 泄漏风险。
  • 考虑 Redis 持久化:AOF 持久化可记录每次令牌变化,但性能损耗大,依赖容错策略更实际。

(4) Redis 故障降级策略

  • Gateway 捕获 Redis 异常,降级到本地 Cache<Key, RateLimiter> 的 Guava RateLimiter.create(50000),TTL 60s。
  • 多 Gateway 实例降级后各自限流 50000,总流量可能超限,但应用层每个 Pod Sentinel 限流 1200,整体受保护。
  • Redis 恢复后自动切回,本地 Guava 过期回收。

(5) 含故障降级路径的架构图

flowchart TD Client[客户端流量 50000 QPS] --> GW[Gateway 实例x2] subgraph Gateway层 GW -->|正常| Redis[(Redis Cluster
replenishRate=50000
burstCapacity=150000)] GW -->|Redis故障| LocalGW[本地Guava RateLimiter 50000
Cache TTL 60s] end GW --> App1[订单Pod-1
Sentinel 1200 QPS WarmUp
Guava 1200 WarmUp] GW --> App2[订单Pod-2
同上] GW --> App50[订单Pod-50] subgraph 单Pod内部 Sentinel[Sentinel 滑动窗口限流 1200] --> Guava[Guava 预热限流 1200] --> Controller[业务逻辑] end

业务流程:客户端请求 → 网关层 RedisRateLimiter 判断全局令牌 → 成功则路由到订单服务 Pod → Pod 内 Sentinel 滑动窗口判断当前 QPS → 通过后 Guava 预热令牌桶再次判定 → 最终处理请求。如果 Redis 不可用,网关退化为本地 Guava,每个网关独立限流 50000,可能出现总流量略超但被应用层 Sentinel 1200/Pod 钳制,保证不会雪崩。

时序图(故障降级)

sequenceDiagram participant Client participant Gateway1 participant Gateway2 participant Redis participant OrderPod Note over Client,OrderPod: 正常阶段 Client->>Gateway1: 请求 Gateway1->>Redis: EVAL 限流脚本 Redis-->>Gateway1: allowed=1 Gateway1->>OrderPod: 转发 OrderPod->>OrderPod: Sentinel+Guava 限流 OrderPod-->>Client: 响应 Note over Redis: Redis 主从切换故障 Gateway1->>Redis: EVAL 超时/异常 Gateway1->>Gateway1: 捕获异常,降级本地Guava(50000) Gateway2->>Gateway2: 同样降级本地Guava(50000) Gateway1-->>Client: 转发(本地限流放行) Gateway2-->>Client: 转发(本地限流放行) Note over OrderPod: 各Pod Sentinel限制1200,实际总吞吐量被压制在60000左右

关键设计决策:网关降级时采用本地 Guava,虽然集群总限流失效,但结合应用层 Sentinel 的精准控制,整体系统容量仍被限制在安全水位(50 Pod × 1200 = 60000),低于系统最大承载,保证不崩溃。这也体现了"永远不要信任任何单一组件"的防御理念。


10.8 Sentinel 的 warmUpPeriodSec 和 Guava 的 warmupPeriod 如何配合?

一句话回答:两者应保持一致或 Sentinel 略大于 Guava,Sentinel 作为分布式控制,Guava 作为单机补充。

详细解释:Sentinel 的 Warm Up 作用于整个服务实例的 QPS 统计,控制集群级别的预热速率;Guava 作用于单 JVM 内部线程,提供更细粒度的令牌获取等待。若 Sentinel 预热 15 秒,Guava 建议 10-15 秒,避免 Guava 预热过快导致 Sentinel 还未升到高阈值时大量本地令牌被消耗,造成限流不准。

多角度追问

  • 预热期间如何避免流量突刺被误判为异常?
  • 如果只设一层预热会有什么问题?
  • 预热时间如何根据 JVM 启动时间自动设置?

加分回答:可通过 JMX 暴露 JVM 启动时间,结合动态调整预热参数。预热本质是防止"冷系统"被高负载击垮,JIT 编译、连接池预热等也需要时间,因此预热时长最好包含这些组件的热身时间。


10.9 网关层限流时,如何避免大 Key 导致 Redis 热点?

一句话回答:使用令牌桶算法分流,或结合本地令牌缓存减少 Redis 访问。

详细解释 :高流量下所有网关实例访问同一个 tokens_key 会成为 Redis 热点。可通过将限流 Key 拆分(如按用户 ID 哈希后取模)将请求分散到多个 Key,但会降低全局精度。更好的做法是在 Gateway 层引入本地预取令牌(如每 10ms 批量获取令牌),减少 Redis 调用次数,Sentinel 也有类似的批量计数优化。

多角度追问

  • 本地预取令牌是否会导致限流不准?
  • 如果限流 Key 很多,Redis 内存压力大怎么办?
  • 是否可以用 Lua 的 redis.call 多次操作不同 Key 避免热点?

加分回答:本地预取可结合"配额"机制,每个网关实例定时从 Redis 总桶中领取一定量配额(例如每次 1000 个),本地消耗完后再领取,这样既减少 Redis 访问,又保持全局相对准确。


10.10 滑动窗口的窗口数量越多越好吗?精度与性能如何平衡?

一句话回答:窗口越多精度越高但内存和计算开销增大,一般 2-10 个窗口足够。

详细解释sampleCount 增加会提高滑动窗口的平滑度,但每个窗口需要维护一个 MetricBucket(内部多个 LongAdder),内存占用随之增长。Sentinel 默认 sampleCount=2,表示 1 秒分为两个 500ms 窗口,足以应对大多数限流场景。极端精确的场景可设置为 10,但需评估对 GC 和 CPU 的影响。

多角度追问

  • 窗口数量为奇数会有什么影响?
  • 滑动窗口和固定窗口相比有何优势?
  • 为什么不在每个请求到来时重新计算精确的 QPS?

加分回答:固定窗口存在临界点突发问题,滑动窗口通过覆盖多个小窗口平滑了边界。LeapArray 通过循环数组避免了大量窗口对象的创建和销毁,这也是其高性能的关键。


10.11 限流与熔断同时触发的优先级与协作机制?

一句话回答:限流优先于业务逻辑,熔断在业务逻辑执行后根据结果触发;两者并发时,先限流减少进入熔断检测的请求量。

详细解释 :在 Sentinel 中,FlowSlot 在调用链中位于 DegradeSlot 之前,请求先经过限流判断,若被限流拒绝则不会进入业务逻辑,也不会被熔断统计(因为未产生结果)。一旦请求通过限流并执行失败或超时,熔断器才会统计并可能打开。这样,限流充当了熔断的"减压阀",防止熔断器因为瞬时大流量误判。

多角度追问

  • 如果熔断器打开,限流还需要吗?
  • 限流和熔断的恢复条件有何不同?
  • 如何设计一个同时包含限流、熔断、降级的处理链?

加分回答:熔断打开时表明下游严重不可用,此时限流依然需要,因为即使熔断恢复后立即涌入大量请求可能再次打垮下游,需要限流控制探测流量和恢复速率。


10.12 在生产环境中,如何动态调整限流阈值而不重启服务?

一句话回答:Sentinel 基于控制台动态下发规则,Guava 需自行实现热更新;Gateway RedisRateLimiter 可修改配置后刷新。

详细解释 :Sentinel 通过 WritableDataSource 将规则持久化到 Nacos/Apollo,控制台修改后推送到客户端,实时生效。对于 Guava,可在 RateLimiter 外层包装一个持有 RateLimiter 的可变对象,通过定时任务或 JMX 接口创建新的 RateLimiter 实例替换旧引用。Gateway 的 RedisRateLimiter 可配合 Spring Cloud Bus 刷新配置,或通过自定义 Endpoint 动态调整 replenishRate 并更新 Redis 脚本参数。

多角度追问

  • 调整限流阈值时如何保证平滑切换?
  • 动态调整是否会影响正在等待的请求?
  • 为什么 Sentinel 控制台直接推送比拉取好?

加分回答 :使用 RateLimiter 替换引用时,旧对象可能仍有线程在 acquire 中阻塞,可以通过 tryAcquire + 超时的方式避免无限等待。Sentinel 的推送模式采用 Netty 长连接,实时性好,适合大规模集群;拉取模式则更易实现。


10.13 限流系统的可观测性如何设计?核心指标有哪些?

一句话回答 :核心指标包括 passQps, blockQps, remainingTokens, avgRt,需接入 Prometheus + Grafana 监控。

详细解释 :Sentinel Dashboard 已展示实时指标,可通过 Sentinel 暴露的 metric API 整合到 Micrometer,导出到 Prometheus。RedisRateLimiter 可通过自定义过滤器在响应头中暴露 X-RateLimit-Remaining,并转为 Gauge 指标。Guava 的 RateLimiter 可通过 AOP 记录 tryAcquire 结果,统计通过/拒绝数量。关键告警:blockQps 持续增长、remaining 接近 0 且持续时间较长、限流器自身异常(如 Redis 故障降级次数)。

多角度追问

  • 如何区分限流拒绝和业务拒绝?
  • 限流指标采集如何避免性能干扰?
  • 怎么跟踪一个请求经过了哪几层限流?

加分回答:在网关层生成唯一 RequestId 并透传,各层限流时记录带有 RequestId 的日志或埋点,可还原完整链路。指标采集采用异步队列写入,避免同步写 Prometheus Exporter 影响请求响应。


10.14 Guava RateLimiter 的 tryAcquireacquire 在工程中如何选择?

一句话回答 :非关键路径用 tryAcquire 立即返回,核心交易用 acquire 保证等待处理。

详细解释tryAcquire 非阻塞,获取不到令牌立即返回 false,适合可降级的查询或非核心逻辑,避免阻塞线程。acquire 会阻塞直到获取令牌,适合必须执行的写操作或核心交易,但需注意设置合理的超时,以免线程无限等待。结合熔断与隔离机制,防止大量 acquire 占用线程池。

多角度追问

  • acquire 阻塞过久可能导致线程耗尽,如何防范?
  • 如何实现带超时的 tryAcquire
  • tryAcquire(permits, timeout, timeUnit) 的内部实现是怎样的?

加分回答 :Guava 的 tryAcquire 带超时版本通过 reserve 计算等待时间,若等待时间超过给定超时则返回 false,否则 sleep 后返回 true。可以基于此封装一个带超时的阻塞获取,避免线程无限制等待。同时配合线程池拒绝策略,进一步控制资源。


10.15 限流算法如何与故障演练结合起来验证系统的韧性?

一句话回答:通过模拟流量洪峰和中间件故障,验证限流层能否按预期工作,以及降级路径是否通畅。

详细解释 :混沌工程中可注入 Redis 故障、网络延迟、CPU 高负载等,观察限流系统行为。验证点包括:Redis 故障时网关降级是否触发、降级后的限流精度是否在预期范围内、恢复后是否自动切回、三层限流在部分实例宕机后是否仍能保护余下实例。可使用 JMeter 施加超过限流阈值的流量,结合 Grafana 监控 blockQps 和业务错误率。

多角度追问

  • 如何设计一个专门测试限流器的压测脚本?
  • 演练中如何模拟 Redis 令牌桶的时钟漂移?
  • 演练结果如何量化为系统容量指标?

加分回答 :可编写 JMeter 插件或使用 Python 脚本定制请求模式(突发、持续),并在 Redis 端通过 CLIENT PAUSE 命令模拟延迟,或者通过 iptables 断开连接模拟分区。通过对比注入前后系统的"最大稳定吞吐量"来评估限流体系的防护效果。


Demo 代码与延伸阅读

Demo 代码

  1. Guava SmoothWarmingUp Bean 定义
java 复制代码
@Configuration
public class RateLimiterConfig {
    @Bean
    public RateLimiter orderSeckillRateLimiter() {
        // 单机 500 QPS,预热 10s
        return RateLimiter.create(500, 10, TimeUnit.SECONDS);
    }
}
  1. Sentinel FlowRule 初始化
java 复制代码
@Component
public class SentinelRulesLoader {
    @PostConstruct
    public void init() {
        List<FlowRule> rules = new ArrayList<>();
        FlowRule rule = new FlowRule("seckillOrder");
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule.setCount(500);
        rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
        rule.setWarmUpPeriodSec(15);
        rules.add(rule);
        FlowRuleManager.loadRules(rules);
    }
}
  1. Gateway 路由限流配置
yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: order-seckill
          uri: lb://order-service
          filters:
            - name: RequestRateLimiter
              args:
                key-resolver: "#{@remoteAddrKeyResolver}"
                redis-rate-limiter.replenishRate: 10000
                redis-rate-limiter.burstCapacity: 20000
  1. Gateway 降级配置 (见第6节 FallbackRateLimiter 代码示例)

延伸阅读

  • Guava RateLimiter 源码 (SmoothRateLimiter.java)
  • Sentinel 核心源码 (LeapArray.java, FlowRuleChecker.java)
  • Spring Cloud Gateway 官方文档 RequestRateLimiter
  • 《System Design Interview》 第4章 Rate Limiter 设计
相关推荐
前端小蜗2 小时前
转生到 AI 时代,我不再相信一键生成代码的传说
前端·人工智能·架构
_Evan_Yao2 小时前
限流的艺术:令牌桶与滑动窗口的博弈,以及我为何在 AI 项目中选择了后者
java·后端·架构
沪漂阿龙2 小时前
Hermes Agent 整体架构详解:AI Agent、Memory、Skills、MCP、工具调用、自我改进闭环全解析
人工智能·架构
leijiwen2 小时前
LinkLifeVerse OS:大消费类平台六层架构
架构
漓漾li3 小时前
每日面试题(2026-05-20)- GO AI agent全栈
后端·架构·go
xG8XPvV5d3 小时前
NUMA架构:多核性能优化指南
性能优化·架构
不是光头 强3 小时前
Java 后端实战进阶:从踩坑到架构的系统化笔记
java·笔记·架构
betazhou3 小时前
SQL server 2017镜像库主从同步架构部署
架构·sql server·高可用·主从同步·镜像库
DianSan_ERP4 小时前
自研电商架构:一套API安全对接60+平台
大数据·运维·数据库·人工智能·安全·架构