Files
cs-note/hzh/TEST/分布式限流.md
T

380 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
create time: 2026-06-01 10:00
tags: [redis, rate-limiting, test]
type: quiz
---
# 分布式限流(基于 Redis)— 练习题
## 一、选择题(15 道)
**Q1.【场景题】** 你的服务部署在 5 台机器上,每台机器各自用内存计数器做限流(每秒 100 次)。用户反馈接口偶尔能跑到 600 QPS。根本原因是?
A. Redis 连接池配置过小
B. 多台机器各自独立计数,实际吞吐量 = 单机阈值 × 机器数
C. 数据库锁竞争导致请求堆积后爆发
D. API Gateway 没有启用限流插件
> [!info]- 参考答案
> B — 每台机器独立计数,100 × 5 = 500,加上边界突发可达 600
---
**Q2.** 固定窗口计数器在窗口边界处可能出现的突放量最多是限制阈值的几倍?
> [!tip]- 💡 术语:什么是突放量?
> 本应均匀通过的请求,因窗口切换缺陷在极短时间内集中挤过来,超过设定限制总量的多余流量。例:限制 100/s,交界 0.2s 内通过 200 个,突放量 ≈ 2×。
A. 1.5 倍
B. 2 倍
C. 3 倍
D. 与窗口大小 N 成正比
> [!info]- 参考答案
> B — 前后两个相邻窗口各满负荷运行,可在极短时间内通过双倍请求
---
**Q3.** 以下哪个 Key 的设计能确保在同一 Redis Cluster[^1] 下路由到同一个 hash slot[^2]?
A. `rate:limit:user123:daily`
B. `rate:limit:{user123}:daily`
C. `rate:limit/user123/daily`
D. `rate:limit-daily-user123`
> [!info]- 参考答案
> B — `{}` 包裹的路由键(Hash Tag)强制锁定到同一 slot
---
**Q4.** 日志级滑动窗口(Sorted Set[^3] 实现)在高并发场景下最大的缺点是什么?
A. 无法保证原子性
B. 空间复杂度 O(maxQPS × window),高频下 Sorted Set 过大
C. 不支持多实例部署
D. Lua 脚本执行时间过长
> [!info]- 参考答案
> B — maxQPS=1000、窗口 60s 需存储 6 万条记录,成为瓶颈
---
**Q5.** 令牌桶和漏桶最核心的区别在于:
> [!tip]- 💡 术语对比:令牌桶 vs 漏桶
> | 维度 | 🪣 令牌桶 | 🕳 漏桶 |
> |------|---------|-------|
> | **突发** | 允许(空闲时令牌累积) | 不允许(多余直接拒绝) |
> | **输出形态** | 跟随输入节奏 | 始终匀速 |
> | **隐含队列** | 无(拒绝即拒) | 有(先入桶排队再处理) |
> | **保护对象** | 对客户端更友好 | 对下游更安全 |
> | **典型场景** | REST API 限流、第三方配额 | DB 连接池限速、防止后端被打满 |
A. 令牌桶使用 Lua 脚本,漏桶不需要
B. 令牌桶允许合理突发,漏桶不允许
C. 令牌桶只能用于 API 网关,漏桶只能用于数据库
D. 令牌桶的速率不可调,漏桶的速率可调
> [!info]- 参考答案
> B — 令牌桶空闲时累积令牌允许突发;漏桶输出始终匀速
---
**Q6.【场景题】** 你负责一个付费 API 服务,需要限制每个用户每天最多调用 1000 次,希望精确统计且对精度要求高。最适合的算法是?
A. 固定窗口计数器
B. 滑动窗口计数器 (N=10)
C. 日志级滑动窗口(Sorted Set)
D. 令牌桶
> [!tip]- 💡 术语:滑动窗口计数器 vs 日志级滑动窗口
> - **滑动窗口计数器**:将总窗口划分为 N 个子窗口加权估算,近似统计总量。优点空间 O(N)、效率高;缺点精度受子窗口数量限制。适合日配额等对精度要求不高的场景。
> - **日志级滑动窗口**:每条请求的时间戳作为 Sorted Set 的成员逐条精确统计。优点是精确到每一条;缺点是空间 O(maxQPS × window),高频下 Sorted Set 规模过大。适合低频次高精度的场景。
> [!info]- 参考答案
> C — 日志级滑动窗口逐条精确统计,适合日配额等低频次高精度的场景
---
**Q7.【场景题】** 你正在为一个数据库连接池设计限速模块,目标是保护下游数据库不被瞬间大量请求打满。以下哪种方案最合适?
> [!tip]- 💡 术语:隐含队列
> 漏桶模型中包含一个"排队缓冲区"的概念——请求先被接收放入桶中,再由排水孔以恒定速率逐步处理。这相当于内置了一个 FIFO 队列,起到削峰填谷的作用。而令牌桶没有隐含队列,令牌不足时请求直接被拒绝。
A. 令牌桶——给前端好体验
B. 漏桶——稳定匀速输出,保护下游安全
C. 固定窗口计数器——实现最简单
D. 日志级滑动窗口——精度最高
> [!info]- 参考答案
> B — 漏桶输出始终匀速且有隐含队列,适合保护数据库连接池等下游资源
---
**Q8.** Redis Token Bucket Lua 脚本中,`math.min(capacity, tokens + elapsed * rate)` 这行代码的作用是什么?
A. 计算当前应该消费的令牌数
B. 根据经过的时间补充令牌,但不超过桶容量
C. 判断是否应该拒绝请求
D. 计算下次可用的等待时长
> [!info]- 参考答案
> B — 补充公式,elapsed 秒内应补充 elapsed×rate 个令牌,但上限为 capacity
---
**Q9.** 客户端收到 429 响应后,以下哪种做法是**错误的**?
A. 遵守 Retry-After 头部进行等待
B. 使用指数退避 + \_\_\_ 策略重试[^4]
C. 立即无脑重发所有失败的请求
D. 将 retry_after_seconds 写入本地计时器
> [!info]- 参考答案
> C — 立即无脑重发会造成 "惊群效应"(thundering herd),再次打垮刚恢复的服务
---
**Q10.【场景题】** 某监控告警显示 Redis CPU 飙升至 90%+,同时发现某个限流 Key 的 Sorted Set[^3] 有 50 万条成员。以下哪个是最优的解决方向?
A. 增大 Redis 服务器内存
B. 将滑动窗口日志(Sorted Set)切换为令牌桶方案
C. 增加滑动窗口的窗口大小
D. 在应用层用本地内存缓存计数结果
> [!info]- 参考答案
> B — Sorted Set 规模过大导致 CPU 飙升,切换到 O(1) 空间的令牌桶/漏桶是最佳解
---
**Q11.** 滑动窗口计数器(N 个子窗口加权估算[^5])中,如果当前时刻在第 8 个子窗口,前 7 个子窗口已过期的部分按什么方式折算贡献度?
A. 全部计入 100%
B. 全部忽略不计
C. 按剩余时间比例折算
D. 按已过期时间的线性函数衰减
> [!info]- 参考答案
> C — 已过期的窗口按其剩余未过期时间占总窗口大小的比例折算贡献度
---
**Q12.** Redis 使用 Lua 脚本来实现限流的核心原因是什么?
A. Lua 语言本身性能优于其他语言
B. Redis 只支持 Lua 作为扩展脚本
C. Lua 脚本原子化[^6]执行,避免多步骤操作产生竞态条件[^7]
D. Lua 脚本更容易被 Go/Java 等语言调用
> [!info]- 参考答案
> C — Redis 单线程模型 + Lua 原子执行,确保读 → 算 → 写不会被并发打断
---
**Q13.** 以下哪个 HTTP 头**不是**标准的限流响应头?
A. `X-RateLimit-Limit: 100`
B. `X-RateLimit-Remaining: 0`
C. `X-RateLimit-Reset: 1690000060`
D. `X-RateLimit-Burst: 50`
> [!info]- 参考答案
> D — 标准限流头包括 Limit(阈值)、Remaining(剩余)、Reset(重置时间戳),Burst 不是标准头
---
**Q14.** 生产环境性能优化中,使用 `EVALSHA`[^8] 替代 `EVAL` 的主要好处是什么?
A. EVALSHA 语法更简洁
B. EVALSHA 可以绕过 Lua 的沙箱限制
C. EVALSHA 无需重复传输脚本内容,节省带宽
D. EVALSHA 执行速度比 EVAL 快 10 倍以上
> [!info]- 参考答案
> C — 先用 SCRIPT LOAD 预加载脚本,后续 EVALSHA 仅传 sha1 哈希值,避免每次传输整个脚本字符串
---
**Q15.** 在多级限流架构的性能优化清单中,为什么推荐用 `SCAN`[^9] 替代 `KEYS` 命令?
A. SCAN 返回的数据更准确
B. KEYS 在数据量大时会阻塞 Redis 服务端,影响其他客户端
C. SCAN 支持更多的通配符匹配模式
D. KEYS 命令已被 Redis 官方废弃
> [!info]- 参考答案
> B — KEYS 是全库扫描的阻塞操作,大库时会严重影响线上;SCAN 分步游标迭代不阻塞
---
## 二、填空题(15 道)
**F1.** Redis 实现令牌桶 Lua 脚本时,使用 `HMSET` 命令写入两个字段:`tokens` 和 \_\_\_\_\_\_。
> [!tip]- 💡 术语:tokens 与 last_refill
> - `tokens`:当前桶中剩余的令牌数量,每次请求到来时检查并扣减。
> - `last_refill`:上一次补充令牌的时间戳,用于计算从上次到现在经历了多少秒,从而推算出这段时间新产生了多少个令牌。
> [!info]- 参考答案
> last_refill
---
**F2.** 被限流的 HTTP 响应应返回状态码 \_\_\_\_\_\_,并在 `Retry-After` 头部中给出建议等待秒数。
> [!info]- 参考答案
> 429
---
**F3.** 生产环境中推荐先通过 \_\_\_\_\_\_[^8] 预加载 Lua 脚本,再通过 EVALSHA 调用以节省带宽并提升性能。
> [!info]- 参考答案
> SCRIPT LOAD
---
**F4.** 当客户端连续多次收到 429 响应时,建议使用 **指数退避 + jitter**[^10] 策略进行重试,避免 "惊群效应"。
> [!tip]- 💡 术语:指数退避 + Jitter
> - **指数退避**:每次重试的等待时间按指数增长,如 `base × 2^attempt`(1s → 2s → 4s → 8s...),避免频繁重试加重服务器负担。
> - **Jitter(随机抖动)**:在指数退避的基础上加入随机偏移,如 `wait = min(base × 2^attempt + random_jitter, Retry-After)`,让不同客户端的重试时间错开,防止所有客户端在同一时刻同时涌入造成"惊群效应"。
> [!info]- 参考答案
> jitter(随机抖动)
---
**F5.** 多级限流架构中,L1 层针对 IP 维度通常采用 \_\_\_\_\_\_ 算法来防刷 / 防攻击。
> [!info]- 参考答案
> 短窗口固定计数(或:固定窗口计数器)
---
**F6.** 滑动窗口日志方案中,Lua 脚本使用 ZREMRANGEBYSCORE 清理过期记录,该操作的时间范围是从 `-inf` 到 \_\_\_\_\_\_。
> [!info]- 参考答案
> now - window * 1000
---
**F7.** 令牌桶初始有 100 个令牌。3 个请求依次到达,各消耗 1 个令牌,请求间隔可忽略不计(elapsed ≈ 0 无补充)。第 4 个请求检查时桶中还剩余 \_\_\_\_\_\_ 个令牌。
> [!info]- 参考答案
> 97
---
**F8.** Redis Key 中的 `id` 是限流维度的路由标识——若要对某个 IP 做限流,`id` 的实际取值应为 \_\_\_\_\_\_。
> [!tip]- 💡 术语:限流维度的路由标识(id)
> Key 中的 `id` 不是固定指某个业务字段,而是**限流维度的路由标识**——你想限制谁,它就等于谁的唯一标识。常见取:
> - **用户级**(L2)→ 用户 ID,如 `rate:limit:user_10086:1717132800`
> - **IP 级**(L1)→ 客户端 IP,如 `rate:limit:192.168.1.42:1717132800`
> - **接口级**(L3)→ 接口路径,如 `rate:limit:/api/register:1717132800`
> - **API Key 级**→ 客户端凭证,如 `rate:limit:app_key_xxx:1717132800`
> 生产环境中通常是**多级组合**:例如同时用 `userId + endpoint` 作为复合 Key。
> [!info]- 参考答案
> 客户端 IP
---
**F9.** 生产环境排查 "偶发性限流不一致" 问题时,应确认核心判断逻辑是否走单个 \_\_\_\_\_\_ 脚本。
> [!info]- 参考答案
> Lua
---
**F10.** 生产环境排查 "内存持续增长" 问题时,可通过 \_\_\_\_\_\_ 命令检查特定 Key 是否已过期(TTL)。
> [!info]- 参考答案
> TTL
---
**F11.** 分布式限流的核心思路是将多个服务实例各自的内存计数器集中到一个共享存储中,这个共享存储需要满足三个条件:原子操作一致性、\_\_\_\_\_\_、以及集群模式可横向扩展。
> [!tip]- 💡 术语:共享存储三要素
> 分布式限流的本质是让所有服务实例看到同一个计数值,因此共享存储必须满足:
> 1. **原子操作一致性**:自增、比较、写入必须是原子性的,避免多实例并发导致超发。
> 2. **低延迟**:每次请求都要查限流状态,额外的延迟不能超过毫秒级,否则限流模块会成为瓶颈。
> 3. **集群模式可横向扩展**:当限流 Key 数量增长时,存储系统需要能够水平扩容,不能出现单点瓶颈。
> [!info]- 参考答案
> 低延迟
---
**F12.** 滑动窗口计数器将总窗口划分为 N 个子窗口,当请求到达第 M 个子窗口时,之前的子窗口按 \_\_\_\_\_\_ 估算当前总量的加权贡献度。
> [!info]- 参考答案
> 剩余时间比例(或:时间占比)
---
**F13.** 令牌桶 Lua 脚本中,EXPIRE 设置 Key 的过期时间为 `ceil(capacity / rate) * 2`,这是一个防止 \_\_\_\_\_\_ 的保护机制。
> [!tip]- 💡 术语:僵尸 Key
> 不再有新请求但仍占用 Redis 内存的 Key。如果没有 EXPIRE,这些 Key 会永远存在,随着时间积累消耗越来越多的内存。`ceil(capacity / rate) * 2` 的含义是:填满整个桶所需的时间(capacity / rate)乘以 2,确保最后一次有活动的 Key 在足够长的时间后自动过期。
> [!info]- 参考答案
> 僵尸 Key(即不再有新请求但仍占用内存的 Key)
---
**F14.** 使用 Pipeline[^11] 减少网络 RTT[^12] 的方式是在限流操作中用 \_\_\_\_\_\_ 命令合并多个 Redis 操作为一个批量事务。
> [!info]- 参考答案
> MULTI/EXEC
---
**F15.** 生产排错中"超出限制大量通过"的症状,往往是因为不同服务器的时钟不同步导致 Lua 脚本中获取的 \_\_\_\_\_\_ 出现异常。
> [!tip]- 💡 术语:now(当前时间戳)
> Lua 脚本中的 `now` 是由应用服务器传入的毫秒级 Unix 时间戳(`time.Now().UnixMilli()`)。如果多台服务器的系统时间不同步,有的服务器 `now` 偏早、有的偏晚,会导致 Lua 脚本计算的 elapsed(elapsed = now - lastRefill)出现负值或异常大值,进而引发令牌补充错误、限流判断失效。排查方法:`date` 命令比对 Redis 和应用服务器的时间。
> [!info]- 参考答案
> now(当前时间戳)
---
## 关联笔记
### 核心文档
- [[REDIS/分布式限流]] — 本题库所属基础笔记,完整的算法演进路线
- [[REDIS/Lua脚本]] — Redis Lua 脚本基础概念与使用模式
### 按知识点索引
| 编号 | 知识点 | 对应文档 |
|------|--------|---------|
| Q3 / Cluster 陷阱 | Hash Tag 路由键 | [[REDIS/路由键与Hash Tag]] |
| Q4 / F6 | Sorted Set 滑动窗口 | [[REDIS/Sorted Set 滑动窗口]] |
| Q11 | 贡献度计算 | [[REDIS/滑动窗口计数器]] |
| Q12 / EVALSHA | Go-Redis Lua 调用 | [[REDIS/Go-Redis Lua 调用指南]] |
| Q13 | 限流 HTTP 响应头 | [[REDIS/限流HTTP响应头]] |
| Q14 | EVALSHA 预加载 | [[REDIS/EVALSHA预加载]] |
| Q15 | SCAN 遍历命令 | [[REDIS/SCAN命令]] |
| F4 | Jitter 抖动 | [[REDIS/Jitter抖动与重试策略]] |
| F5 | 多级限流 | [[REDIS/多级限流架构]] |
| F10 | 内存持续增长 | [[REDIS/内存持续增长排查]] |
| F13 | 僵尸 Key | [[REDIS/僵尸Key防护]] |
| F14 | Pipeline | [[REDIS/Pipeline批量操作]] |
| — | 滑动窗口日志细节 | [[REDIS/滑动窗口日志方案]] |
[^1]: **Redis Cluster**:Redis 的分布式集群模式,数据按照 hash slot(共 16384 个 slot)分布在不同节点上。
[^2]: **Hash Slot**:Redis Cluster 中的数据分片单位。多个 Key 如果要一起操作(如 Lua 脚本中的多 Key),它们必须落在同一个 hash slot。使用 `{}` 包裹的路由键(Hash Tag)可以强制锁定到同一 slot。
[^3]: **Sorted Set**:Redis 的一种数据结构,每个成员关联一个分数(score),集合自动按分数排序。常用于实现精确滑动窗口、排行榜等场景。
[^4]: **惊群效应(Thundering Herd)**:大量客户端在同一时刻同时发起请求,导致刚刚恢复的服务再次被打垮。通过 jitter 随机偏移可以让客户端的重试时间错开。
[^5]: **加权估算**:用多个子窗口的计数按比例估算当前总量,是一种近似而非精确的统计方式。N 越大越精确,但内存开销也越大。
[^6]: **原子性(Atomicity)**:一组操作要么全部执行、要么全部不执行,中间状态不会被其他客户端观察到。Lua 脚本在 Redis 中天然具有原子性。
[^7]: **竞态条件(Race Condition)**:多个并发操作交叉执行导致的结果不确定性问题。例如:读取计数 → 判断超限 → 写入新计数,如果这三步被并发打断,就会出错。
[^8]: **EVALSHA / SCRIPT LOAD**:`SCRIPT LOAD` 先将 Lua 脚本文本发送给 Redis,Redis 计算其 sha1 哈希值并缓存;`EVALSHA` 后续只需传递这个哈希值即可执行对应脚本,避免每次传输冗长的脚本文本。
[^9]: **SCAN**:Redis 提供的分步游标遍历命令,每次返回少量结果并返回新游标继续遍历。不会阻塞服务端,适合大数据量场景。与之对比的 `KEYS` 是一次性全量返回所有匹配 Key,大库时会严重阻塞。
[^10]: **指数退避 + Jitter**:重试策略的经典组合。指数退避防止过早重试浪费资源,jitter 防止大量客户端同时重试引发惊群效应。