☰
限流算法与分布式限流实战:从原理到网关配置
2026/10/21 14:57:53 网站建设 项目流程

做网关和中间件这么久,打交道最多的就是限流。面试必问,生产必踩坑,线上出事第一个背锅的也经常是它。限流算法看着就那几种,固定窗口、滑动窗口、漏桶、令牌桶,名字都认识,但真到了网关侧落地,什么时候选哪种、分布式怎么做才不崩、阈值该配多少,能讲清楚的人并不多。这篇把限流算法的原理、分布式落地的坑,以及网关实战配置的思路从头到尾捋一遍,不绕弯子,直接按我实际做项目的顺序来。

先说清楚它解决什么问题:限流本质上是在“保护系统”和“服务用户”之间找一个平衡点。系统能扛的量是固定的,超过就必然出问题,不是变慢就是直接挂。限流就是用一个规则,把超过承载能力的流量挡在外面,让系统在压力下还能给一部分用户提供服务。它适合谁看?后端开发、微服务网关的使用者、中间件维护人员都适用。尤其是刚接手网关配置、准备在线上打开限流开关的同学,这篇能帮你少走不少弯路。

1. 限流算法全景:四种算法的性格差异

1.1 固定窗口:实现最简单,但边界上有硬伤

固定窗口的思路非常直白:把时间切成固定大小的窗口,比如一秒一个窗口,每个窗口内有固定容量,比如 100 个请求。每来一个请求,就检查当前窗口的计数器,没超过 100 就放行并把计数加一,超过就拒绝。

这个算法实现成本极低,用单机内存就能搞定,Map 加一个整数做计数,半分钟就能写出来。伪代码都不用看,核心就一个判断:当前窗口计数是否达到阈值。

但它的致命伤是临界突发。假设限流阈值是每秒 100 次,第一秒的最后 50 毫秒内来了 100 个请求,第二秒的前 50 毫秒内又来了 100 个请求。从每个单窗口看都合法,实际上一秒内过去了 200 个请求。对下游系统来说,这就是一次瞬时 200 QPS 的冲击。固定窗口只能限制“每个窗口内的总量”,限不住“窗口邻接处的连续流量”,这是我第一次上线限流配置时,压测直接打穿后端连接池才彻底理解的。

所以在正式环境里,我基本不会单独用固定窗口做全局限流,它更适合用在那些“允许偶发抖动、只要整体不超量”的场景,比如内部分发的简单控制。

1.2 滑动窗口:把窗口切碎,缓解边界问题

滑动窗口是对固定窗口的一种补救思路:不固定窗口边界,而是让窗口按时间不断往前滑动。常见做法是像前面说的切多个小窗口,用 Spring Cloud Gateway 的 RequestRateLimiter 就是这种思想——它内部用令牌桶实现,但很多自研限流组件也喜欢用滑动窗口来实现 Redis 的方案。再用我自己的话说,滑动窗口限流就是:窗口还是那个窗口,但窗口的起点不是固定在 0 秒,而是随当前时间不断前移。

举个最常用的“格子化”实现:一秒钟拆成 4 个 250ms 的小格,每个格子单独计数,窗口就是最近 4 个格子的总和。请求来了先算落在哪个格子里,然后统计当前时间之前的 4 个格子总计数,超过阈值就拒绝。

相比固定窗口,滑动窗口的好处是能覆盖跨边界流量:因为窗口起点是连续移动的,刚才 100ms 前的请求和现在的请求会落在同一个滑动区间里,瞬时大流量到来时更容易被识别出来。代价是存储量更大、计数更频繁,如果纯靠内存做,内存空间得按分钟级窗口存多个格子。

但它也有自己的局限。滑动窗口不是精确流量整形,它只是“粒度更细的近似限流”。格子切得越细,越接近真实流量轮廓,但对 Redis 的读写压力也越大。在生产中做滑动窗口,格子数量一般不超过 10 到 20 个,再细就纯属自找麻烦。

1.3 漏桶与令牌桶:从“数请求”到“控速率”

固定窗口和滑动窗口的本质都是“计数”:数一数窗口内有多少请求。但漏桶和令牌桶的视角完全不同,它们模拟的是物理装置:一个控制流出速率,一个控制流入速率。

漏桶的模型是一个底部有洞的水桶,水进来多了就从上面溢出去。系统处理请求的速率恒定,桶里堆积的是请求,超过桶容量的请求直接丢弃。它的核心特性是强行让流量排成队,平滑程度最高。

令牌桶的模型是一个按固定速率往桶里放令牌的装置,桶里有令牌有上限。请求来了先拿令牌,拿到就放行拿不到就走人(或等待)。跟漏桶相反,令牌桶容忍突发流量:只要桶里有攒下的令牌,瞬时来了 10 个请求,只要有 10 个令牌,10 个请求瞬间放行毫无压力。

做网关限流,我优先令牌桶。为什么?因为真实业务流量天生带突发:活动开始、秒杀瞬间、热门视频发布后的刷量,都是陡增。如果直接用漏桶,所有突发流量都会被切平,用户的等待时间会明显拉长,一些本来能快速处理的短请求也可能被排在长任务后面。令牌桶能在保证平均速率不超限的前提下,允许短时突发,对用户侧更友好,对后端侧又有个保护上限。

当然,如果你是做削峰填谷,比如夜间把任务队列的写入速率固定下来,让后端慢慢消化,那漏桶更合适。选型不取决于哪个“高级”,而是取决于流量的容忍度是否允许突发。

1.4 选型对照:别只背名字,要看业务容忍度

算法限速模式是否允许突发实现复杂度典型适用场景
固定窗口按窗口总量边界处有较大突发极低简单计数,内部分流
滑动窗口按滚动时段总量小突发(取决于格子粒度)中多数普通接口限流
漏桶恒定流出速率不允许中流量整形、消息入队
令牌桶平均速率 + 突发容量允许中高网关接口限流、秒杀场景

这里想多说一句:网关上做限流,不是“选一个算法就完事”,而是要考虑多级限流。入口用令牌桶挡住大部分流量,核心写操作再加一层滑动窗口限制单位时间总量,两个层级配合。我在实践中最常用的组合是“网关令牌桶 + 业务关键链路滑动窗口”,既保证了用户侧体验,又兜底了瞬时峰值。

2. 分布式限流:从单机到集群,难点在一致性

2.1 单机限流的局限:网关多实例后,计数器就失真了

早期我把限流写在网关单机里,本地内存记数,一切看起来很美好。后来网关扩容到 3 个实例,问题就出现了:同一时间来了 150 个请求,负载均衡分给每个实例各 50 个,每个实例都以为才 50 个请求,欢快地放行了,实际上游端已经扛了 150 个 QPS,还是超了。

这就是单机限流的根本矛盾:限流规则是“全局”的,计数状态却是“局部”的。只要实例数大于 1,局部计数就不能代表全局流量。

也有人问,那我在负载均衡层做不行吗?不行,负载均衡一般不感知业务层的限流策略,它只负责分发请求,而且大部分负载均衡中间件也没有限流计数能力。

2.2 为什么是 Redis + Lua:原子性与一次网络往返

分布式限流的常规解法是引入一个中心化状态存储,Redis 是当前最常用的方案。但用 Redis 做限流计数器有一个并发难题:多实例同时读写同一个 key,存在竞态条件。

比如两台网关同时读到当前计数是 98,都判断“还能放行 2 个”,同时把计数加一,结果实际变成 100,但请求可能已经放了超过阈值的量。一次或两次误差可能不算大,但在高并发下会累积成显著偏差,用 Redis 但不用原子操作,等于把分布式限流做成了概率限流。

解决方法是把整个“读计数 + 判断阈值 + 增加计数”三步放进一个 Lua 脚本里交给 Redis 原子执行。为什么 Lua 能保证原子性?因为 Redis 是单线程模型,在服务器执行 Lua 脚本期间,Redis 不会同时执行其他命令,整个脚本期间没有其他客户端能够插入新的读写操作。

而且把多条操作封装在一个脚本里发过去,网络只往返一次,性能比多次 round trip 好得多。我写过一个固定窗口的实现,核心逻辑就是先用 GET 拿当前计数值,判断是否超过阈值,如果没超就 INCR 并把过期时间设置好。这里有一个关键点:当 key 不存在时,通过 SETNX 初始化计数值并同时设置过期时间,避免每次请求都刷新过期时间导致窗口永远不会过期。

实际生产环境的脚本往往比这复杂,比如要做“每实例限流 + 集群全局限流”两层校验。我踩过一个坑:只在网关集群层设了限流,结果某个实例因为客户端连接保持策略被单独打爆,后来在单实例上也加了最大并发数,两层互相兜底,才彻底解决。

2.3 限流维度设计:Key 的粒度和维度决定命中率

限流 key 的设计直接影响限流效果。按粒度分,常见维度有:

  • IP 维度:限制单个 IP 对系统的访问频率,防止恶意刷量。但同一个出口 IP 后面可能站着一整个公司的人,粒度太粗会误伤。
  • 用户维度:按用户 ID 或账号进行限流,比 IP 精确得多,但需要网关能获取到用户标识,匿名接口就做不了。
  • 接口维度:按 URI + Method 维度对每个接口单独限流,是最常用的兜底维度。很多团队的起步配置就是接口维度全局限流。
  • 组合维度:真正生产环境我几乎不会用单一维度,基本都是接口维度和用户维度叠加。比如“获取订单接口 + 用户 ID”组合 key,这样既能防止单个用户疯狂刷接口,也能在流量暴涨时通过接口维度上限兜底整体访问量。

设计限流 key 时有三个注意点:key 要有前缀区分,避免和业务缓存混在一起;key 不能过长,否则 Redis 内存浪费;key 要有过期时间兜底,防止永不失效的垃圾 key。另外要注意:限流 key 的维度划分要和工作量级成正比。比如每秒上千 QPS 的系统,如果用“接口 + 用户”粒度限流,key 数量巨大,Redis 高并发写入本身也会成为瓶颈,此时做级联限流或本地预计算可能是更好的选择。

3. 网关限流实战:配置一条可用的优质限流规则

3.1 在网关中配置一套可用规则

市面上主流的开源网关大多内置了限流过滤器,配置思路是:定义限流维度的 Key Resolver,配置 Redis 限流器和参数。以使用较广的 Spring Cloud Gateway 为例,它的 RequestRateLimiter 过滤器基于令牌桶算法实现,内部使用 Redis + Lua 脚本进行计数。

关键配置有两块。第一块是 KeyResolver,它决定限流维度。比如想按 IP 维度限流,就写一个解析客户端 IP 的 Bean:

@Bean public KeyResolver ipKeyResolver() { return exchange -> Mono.just( Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress() ); }

用 RedisRateLimiter 指定限流参数时,有三个核心参数:

  • replenishRate:令牌桶每秒填充速率,也就是平均每秒放行的请求数。
  • burstCapacity:令牌桶最大容量,代表允许的突发流量大小。
  • requestedTokens:每次请求消耗的令牌数,通常为 1,但做优先级控制时可以设为更大值。

对应的配置写法大概如下:

spring: cloud: gateway: routes: - id: order-service-route uri: lb://order-service predicates: - Path=/order/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 50 redis-rate-limiter.burstCapacity: 150 redis-rate-limiter.requestedTokens: 1 key-resolver: "#{@ipKeyResolver}"

这套配置的含义是:订单接口平均每秒放行 50 个请求,允许突发最多 150 个,按 IP 维度计数。补充一个小经验:同一环境下,replenishRate为 50、burstCapacity为 150,则处理突发后有约 100 个闲置令牌供消耗,令牌桶的容量设置不要审得太紧,否则会引发整体可用性下降。

3.2 超时与降级:被限流后用户看到什么

限流配置好,只完成了一半。另一半是用户被限流后的响应。默认的限流响应往往是 429(Too Many Requests)或空响应,直接返回对用户不友好,也不方便排查。

我通常会给限流出口定制一个统一返回结构,包含状态码、描述、当前时间戳和请求 ID,方便前端拿到后做兜底提示,也方便后端日志排查。代码上可以写一个全局异常处理或过滤器,检测到限流异常后返回 JSON:

{ "code": 429001, "message": "请求过于频繁,请稍后再试", "requestId": "a8f3b2c1-5a7e-4d0f-9d33-6c1f3a9812e1" }

前端拿到 429 或自定义 code 后,可以弹窗提示“操作太频繁”,而不是让用户看到一个白屏。没有降级方案就匆匆上线限流,等于把一个故障变成另一个故障。

提示:限流响应必须走独立的降级链路,不能和业务异常混在一起。很多网关默认把限流响应当作普通错误处理,结果限流后触发熔断,雪球越滚越大。

3.3 压测验证:用可控流量确认限流策略生效

配置完要做压测,不能靠感觉。简单压测可以直接用开源压测工具,约定一个模拟接口,设置总请求数和并发数,再在网关端观察日志和 Redis 计数。

假设配置是每秒 50 个许可、突发容量 150,压测时直接以每秒 200 QPS 持续请求 3 分钟,预期的结果是:最初短时间内会有 150 个请求放行(突发容量),随后稳定在每秒约 50 个请求的水平,很多请求被拒绝并返回 429。

判断限流是否生效不能只看状态码,要同时观察:通过的请求数和被拒的请求数比例是否符合预期、后端服务的负载是否保持平稳、Redis 的 key 是否按预期增长和过期。压测限流比压测业务难在:你很难只通过请求总数判断限流准不准,因为限流是动态过程,得结合时间窗口分析。

4. 不拍脑袋定阈值:限流参数的容量估算过程

4.1 令牌桶参数的计算逻辑

阈值设定是最玄学的地方,很多团队直接拍个数字:哦这个接口给 50 QPS 吧。没有计算依据,配置之后往往要么限得太紧把正常用户挡在门外,要么限得太松没起到保护作用。

正确做法是反向推导:先看下游服务的实际承载能力。比如要对某个下单接口限流,接口的数据库写压力比较大,单机数据库能承受的最高 QPS 是 200,该接口平均响应时间 200ms,需要依赖 2 个数据库节点,估算整体能承受 400 QPS。

在这基础上,还要预留安全余量,一般按 70% 到 80% 的容量计算。假设数据库能力是 400 QPS,那限流阈值建议约 300 QPS。为什么要留余量?因为 QPS 不是匀速的,接口耗时会抖动,连接池状态也不确定,把阈值顶满等于站在悬崖边上操作。

然后是突发容量的计算。假设业务上特殊的峰值可能是平均值的 2 到 3 倍,比如大促时段可能达到 800 QPS,期望突发峰值持续不超过 15 秒,那令牌桶容量要根据需求的突发时长和多余速率计算:(峰值速率-平均速率)× 持续时间。如果平均 300 QPS、实际峰值 800,持续 15 秒,需要的突发令牌是(800-300)× 15 = 7500 个。这样算出来的容量明显清晰得多,后面压测验证也方便。

4.2 集群实例数对限流配置的影响

分布式限流常见的困惑,是配置的阈值是“每实例”还是“集群总量”。在网关侧做分布式限流时,配置值代表的是整个集群允许量。比如配置每秒 300 QPS,3 实例网关,则集群内所有实例合计每秒只能放 300 个请求。

但有个细节要注意:如果网关和 Redis 之间存在网络分区或实例间请求路由不规范,或者部分场景使用本地限流时,配置值在每实例生效,集群总量就会乘以实例数,效果完全不一样。配置上线前先明确限流器是本地模式还是分布式模式,这个小事搞错了,数字偏差一个量级。

我曾经在一个项目上就栽过跟头:把集群总量 300 配到单实例上,3 实例实际限流到 900 QPS,压测的时候差点把下游打崩。发现时还庆幸是在压测环境被提早发现,如果在生产线上才暴露,就是个不小的故障。

4.3 阈值与业务模型匹配

不同业务形态对阈值的要求差异很大:

业务场景流量特征限流策略建议
普通业务接口波动小,偶有毛刺平均速率 × 1.5 做阈值,突发容量不用太大
秒杀、抢购活动瞬时爆发大较大的突发容量,配合削峰策略
写操作接口QPS不高但耗时长按后端连接数/线程数估算,用并发限流更合适
消费消息任务持续恒定负载用漏桶思想控制处理速率
第三方开放接口有 SLA 约束按令牌桶精确控制平均速率和突发容量

这里补一个重要观点:对写操作类接口,限流不等于并发控制。如果接口平均耗时 1 秒,每秒 10 QPS 也意味着有 10 个请求同时在处理中,如果连接池只有 5 个连接,可能连接池先扛不住。这类接口更应该关注“并发中的请求数”,而不是纯 QPS 限流。网关限流器解决的更多是入口流量过快问题,后端还要配合信号量或线程池隔离控制并发度。

5. 常见问题与排查技巧实录

5.1 限流不生效:从这几个方向逐个排查

限流上线后发现没效果,几乎每个团队都会遇到。结合我自己的排障经验,按以下顺序排查基本能定位问题:

排查项典型原因验证方法
限流 key 是否稳定用随机参数/时间戳做 key查看 Redis 中 key 的分布是否集中
限流器是否生效配置被禁用、路径匹配不上观察网关日志中过滤器是否打印
多实例配置是否一致各实例配置不统一对比各实例配置文件的限流参数
限流维度是否过细key 粒度过细导致总量达不到阈值临时换成接口维度压测验证
Redis 是否成为瓶颈高并发下 Redis 网络延迟高观察 Redis 命令耗时

最诡异的一个案例是:配置了 IP 维度限流,结果限流 key 里意外混入了客户端传来的一个参数,导致每个请求的 key 都不一样,限流形同虚设。排查方法很简单,在限流过滤器里临时打印 key 值,看看一批请求的 key 是不是一致。

5.2 热 Key 把 Redis 节点打爆

限流依赖 Redis,Redis 本身也可能成为瓶颈。尤其是限流 key 集中在少数几个热点上时,比如秒杀活动只针对一个接口、一个用户维度,大量的 INCR 命令全打在同一个 Redis key 上,单节点 CPU 直接飙升,响应变慢甚至超时。

我的实操经验是:限流 key 的粒度不要永远保持单一维度,必要时可以在 key 后面加一个随机子项做预分片,比如把用户 ID 哈希后拆到 100 个逻辑分片里,每个分片单独计数,请求时根据用户 ID 命中对应分片。判断限流时把 100 个分片的计数汇总再做判断。这样单个 Redis key 的压力就降低了 100 倍,整体效果近似原来的限流。

不过这个方案有个取舍:汇总计数的计算和判断会比单个 key 复杂得多,实时性也略差。大部分场景下,限流 key 的分布其实比较分散,这个优化只在单 key 热点极其严重的场景才需要做,属于小众场景的优化技巧。

5.3 集群时钟偏移与配置残留

滑动窗口限流依赖时间,如果网关实例和 Redis 服务器的系统时间偏差很大,窗口的计算就可能出错。比如客户端时间比 Redis 快 10 秒,限流窗口会被错误拉长或缩短,导致限流失效或者误杀正常请求。所有限流组件所在服务器的系统时间必须通过 NTP 同步,生产环境用时会话和限流判定,不能依赖各服务器的本地时钟。

另一个容易踩的坑是动态配置发布失败后,旧限流规则残留在 Redis 缓存里。由于 key 相同,新配置不会覆盖旧值,导致限流阈值没有用配置里的新值,而是沿用旧值运行。这个问题的对策是:配置变更后要主动清理或让 key 带版本号,确保旧配置不再生效。限流配置上线/下线,都应该有配套的缓存清理步骤,不能改了配置文件就算完事。

5.4 可观测性建设

限流要长期稳定运行,必须有可视化监控和告警。我会在限流组件中埋点统计以下指标:总请求数、被限流请求数、放行请求数、限流 key 口径命中率、限流器执行耗时。通过这些指标可以做三件事:压测时判断限流策略是否符合预期;线上判断是否出现限流误杀;事后分析策略是否需要调整。

告警阈值建议设置两个级别:WARN 级别:最近一分钟限流拒绝率超过 20%,说明系统可能接近瓶颈或策略过严,需要关注;ERROR 级别:最近一分钟限流拒绝率超过 50%,或者限流器自身出错(如 Redis 超时),需要立即介入。再提一个小技巧:限流日志不要打在业务日志里,要打在独立的限流访问日志里,用日志采集管道单独分析。否则大流量下业务日志会被限流事件刷爆,影响正常排障。

写在最后

最后分享一个我反复说过很多次的体会:限流配置上线前,一定要先在压测环境模拟一次“故意打爆限流”的演练。很多同学在压测环境里只测“能扛多少量”,不测“超量后的表现”,结果限流降级的链路从来没被真实触发过,上线后一遇到超量才暴露问题。我在项目上吃过这个亏之后,所有限流配置的验收标准都固定为两条:一是在阈值范围内系统能稳定运行;二是超出阈值后拒绝效果和降级响应符合预期。能同时满足这两条,限流才算真正可用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询