如何设计一个分布式限流器?
简化版
分布式限流器要在多个服务实例之间共享限流状态,常见方案是 Redis + Lua 原子计数、集中式限流服务、网关统一限流或本地令牌预分配。设计时要权衡精确性、性能、可用性、中心依赖、降级策略和多维度规则管理。
详细版
设计分布式限流器要考虑:
- 限流维度:接口、用户、IP、租户、AppKey、下游资源;
- 算法选择:固定窗口、滑动窗口、令牌桶、并发数;
- 原子性:多实例并发判断和扣减必须一致,常用 Redis Lua;
- 性能:限流器不能比业务接口更慢,要关注网络 RTT 和 Redis 压力;
- 可用性:Redis 或限流服务故障时,是 fail-open 还是 fail-close;
- 配置治理:规则动态生效、灰度、回滚、审计;
- 监控告警:通过量、拒绝量、热点 key、限流耗时、错误率。
面试回答时可以给出典型流程:请求进入网关,生成限流 key,执行原子脚本判断额度,通过则转发,拒绝则返回 429 或业务降级,同时记录指标。
完整版教学
一、分布式限流解决的是多实例共享额度问题
单机限流只控制本实例流量。假设一个接口总额度是 1000 QPS,服务部署 10 台,如果每台本地限 100 QPS,理论上总量是 1000。但实际负载均衡不一定均匀,有的实例 50 QPS,有的 180 QPS;如果每台都限 100,流量不均时会误伤部分请求,也可能总量控制不准。
分布式限流要让多个实例围绕同一个全局额度做决策。比如某租户每秒最多 500 次请求,无论请求落到哪个实例,都要共享这 500 次额度。这就需要中心化状态,或者把额度按某种策略分配到各实例。
全局越精确,中心协调越重;越去中心化,误差越大但性能和可用性更好。这是分布式限流设计的主矛盾。
二、Redis + Lua 是最常见的实现方式
Redis 适合做分布式限流,因为它单线程执行命令,延迟低,支持过期时间和 Lua 脚本。Lua 可以把“读取计数、判断阈值、递增计数、设置过期”放在一个原子操作里,避免并发超放。
固定窗口的 Lua 思路如下:
current = INCR key
if current == 1:
EXPIRE key windowSeconds
if current <= limit:
allow
else:
reject
令牌桶或滑动窗口也可以用 Lua 实现,只是状态更多。令牌桶要保存当前令牌数和上次补充时间;滑动日志要操作 ZSet 删除过期记录并计数。
Redis 方案的问题是每次请求都可能访问 Redis。高 QPS 下 Redis 会成为瓶颈,网络 RTT 也会增加接口耗时。因此要考虑本地缓存、批量申请令牌、热点 key 拆分或在网关层集中处理。
三、集中式限流服务和网关限流的区别
集中式限流服务把限流逻辑封装成独立服务,业务服务调用它判断是否放行。好处是规则统一、语言无关、治理集中;缺点是多一次 RPC,限流服务自身要高可用,否则会影响所有业务。
网关限流把限流放在入口层。请求还没进入后端服务就被拦截,可以节省后端资源,适合 API、用户、IP、租户、路径维度限流。缺点是网关不一定了解服务内部资源状态,比如某个数据库分片热了、某个下游支付渠道慢了,网关未必知道。
服务内限流更贴近业务和资源,可以按库存、下游、线程池、用户状态做细粒度控制。成熟系统通常网关做第一层粗限流,服务内做第二层精细保护。
四、本地预分配令牌能降低中心压力
如果每个请求都访问 Redis,中心压力可能很大。一个折中方案是本地预分配令牌。比如全局每秒 10000 个令牌,限流服务按实例权重给每个节点分配一批本地令牌,节点在本地快速扣减,用完再申请。
这种方案性能好,但精确性下降。某个节点拿到令牌却没有用完,另一个节点令牌不够,可能出现额度浪费。节点宕机时,已分配未使用令牌也会丢掉一个窗口内的额度。可以通过小批量、短租约、动态再分配降低误差。
这类设计体现了分布式系统常见取舍:为了性能和可用性,接受一定统计误差。
五、限流器故障时要选择 fail-open 还是 fail-close
如果 Redis 或限流服务不可用,业务请求怎么办?fail-open 是限流器故障时放行请求,优点是可用性高,缺点是可能失去保护;fail-close 是限流器故障时拒绝请求,优点是保护严格,缺点是限流系统故障会导致业务不可用。
选择要看业务场景。普通查询接口更倾向 fail-open,避免限流器成为单点;短信验证码、支付风控、开放平台计费接口可能更倾向 fail-close 或使用本地兜底额度,防止绕过限制。
更稳的方案是多级降级:中心限流不可用时,退化为本地限流;本地限流使用保守阈值;同时告警。这样既不完全失控,也不因为中心故障全拒绝。
六、多维度规则会带来组合复杂度
真实限流往往不是一个规则。可能同时有全站 10 万 QPS、接口 5000 QPS、租户 1000 QPS、用户 10 QPS、IP 100 QPS。一个请求要经过多条规则判断,任意一条不通过都拒绝。
规则组合要注意顺序和成本。可以先判断低成本本地规则,再判断高成本全局规则;先判断粗粒度规则,再判断细粒度规则。规则 key 要避免爆炸,比如接口 + 用户 + IP + 设备 + 地区组合可能产生大量 key。
配置治理也很重要:谁能改规则,规则何时生效,是否支持灰度,改错能否回滚,是否有审计记录。限流规则改错可能造成大面积拒绝或放开攻击流量。
七、面试追问中的系统设计闭环
如果面试官让你设计限流器,可以按闭环回答:入口解析请求生成限流 key;规则中心下发限流配置;执行器根据算法做原子判断;通过请求继续执行,拒绝请求返回业务语义结果;指标系统记录通过、拒绝和耗时;配置系统支持灰度和回滚;限流依赖故障时有本地兜底。
还要补充容量:Redis 集群如何分片,热点 key 如何处理,Lua 脚本耗时如何监控,限流 key TTL 如何设置,是否会产生大量冷 key。这样答案从“会用 Redis”升级成“能设计生产可用的限流系统”。
八、常见误区与追问
这道题要紧扣「分布式限流器设计」本身回答,不能把它混成泛泛的流量控制套话。面试官通常不是只听定义,而是看你能不能把适用场景、关键流程、失败边界和工程取舍串起来,尤其要说明限流位置、算法窗口、阈值来源、拒绝策略、退避降级和观测指标。
| 回答层次 | 要讲清的内容 | 容易漏掉的边界 |
|---|---|---|
| 核心结论 | 分布式限流器要在多实例间共享计数或令牌,保证全局阈值不被各节点本地计数放大 | 不要停在名词解释 |
| 流程机制 | 确定全局阈值 -> 选择 Redis 或本地配额 -> 原子扣减令牌 -> 返回通过或拒绝 -> 同步监控指标 -> 故障时降级策略 | 要说清触发点、状态变化、确认点和失败兜底 |
| 工程取舍 | 10 个网关实例各自限 1000 QPS,如果只做本地限流,全局可能放到 10000 QPS | 限流保护系统稳定性,但会牺牲一部分请求成功率或响应实时性 |
分布式限流器设计 面试拆解:
1. 确定全局阈值
2. 选择 Redis 或本地配额
3. 原子扣减令牌
4. 返回通过或拒绝
5. 同步监控指标
6. 故障时降级策略
记忆钩子:先给结论,再拆流程,再讲数字例子和失败边界;回答「分布式限流器设计」时要围绕题目问法收束,不要把相邻概念堆成一段没有重点的名词清单。
- 误区:限流就是简单拒绝请求。 合格答案要区分放行、排队、快速失败、降级、退避和熔断,而不是只说“挡住流量”。
- 误区:全局平均流量低就没有风险。 热点 key、单实例、单下游资源仍可能先被打满,所以要看局部水位。
- 误区:限流阈值可以拍脑袋配置。 阈值要来自压测容量、依赖水位、错误率、P99 延迟和业务优先级。
- 追问:限流应该放在哪里? 入口网关、服务内部、客户端和下游资源侧可以分层配置,分别保护不同边界。
- 追问:触发限流后怎么处理? 核心写请求可排队,非核心读请求可降级,用户侧要给明确失败或稍后重试。
- 追问:如何验证流控策略有效? 看 QPS、并发、延迟、拒绝率、队列长度、下游错误率和恢复时间。
九、加强记忆
分布式限流的核心矛盾是全局精确和系统成本之间的取舍。Redis + Lua 简单可靠,网关限流保护入口,服务内限流贴近资源,本地预分配提升性能,故障时要有 fail-open、fail-close 或本地兜底策略。限流器本身也必须高可用,否则保护系统会变成新的单点。