概述
系列定位与文章概述
本文是 高并发与稳定性工程 系列的第 1 篇。在总纲系列(《分布式系统架构认知与设计》)确立了"故障是常态"与"优雅降级"的核心原则,并深入拆解了超时公式、退避算法与跨层故障阻断策略之后,本文正式进入稳定性工程的第一道防线------限流。限流是整个高并发防御体系的基石,后续的熔断、降级、隔离、压测等机制,均建立在"先限住流量,再谈如何更优雅地处理被限流量"这一前提之上。
大促秒杀,订单服务的 QPS 从平日的 2000 瞬间飙到 15000。如果没有任何防护,数据库连接池将首先被打满,随后所有依赖服务连锁超时,最终导致全站雪崩。限流,就是在这种场景下说"不"的艺术------直接拒绝超出系统承载能力的请求,保护核心链路不被冲垮。然而,限流远不止"QPS 超过 10000 就拒"这么简单:令牌桶与漏桶选哪个?突发流量是允许还是丢弃?预热阶段如何渐进式放量?单机限流与分布式限流的偏差如何处理?Sentinel 的滑动窗口如何做到无锁又精确?Gateway 的 Redis 令牌桶在 Lua 脚本里是如何原子执行的?Redis 主从切换时令牌数据丢失了怎么办?本文将以 电商订单服务的秒杀限流 为实战场景,从 Guava RateLimiter 的 SmoothBursty 和 SmoothWarmingUp 源码出发(深入解析 synchronized(mutex()) 的线程安全机制),到 Sentinel LeapArray 的无锁环形数组与 CAS 循环,再到 Gateway 的 RedisRateLimiter Lua 脚本(含 Redis 故障时降级为本地 Guava 限流的完整容错策略),逐层拆解限流算法的数学原理和源码实现,最终输出一套 "网关层 → 应用层 → 单机层"三层限流协同 的生产级配置方案。
核心要点
- 令牌桶(Guava) :
SmoothBursty恒定速率与突发容量,SmoothWarmingUp预热梯形积分公式与coldFactor,synchronized(mutex())线程安全。 - 滑动窗口(Sentinel) :
LeapArray的AtomicReferenceArray无锁环形数组,currentWindow的 CAS 循环,LongAdder分散竞争累加。 - 控制效果(Sentinel) :直接拒绝、Warm Up 预热、匀速排队(漏桶)的三种
controlBehavior源码实现。 - 网关限流(Gateway) :
RedisRateLimiter的 Lua 脚本原子令牌桶,replenishRate与burstCapacity调优,Redis 故障时降级为本地 Guava 兜底。 - 三层限流协同:网关 RedisRateLimiter(集群入口)→ 应用 Sentinel(服务级)→ 单机 Guava(单机预热),含降级容错路径。
文章组织架构图
架构图说明:
- 总览说明:全文从算法原理出发,经由三个主流实现(Guava → Sentinel → Gateway),加入容错降级策略,最后以多层协防和调优收尾,构建完整的限流知识体系。
- 逐模块说明:模块 1 建立数学基础与算法选型;模块 2-4 拆解单机和分布式限流的源码实现;模块 5-6 将限流能力前移到网关层并补齐容错策略;模块 7 形成多层协同的全局视角;模块 8 用电商实战案例贯穿所有知识点;模块 9 缝合系列;模块 10 面试巩固。
- 关键结论 :限流的本质不是"拦住更多请求",而是"用最小的拒绝代价保护系统容量"。从单机令牌桶到分布式滑动窗口再到网关层统一限流,每一层都有其适用边界和性能特征。理解源码是为了在生产中能精确调出
burstCapacity和coldFactor,以及在 Redis 故障时知道降级路径在哪里,让限流器成为保护系统的最可靠防线------即使依赖的中间件故障,限流本身也不能失效。
1. 限流本质与算法对比:令牌桶、漏桶、滑动窗口
限流的数学本质是 控制到达速率 ≤ 系统处理速率。当请求速率超过处理速率时,超出部分必须被丢弃或排队,以保护系统不被过载冲垮。经典算法主要有令牌桶、漏桶和滑动窗口三种。
1.1 令牌桶算法
令牌桶以恒定速率(rate)生成令牌并存入桶中,桶的容量(capacity)有限。请求到达时必须先获取令牌,若能获取则通过,否则被拒绝或等待。由于桶具备一定容量,令牌桶允许在短时间内突发等于桶容量的流量(burst = capacity),这是其与漏桶的最大区别。
1.2 漏桶算法
漏桶模型是一个固定容量的桶,请求以任意速率流入桶中,若桶满则溢出丢弃。桶底部有一个恒定速率(rate)的出水口,请求以固定速率流出并被处理。漏桶强制流量整形为恒定速率,不允许任何突发,适合对流量进行严格平滑的场景。
1.3 滑动窗口算法
滑动窗口将时间划分为多个小窗口(如1秒分为2个500ms窗口),每次统计当前时间所在窗口周期的请求计数,随着时间推移窗口不断滑动,统计窗口内的请求总数。精度取决于窗口数量:窗口越多越平滑,但存储和计算开销越大。Sentinel 的滑动窗口基于 LeapArray 实现,在性能和精度之间取得平衡。
令牌桶与漏桶算法对比示意图
容量=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。
线程安全机制 :整个 acquire 和 tryAcquire 方法体都由 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 存储令牌曲线图
突发容量= 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 循环逻辑:
- 计算时间窗口 ID 和数组下标。
- 获取当前位置的
WindowWrap引用。 - 若为
null,则 CAS 新建窗口放入;若时间戳匹配,直接返回;若窗口过期,则尝试获取更新锁(updateLock,内部使用ReentrantLock)进行窗口重置,重置时更新windowStart和清空数据。 - 若竞争失败,通过
Thread.yield()让出 CPU,稍后重试,避免长时间自旋。
这种设计使得大部分统计操作(判断窗口是否匹配)无需锁,只在窗口跨过临界点时才会锁竞争,且竞争窗口极短。LongAdder 的 Cell 数组进一步减少了并发累加时的 CAS 冲突,高并发下性能远超 AtomicLong。
StatisticNode 持有两个 LeapArray<MetricBucket>:
rollingCounterInSecond:秒级滑动窗口(1s,sampleCount 默认 2)。rollingCounterInMinute:分钟级滑动窗口(60s,sampleCount 默认 12)。
当需要查询当前 QPS 时,调用 MetricBucket 的 pass() 方法累加当前窗口所有小窗口的值,即可获得精确的通过数量。
3.2 FlowRule 控制效果实现
FlowRuleChecker.checkFlow 方法根据 FlowRule.controlBehavior 选择不同的限流逻辑:
- CONTROL_BEHAVIOR_DEFAULT(直接拒绝) :若
passQps + acquireCount > count,则抛出FlowException,快速失败。适用于对响应时间敏感的核心接口。 - CONTROL_BEHAVIOR_WARM_UP(预热) :采用类似 Guava
SmoothWarmingUp的预热算法,内部维护一个令牌桶,根据warmUpPeriodSec和coldFactor计算当前速率。随着系统运行,桶内令牌逐渐消耗,通过速率从冷启动速率升至设定 QPS。 - CONTROL_BEHAVIOR_RATE_LIMITER(匀速排队) :实现漏桶算法,请求必须通过一个虚拟队列,以固定间隔通过。若队列等待时间超过
maxQueueingTimeMs,则拒绝。适用于处理突发流量但对延迟有一定容忍度的场景,如消息队列消费。
Sentinel LeapArray 环形数组滑动窗口机制图
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 实现时间窗口的无锁滑动和统计。
逐层/逐元素分解:
- 时间轴:每个小窗口具有固定的
windowStart和windowLength。 AtomicReferenceArray:存储两个窗口引用,下标由时间计算得出。- 窗口匹配:当前时间对应的窗口若已存在且未过期,直接复用。
- 窗口过期或缺失:通过 CAS 操作创建或重置窗口,失败则重试或 yield。
设计原理映射:无锁滑动窗口结合了"时间窗口"的统计精确性和"CAS"的高并发性能,避免了全局锁带来的瓶颈,是 Sentinel 高性能限流的基础。
工程联系与关键结论 :sampleCount 决定了统计平滑度与内存/CPU开销的平衡。 默认 2 个窗口在秒级统计中已经足够精确,若需要更细粒度监控,可增加样本数。每个 MetricBucket 的 LongAdder 保证了高并发下 pass/block 指标的累加几乎无竞争。
Sentinel FlowRule 三种 controlBehavior 控制流程图
动态计算当前速率] 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 实现网关层限流,核心组件是 KeyResolver 和 RedisRateLimiter。
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 脚本执行时序图
(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 三层限流架构
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 = 目标总 QPSburstCapacity = replenishRate × burstSeconds(建议burstSeconds=2~5s,允许合理突发)- 每秒补充令牌数 =
replenishRate
- Sentinel FlowRule :
- 单 Pod 限流 QPS =
目标总 QPS / Pod 数量,并预留 10%~20% 余量。 warmUpPeriodSec需大于 JVM 预热时长(通常 10~30s)。
- 单 Pod 限流 QPS =
- Guava SmoothWarmingUp :
permitsPerSecond = 单 Pod 限流 QPSwarmupPeriod建议与 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 故障演练与效果验证
故障演练效果: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,调整后对系统有何影响?resync在SmoothBursty中如何允许突发?
加分回答 :RateLimiter 设计哲学是"尽可能快的路径",synchronized 在短临界区下偏向锁/轻量级锁优化充分,无额外内存开销。预热公式本质是线性函数在梯形面积下的积分,反映了冷启动处理能力线性增长。可通过自定义 RateLimiter 的 SleepingStopwatch 实现更细粒度的等待控制。
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 预热,通过 storedTokens 和 lastFilledTime 动态计算阈值;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) 限流参数与工具选型
- 网关层 :
RedisRateLimiter,replenishRate=50000,burstCapacity=150000(突发3秒)。KeyResolver使用RemoteAddrKeyResolver或自定义 token 限流。 - 应用层 :Sentinel FlowRule,单 Pod
count=1200(50*1200=60000,留有 20% 余量),controlBehavior=WARM_UP,warmUpPeriodSec=20。maxQueueingTimeMs=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>的 GuavaRateLimiter.create(50000),TTL 60s。 - 多 Gateway 实例降级后各自限流 50000,总流量可能超限,但应用层每个 Pod Sentinel 限流 1200,整体受保护。
- Redis 恢复后自动切回,本地 Guava 过期回收。
(5) 含故障降级路径的架构图
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 钳制,保证不会雪崩。
时序图(故障降级):
关键设计决策:网关降级时采用本地 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 的 tryAcquire 与 acquire 在工程中如何选择?
一句话回答 :非关键路径用 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 代码
- Guava SmoothWarmingUp Bean 定义
java
@Configuration
public class RateLimiterConfig {
@Bean
public RateLimiter orderSeckillRateLimiter() {
// 单机 500 QPS,预热 10s
return RateLimiter.create(500, 10, TimeUnit.SECONDS);
}
}
- 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);
}
}
- 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
- Gateway 降级配置 (见第6节
FallbackRateLimiter代码示例)
延伸阅读
- Guava RateLimiter 源码 (
SmoothRateLimiter.java) - Sentinel 核心源码 (
LeapArray.java,FlowRuleChecker.java) - Spring Cloud Gateway 官方文档 RequestRateLimiter
- 《System Design Interview》 第4章 Rate Limiter 设计