周一早高峰,市中心地铁站迎来一天中最严峻的考验。A、B、C 三个入口同时涌进乌泱泱的人群,如果所有人毫无节奏地扑向闸机,站台会瞬间饱和,列车延误,甚至发生踩踏。地铁站的管理员是怎么做的?他们在安检口拉起了波浪护栏,让队伍一拨一拨地流动;闸机一次只放一个人;站台入口的电子屏实时显示站内人数;多个入口之间共享同一条客流规则。这场面,就是一套教科书式的分布式限流系统——乘客是请求,闸机是限流器,总控室里的共享台账就是 Redis 计数器。这篇文章不堆术语、不摆深奥理论,就带你把“分布式计数与限流”这出大戏,一站一站地看明白。学完之后你会发现:所谓高并发防护,不过是给系统装上一套聪明的“地铁闸机”。

岗位关联

限流是系统的“最后一道闸门”。掌握分布式计数与限流,你就能在流量洪峰下守住系统可用性,从被动救火变成主动设防——这是高并发系统架构师的必备基本功。

目录
  1. 可视化演示
  2. 1. 高并发之痛:流量洪峰下的崩溃危机
  3. 2. Redis原子计数:总控室的电子看板
  4. 3. 固定窗口限流:五分钟一波的人流管制
  5. 4. 滑动窗口限流:滚动统计堵住边界漏洞
  6. 5. 漏桶限流:匀速旋转的刚性闸机
  7. 6. 令牌桶限流:弹性放行的取票机
  8. 7. 分布式协同:多站点共写一本台账
  9. 8. 限流落地实践:从闸机到总控的完整防线
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示限流三模式:令牌桶(令牌补充/消费/通过拒绝)、窗口对比(固定窗口临界突发vs滑动窗口平滑计数)、分布式计数(Redis INCR单点原子/G-Counter CRDT多副本合并)。

本节配有交互动效(进入视口后自动加载,约 3KB)。

令牌桶限流算法 令牌以固定速率补充到桶中,桶满则丢弃多余令牌 请求到达时检查令牌:有则消费通过,无则拒绝 tokens = min(capacity, tokens + rate*dt) 补充80/s,通过率约75%

1. 高并发之痛:流量洪峰下的崩溃危机

地铁站最怕什么?怕所有人同时挤向同一个闸机口。没有分流护栏、没有分批放行,几百人怼在一起,闸机死机、旋杆折断、站台爆满,整个车站陷入瘫痪。互联网系统也一样:秒杀活动开始的瞬间,几十万请求像洪水一样涌向服务器,数据库连接池被瞬间占满,线程池耗尽,CPU 飙到 100%,新来的请求只能排队等待,超时后又变成更沉重的重试风暴——这就是流量洪峰之下的系统雪崩。

我们常用“可用性”来衡量系统扛住了多少冲击。它描述的是系统在统计周期内正常工作的时间占比。同时,整个集群的能力受限于最弱的一环,也就是“木桶效应”:CPU、内存、数据库连接、带宽,任何一个资源到达瓶颈,都会拖垮全局。限流要解决的,正是如何在资源到达瓶颈之前,把流量控制在一个系统能承受的范围。

A=UTUT+DT×100%A = \frac{UT}{UT + DT} \times 100\%

1. 正常运行时间:统计周期内系统保持可用的累计时长,用 UT 表示。

UT=Ttotal−DTUT = T_{\text{total}} - DT

2. 宕机时间:系统不可用时间的总和,包括崩溃、宕机、重启等,用 DT 表示。

DT=∑TdownDT = \sum T_{\text{down}}

3. 总时间:正常与不可用时间之和,构成整个统计周期。

Ttotal=UT+DTT_{\text{total}} = UT + DT

4. 计算可用性:用正常时间除以总时间再乘以 100%,得到可用性百分比。

A=UTUT+DT×100%A = \frac{UT}{UT + DT} \times 100\%

为什么要在意可用性?每提高一个“九”,意味着一年内允许宕机的时间从 8.76 小时(99.9%)降到 52.6 分钟(99.99%)。对地铁站来说,一条线路瘫痪 52 分钟就是大事故;对互联网公司来说,这意味着真金白银的订单流失。所以,必须在流量到达系统之前就为它设好闸、限好速。

2. Redis原子计数:总控室的电子看板

地铁站总控室里挂着一块电子看板,写着“当前站内人数:128”。每放行一个乘客,安检员按一下“+1“;每出去一个,按一下”-1“。看起来很简单,可如果几个安检员同时冲到看板前,都看到 128,都按下”+1”,看板会变成多少?理想情况是 131,可实际完全可能停留在 129——因为两次更新都基于同一个旧值 128,后写的人把先写的人覆盖了。少记了两个人,可就是安全隐患。

这就是分布式计数的核心难点:并发写冲突。在分布式系统里,几十个实例可能同时在更新同一个计数器,必须保证更新操作的“原子性”——就像看板按钮被设计成机械锁定,一个人按下时,其他人的按钮物理按不动。Redis 的 INCR 命令正是这样的“机械锁定按钮”:把读旧值、加一、写回三步捆绑成一个不可分割的整体,任何时刻只允许一个客户端完整执行。

Nnew=Nold+1, 全程原子执行N_{\text{new}} = N_{\text{old}} + 1,\ \text{全程原子执行}

1. 旧计数值:Redis 中当前保存的数字,是本次操作的起点。

Nold=value(key)N_{\text{old}} = \text{value}(\text{key})

2. 加一运算:在旧值基础上执行加 1,得到中间结果。

Nold+1N_{\text{old}} + 1

3. 原子写回:加一后的新值立即覆盖旧值,整个过程不被其他命令打断。

Nnew←Nold+1N_{\text{new}} \gets N_{\text{old}} + 1

有了这块电子看板,地铁站不仅能统计站内人数,还能顺手做很多事:给每个乘客发一个“当日进站序号”,这就是分布式的发号器;把每个入口的放行人数加在一起,这就是最原始的窗口计数。计数稳稳当当,限流的大楼才有了地基。

3. 固定窗口限流:五分钟一波的人流管制

有了电子看板,地铁站开始定规矩:以 5 分钟为一个“窗口”,这个窗口内最多放行 500 人。每进来一个人,看板 +1;一旦到了 500,闸机立即锁死;5 分钟结束,看板清零,重新放 500 人。简单、直观、极其好用——这就是固定窗口限流,它是所有限流算法里最容易理解的一种。

判断逻辑也非常直白:窗口内计数小于阈值就放行,大于等于就拒绝。它不需要记录每个请求的精确时间,只需要一个不断累加的计数器和一个到点清零的定时器,所以在 Redis 里实现起来成本极低。

allow={true,count<limitfalse,count≥limit\text{allow} = \begin{cases} \text{true}, & \text{count} < \text{limit} \\ \text{false}, & \text{count} \geq \text{limit} \end{cases}

1. 当前计数 count:本窗口内已经放行的请求数量。

count=窗口内累计请求数count = \text{窗口内累计请求数}

2. 阈值 limit:管理者预先设定的窗口放行上限。

limit=500 requests/5minlimit = 500\ \text{requests}/\text{5min}

3. 未达阈值:count 小于 limit 时,请求被放行。

true: count<limit\text{true}:\ count < limit

4. 达到阈值:count 达到或超过 limit 时,请求被拒绝。

false: count≥limit\text{false}:\ count \geq limit

固定窗口看起来完美,却藏着一个致命漏洞:边界突刺。假设 9:59:59 的窗口已有 499 人,最后一秒又放进 1 人;10:00:00 新窗口开始,又有 500 个新名额。从 9:59:59 到 10:00:01,短短两秒内最多可能涌进 1000 人,比平时多了一倍。地铁站如果这样设计,站台在窗口交界处依然会瞬间爆满——“固定的时间块”本身就是漏洞的来源。

4. 滑动窗口限流:滚动统计堵住边界漏洞

为了堵上边界突刺,地铁站的统计方式升级了:不再用“固定 5 分钟一块”的板子,而是把看板改成“永远统计过去 5 分钟内的总人数”,并且每秒滚动刷新一次。10:00:01 时看的是 9:55:01 到 10:00:01 的进站人数;到了 10:00:02,看板就自动平移为 9:55:02 到 10:00:02。窗口永远在滑动,任何一秒回头看,数据都是最近 5 分钟的真实流量,再也不存在“两个固定窗口交界处”的漏洞。

滑动窗口的常见实现有两种。日志法:每进来一个请求就记下当前时间戳,判断时把窗口外的时间戳删掉,剩下数量就是窗口内请求数;细分窗口法:把 5 分钟切成 60 个小格子,每格 5 秒,用一个环形数组滚动累加。两种方法各有取舍:日志法精度高但存储开销大,细分窗口法速度快但精度受格子数限制。

C(t)=∑i=1nci(t)C(t) = \sum_{i=1}^{n} c_i(t)

1. 小格子计数:每个时间小格内记录的请求数,用 cᵢ 表示。

ci(t)=第 i 格的请求数c_i(t) = \text{第 } i \text{ 格的请求数}

2. 格子数量:窗口长度除以格子长度,决定了统计精度和内存开销。

n=TwindowTslotn = \frac{T_{\text{window}}}{T_{\text{slot}}}

3. 求和累加:把窗口内所有格子的计数相加。

∑i=1nci(t)\sum_{i=1}^{n} c_i(t)

4. 滚动窗口总数:得到当前时刻往前推一个窗口的总请求数。

C(t)=过去一个窗口的总请求数C(t) = \text{过去一个窗口的总请求数}

滑动窗口用“滚动视角”解决了边界突刺,但并非没有代价:它需要更大的存储和更多的计算。地铁站要为每一秒的数据都保留一个统计格;系统里每多一个接口的限流,就要多一组时间格数组。这是典型的“用空间换精准”。

5. 漏桶限流:匀速旋转的刚性闸机

固定窗口和滑动窗口都在回答“这段时间内放多少人”,漏桶却换了一个思路:不管外面涌来多少人,我每分钟就匀速放那么多人出去,就像一只底部开了小口的桶——水以恒定的速度一滴一滴漏出去。桶里的水就是排队的请求,桶的容量就是最大等待数。请求来了先进桶排队,桶按固定速率放行;桶满了,再来的请求直接倒掉。

漏桶看起来和滑动窗口一样能限流,但本质截然不同:滑动窗口控制的是“总量”,漏桶控制的是“速率”。它最大的价值是削峰——把突发的流量脉冲变成平滑的匀速流。对下游的数据库和外部接口来说,匀速是最友好的输入:不会冷不丁被一波 1000 QPS 打挂。代价也很明显:它毫无弹性,就算系统当前很空闲,漏桶依然按老牛拉车的速度放行,突发流量一律排队,用户体验可能受损。

v=1Δtv = \frac{1}{\Delta t}

1. 放行间隔:每放出一个请求需要等待的固定时间间隔。

Δt=相邻放行时间间隔\Delta t = \text{相邻放行时间间隔}

2. 恒定速率:每秒流出的请求数等于放行间隔的倒数。

v=1Δtv = \frac{1}{\Delta t}

3. 输入不影响输出:无论请求多猛烈,出口速率保持不变。

vout=constv_{\text{out}} = \text{const}

4. 桶满即丢:排队容量耗尽时,新请求被直接丢弃。

request→{排队,q<cap丢弃,q=cap\text{request} \to \begin{cases} \text{排队}, & q < cap \\ \text{丢弃}, & q = cap \end{cases}

用一句话总结漏桶:它像一个固定口径的水龙头,无论上游水管里是涓涓细流还是滔天洪水,流进你杯子的水永远一样快。如果你需要绝对稳定的下游输出,漏桶是最可靠的选择。

6. 令牌桶限流:弹性放行的取票机

漏桶太“死板”了,即便系统很闲也不肯多放人。令牌桶算法则聪明地兼顾了稳定与弹性:地铁站闸机旁多了一台取票机,它每秒钟自动吐出一张“进站票”放进票盒,票盒最多放 10 张;乘客进站前必须先取一张票投进闸机。平时人流少,票盒里渐渐攒满 10 张;突然来一大波人,大家可以在几秒内把攒下的票全部用完,实现短时“放行加速”;票用完后,就只能按每秒一张的节奏进站。

这就是令牌桶的精髓:系统空闲时积攒令牌,突发时一次性消耗。判断是否放行时,先补充这段时间新长出来的令牌,再看桶里够不够用。令牌桶用两个参数分别控制两种性格:速率 r 决定长期平均吞吐,容量 C 决定短时突发上限。

T=min⁡(C, T0+r⋅Δt)T = \min\left(C,\ T_0 + r \cdot \Delta t\right)

1. 旧令牌数:上次补充后桶内剩余的令牌数量,用 T₀ 表示。

T0=桶中已有令牌T_0 = \text{桶中已有令牌}

2. 新增令牌:按速率 r 在 Δt 秒内生成的新额度。

r⋅Δtr \cdot \Delta t

3. 合并累计:旧令牌加新令牌,得到理论上的令牌总量。

T0+r⋅ΔtT_0 + r \cdot \Delta t

4. 容量封顶:超过桶容量 C 的部分被丢弃,令牌不会无限积攒。

min⁡(C, T0+r⋅Δt)\min\left(C,\ T_0 + r \cdot \Delta t\right)

秒杀场景偏爱令牌桶,因为它让系统在保护自己的同时,还能“配合演出”地扛住开抢瞬间的天量流量,而不是把所有人都拒之门外。r 大、C 小,系统平稳少波动;r 小、C 大,则能承受更强的瞬时冲击——调参,就是在稳定与弹性之间寻找平衡。

7. 分布式协同:多站点共写一本台账

当城市里多个地铁站共享同一本客流台账时,新问题出现了:A 站安检员和 B 站安检员同时看到“当前人数 500”并按下“+1“,看板真的会变成 502 吗?单块看板上的机械锁机制,在跨站点场景下还能生效吗?答案是:可以,但前提是把”读写同一块看板”变成唯一的权威路径。在分布式系统里,这个权威路径就是 Redis——所有实例都读写同一个 key,谁也别想在本地私自记账。

但这还不够。多实例同时执行“读计数、判断、写回”时,即使每个 INCR 都是原子的,组合操作依然可能被穿插。解决办法是用 Lua 脚本把整套逻辑打包,在 Redis 服务器端一次性执行,执行期间不会被其他命令打断——相当于把“看板操作规则”写成了不可撕毁的一页纸,谁来了都只能照章办事。另一方面,Redis 主从架构也带来一致性风险:主节点宕机时,未同步到从节点的计数会丢失;从节点在同步延迟期可能读到旧值。要求更高时,可以引入共识协议,保证“大多数节点都确认”才算成功。quorum(法定人数)机制就是如此。

Q=⌊n2⌋+1Q = \left\lfloor \frac{n}{2} \right\rfloor + 1

1. 节点总数 n:参与共识决策的集群节点数量。

n=集群节点数n = \text{集群节点数}

2. 半数向下取整:对 n/2 向下取整,得到不超过半数的最小整数。

⌊n2⌋\left\lfloor \frac{n}{2} \right\rfloor

3. 加一形成多数:在半数的基础上加 1,保证严格超过 50%。

Q=⌊n2⌋+1Q = \left\lfloor \frac{n}{2} \right\rfloor + 1

4. 决策阈值:至少 Q 个节点确认,写入结果才能生效。

commit needed≥Q\text{commit needed} \geq Q

分布式协同的核心思想一句话:计数不是“A 站说了算”,而是“多个站点按同一套规则达成一致”。Redis 提供权威存储,Lua 保证操作原子,quorum 机制保证共识,三者叠加,分布式限流才有了真正可信的“总台账”。

8. 限流落地实践:从闸机到总控的完整防线

光在入口放一台令牌桶就万事大吉了吗?地铁站不会只依靠一道闸机:安检口外有波浪护栏做“粗限流”,站厅入口有电子看板做“分区统计”,站台门口还有工作人员手持喇叭动态调整放行速度。互联网系统同样需要多级防线——把限流部署在每一层,而不是只堵一个点。

典型的落地是分层的:最外层是 Nginx/API 网关,按来源 IP、用户 ID、接口路径做全局粗粒度限流,挡住洪水的大部分;中间业务服务层针对核心接口设置细粒度限流,比如下单接口 500 QPS、查询接口 2000 QPS;再配合监控系统实时观测计数器和系统负载,当发现某个实例异常,自动调低阈值或触发熔断降级。多层限流叠加后,最终生效的阈值取各层的最小值——就像地铁站里“安检只放 100 人”和“站台只收 80 人”同时作用,实际进站人数绝不会超过 80。

Leff=min⁡(Lgw, Lsvc)L_{\text{eff}} = \min\left(L_{\text{gw}},\ L_{\text{svc}}\right)

1. 网关阈值:入口网关层允许的最大请求速率,用 L_gw 表示。

Lgw=网关层限流值L_{\text{gw}} = \text{网关层限流值}

2. 服务阈值:业务服务层为保护核心资源设置的限流值,用 L_svc 表示。

Lsvc=服务层限流值L_{\text{svc}} = \text{服务层限流值}

3. 取最小值:最终生效的限流阈值,是两层中较严的一档。

Leff=min⁡(Lgw, Lsvc)L_{\text{eff}} = \min(L_{\text{gw}},\ L_{\text{svc}})

4. 互为兜底:即使某一层被绕过,另一层仍能保护系统不被冲垮。

从一块简单的电子看板,到固定窗口、滑动窗口、漏桶、令牌桶,再到 Redis + Lua + quorum 的分布式协同,最后到多级限流的完整防线——我们要做的,就是给系统装上一套聪明的地铁闸机系统。限流看似是“拒绝”,本质是“有管理地放行”。下一次流量洪峰来临时,你不再是站在站台中央慌张疏散人群的站长,而是坐在总控室里从容调度各入口闸机的指挥官。

AI 应用场景

场景一:电商秒杀系统的入口守护

阶段:系统架构设计 / 容量保障

解决的问题:秒杀开抢瞬间流量是平时的几百倍,若全部放行,订单服务、库存服务和数据库会被瞬间打满,导致下单超时、库存超卖甚至全站宕机。用令牌桶为秒杀接口设置弹性限额,允许开抢时消耗积攒的令牌,同时用漏桶保护写库链路,保证订单按恒定速率落地。

具体案例:某电商在大促时,网关层对秒杀接口配置 10 万 QPS 令牌桶,订单服务配置 5000 QPS 漏桶;Redis 计数与 Lua 脚本保证多机房实例共享同一份配额,页面通过排队进度条安抚用户,最终扛住了流量洪峰,交易成功率保持在 99.95%。

场景二:春晚红包与热搜的流量洪峰

阶段:线上稳定性治理 / 故障演练

解决的问题:热点事件瞬间制造巨大读流量,直接用数据库扛会引发缓存击穿和雪崩。用滑动窗口对读接口做精细限流,按窗口滚动的视角统计真实流量,在保证用户体验的同时保护下游资源。

具体案例:某内容平台在热搜爆发时,对 feed 接口采用滑动窗口限流(窗口 1 秒、格子 10 毫秒),并在 Redis 中共享窗口计数,保证多实例行为一致;同时埋点监控被限流比例,当限流比例超过 5% 时自动扩容或降级非核心功能。

场景三:开放平台 API 配额管理

阶段:多租户资源治理 / 流量运营

解决的问题:不同企业客户的 API 调用配额不同,需要为每个 client_id 维护独立的计数和限流阈值。用 Redis INCR 计数 + Lua 脚本判断配额,保证多个 API 网关实例共享同一份配额且不会被并发写乱。

具体案例:某开放平台为每个客户分配“每分钟 100 次”的配额,使用 Redis INCR + Lua 实现原子判断,超限返回 HTTP 429 并附带重试等待时间,既保证了租户间的公平性,又防止单个客户拖垮整体服务。

Python 演示

python61 行
import time  class TokenBucket:    """令牌桶限流器:每秒补充 rate 个令牌,桶容量封顶"""    def __init__(self, capacity, rate):        self.capacity = capacity        self.rate = rate        self.tokens = capacity        self.last_refill = time.time()     def _refill(self):        now = time.time()        self.tokens = min(self.capacity, self.tokens + (now - self.last_refill) * self.rate)        self.last_refill = now     def allow(self, n=1):        self._refill()        if self.tokens >= n:            self.tokens -= n            return True        return False  class SlidingWindowLimiter:    """滑动窗口限流器:基于请求时间戳日志"""    def __init__(self, window_seconds, max_requests):        self.window_seconds = window_seconds        self.max_requests = max_requests        self.requests = []     def allow(self):        now = time.time()        self.requests = [ts for ts in self.requests if now - ts < self.window_seconds]        if len(self.requests) >= self.max_requests:            return False        self.requests.append(now)        return True  def demo():    bucket = TokenBucket(capacity=5, rate=2)    print("=== 令牌桶:每 0.2s 发一个请求 ===")    for i in range(10):        time.sleep(0.2)        print(f"请求{i + 1}: {'放行' if bucket.allow() else '拒绝'}")     burst = TokenBucket(capacity=5, rate=1)    print("\n=== 令牌桶:前两秒突发 8 个请求(攒了 5 个令牌)===")    for i in range(8):        print(f"请求{i + 1}: {'放行' if burst.allow() else '拒绝'}")     window = SlidingWindowLimiter(window_seconds=5, max_requests=3)    print("\n=== 滑动窗口:5 秒窗口限 3 个请求,每秒发 1 个 ===")    for i in range(5):        time.sleep(1)        print(f"请求{i + 1}: {'放行' if window.allow() else '拒绝'}")  if __name__ == "__main__":    demo()
目的:用纯 Python 实现令牌桶和滑动窗口两种限流器,通过模拟请求直观对比“弹性突发”与“滚动窗口”的放行行为。
  1. self.tokens = min(self.capacity, self.tokens + (now - self.last_refill) * self.rate)按流逝时间补充令牌,超出容量则封顶,这就是令牌桶的核心公式
  2. if self.tokens >= n:有足够令牌才放行,消耗后立即扣减
  3. self.requests = [ts for ts in self.requests if now - ts < self.window_seconds]只保留窗口内的时间戳,实现“窗口随时间滑动”的效果
  4. if len(self.requests) >= self.max_requests:窗口内请求已达上限则拒绝
  5. burst = TokenBucket(capacity=5, rate=1)初始令牌就是满的 5 个,所以突发前 5 个请求瞬间放行,第 6 个开始拒绝
  6. TokenBucket(capacity=5, rate=2)容量 5、每秒补充 2 个令牌,空闲时积攒能力决定了突发时的弹性

常见误区

误区限流会把用户请求直接丢掉,用户体验变差,所以限流阈值应该尽量调高,最好永远用不到。

正解限流像地铁站的波浪护栏,它不会让乘客的旅程消失,只是让他们晚一点到达。被限流的请求可以排队、重试或降级返回,换来的是绝大多数用户请求依然稳定可用。真正的问题不是“用了限流”,而是“该限流时没限流,结果全站宕机”。

为何常见:把“被限流”等同于“请求失败”,忽略了在系统过载时,拒绝少数请求恰恰是在保护更多请求的成功。

误区边界突刺是因为窗口太长,把窗口从 60 秒改成 10 秒,突发翻倍的时间窗就变小了,问题自然就弱化了。

正解边界突刺的本质是固定窗口的“重置效应”,无论窗口多短,只要窗口是固定的,相邻两个窗口交界处都可能涌入 2 倍流量。唯一彻底解决的办法是换用滑动窗口,让窗口边界永远在滚动。

为何常见:容易被“改小窗口 = 更精确”的直觉误导,没有理解突刺的根源在于窗口重置本身,而不是窗口大小。

误区两者都能把流量限制在某个速率内,效果差不多,选一个实现简单的就行。

正解漏桶强制匀速输出,绝对平滑,但系统空闲时也不能加速处理积压;令牌桶允许桶内积攒的令牌被一次性消耗,能短时突发,体验更好。保护数据库等脆弱下游选漏桶,兼顾用户体验和系统保护选令牌桶。

为何常见:只看到“限流”二字,忽略了它们对突发流量的态度截然不同——一个削峰,一个蓄峰。

误区INCR 命令本身是原子操作,所以只要用 INCR 计数,主从切换、超时重试都不会出问题。

正解INCR 的原子性只保证单条命令内部不出错。主从复制延迟时,从节点可能是旧值;主节点宕机切换后,未同步的计数会丢失;客户端超时重试还可能导致同一请求被计两次。需要结合业务对一致性的要求,配合 Lua 脚本、幂等键或共识协议来解决。

为何常见:把“原子性”理解成了“完整的一致性保证”,混淆了单机原子性与分布式一致性这两个层次的问题。

面试题

Q1. 令牌桶限流最核心的特点是?

单选题 · easy

  • A.请求以恒定速率被处理
  • B.允许一定程度的突发流量← 正确答案
  • C.令牌生成后永不消失
  • D.拒绝请求立即恢复计数

答案:B。令牌桶在系统空闲时积攒令牌,突发时可以一次性消耗,所以允许短时突发。A 是漏桶的特点;C 错误,令牌受容量上限约束;D 是对行为的误解,被拒绝不会立即恢复计数。

Q2. Redis 的 INCR 命令用于分布式计数的最大优势是?

单选题 · easy

  • A.执行速度快于普通命令
  • B.原子性保证并发不错乱← 正确答案
  • C.支持事务回滚的语义
  • D.自动生成全局唯一的ID

答案:B。INCR 把“读-加-写”捆绑成一个原子操作,并发环境下不会出现覆盖写,这才是它适合做分布式计数的根本原因。A 不是决定性优势;C 错误,Redis 事务不保证回滚;D 只是 INCR 的衍生用途。

Q3. 固定窗口限流的“边界突刺”是指?

单选题 · mid

  • A.窗口计数超过阈值
  • B.边界前后涌入两倍流量← 正确答案
  • C.窗口内请求被丢弃
  • D.计数器溢出导致崩溃

答案:B。两个固定窗口交接的瞬间,前一个窗口的剩余配额和新窗口的满额配额可以接连使用,短时间内最多涌入接近 2 倍阈值。A 是限流正常生效;C 描述不清;D 是另一个问题。

Q4. 下列属于滑动窗口限流实现思路的是?

多选题 · hard

  • A.记录请求的时间戳日志← 正确答案
  • B.用令牌填充满窗口消耗速度
  • C.将窗口拆分为多个小窗口← 正确答案
  • D.每请求重置整个窗口计数

答案:AC。日志法记录每个请求的时间戳并滚动清理,是滑动窗口的经典实现;细分窗口法把大窗口拆成小格子滚动求和,也是常见实现。B 是令牌桶的思路;D 会破坏窗口的连续性,属于错误实现。

Q5. 漏桶限流最典型的特征是?

单选题 · easy

  • A.允许瞬时突发流量
  • B.输出速率恒定不变← 正确答案
  • C.输入越快输出越快
  • D.桶空时请求全部拒绝

答案:B。漏桶的出口速率是固定的,无论输入多么猛烈,输出永远匀速。A 是令牌桶的特征;C 恰恰相反;D 错误,桶空只是没有积压,新请求仍可正常流出。

Q6. 分布式限流与单机限流的本质区别是?

单选题 · mid

  • A.分布式限流吞吐量更高
  • B.计数状态需要跨节点共享← 正确答案
  • C.分布式限流没有时间窗口
  • D.分布式限流不产生延迟

答案:B。分布式限流的核心难点在于多个实例必须共享同一份计数状态,否则各实例各自计数会导致总流量失控。A 不是必然的;C、D 与分布式限流实现无关。

Q7. 关于 Redis 主从架构计数一致性问题,下列说法正确的有?

多选题 · extreme

  • A.主节点切换可能丢失未同步计数← 正确答案
  • B.从节点可能读到过期的计数值← 正确答案
  • C.Lua 可彻底解决主从延迟问题
  • D.合理配置可降低不一致风险← 正确答案

答案:ABD。主从复制是异步的,主节点宕机时未同步的计数会丢失,切换期间从节点也可能读到旧值,所以 A、B 正确。Lua 只能保证单条脚本的原子性,无法消除主从复制延迟,C 错误。通过合理配置主从策略、配合降级兜底,可以降低不一致带来的影响,D 正确。

Q8. 秒杀场景选令牌桶而非漏桶,主要原因是令牌桶?

单选题 · mid

  • A.代码实现相对更简单
  • B.能响应短时突发流量← 正确答案
  • C.不需要中心化存储
  • D.可以杜绝商品超卖

答案:B。秒杀开抢的瞬间流量最猛,令牌桶可以把空闲时积攒的令牌一次性消耗,让系统“扛住”这波突发。A 不成立,两者实现难度相当;C 错误,分布式限流都要中心化存储;D 与限流无关,超卖要靠库存一致性解决。

Q9. 多实例限流时,为了保障计数正确,以下做法合理的有?

多选题 · mid

  • A.读写都走 Redis 主节点← 正确答案
  • B.使用 Lua 脚本保证原子性← 正确答案
  • C.每次请求都查从节点提升性能
  • D.配合幂等设计降低重试误差← 正确答案

答案:ABD。强一致场景下读写主节点能拿到最新计数,A 正确;Lua 脚本把“读判写”打包成原子操作,B 正确;从节点有同步延迟,读到的可能不是最新计数,会影响限流精度,C 错误;超时重试可能造成重复计数,用幂等键可以规避,D 正确。

Q10. 用滑动窗口限流一个读多写少的接口,最合理的存储选择是?

单选题 · hard

  • A.时间戳入消息队列异步处理
  • B.计数存 Redis 并定期清理过期← 正确答案
  • C.仅存服务本地内存数组
  • D.写数据库再全量统计

答案:B。多实例共享计数必须用 Redis,滑动窗口数据存 Redis 并定期清理过期时间戳是标准做法。A 异步处理会让限流判断滞后,失去意义;C 只在单机生效;D 全量写库统计太重,无法支撑高并发。