
做网关和中间件这么久打交道最多的就是限流。面试必问生产必踩坑线上出事第一个背锅的也经常是它。限流算法看着就那几种固定窗口、滑动窗口、漏桶、令牌桶名字都认识但真到了网关侧落地什么时候选哪种、分布式怎么做才不崩、阈值该配多少能讲清楚的人并不多。这篇把限流算法的原理、分布式落地的坑以及网关实战配置的思路从头到尾捋一遍不绕弯子直接按我实际做项目的顺序来。先说清楚它解决什么问题限流本质上是在“保护系统”和“服务用户”之间找一个平衡点。系统能扛的量是固定的超过就必然出问题不是变慢就是直接挂。限流就是用一个规则把超过承载能力的流量挡在外面让系统在压力下还能给一部分用户提供服务。它适合谁看后端开发、微服务网关的使用者、中间件维护人员都适用。尤其是刚接手网关配置、准备在线上打开限流开关的同学这篇能帮你少走不少弯路。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 的 BeanBean 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 超时与降级被限流后用户看到什么限流配置好只完成了一半。另一半是用户被限流后的响应。默认的限流响应往往是 429Too 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 QPS3 实例网关则集群内所有实例合计每秒只能放 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 节点打爆限流依赖 RedisRedis 本身也可能成为瓶颈。尤其是限流 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 超时需要立即介入。再提一个小技巧限流日志不要打在业务日志里要打在独立的限流访问日志里用日志采集管道单独分析。否则大流量下业务日志会被限流事件刷爆影响正常排障。写在最后最后分享一个我反复说过很多次的体会限流配置上线前一定要先在压测环境模拟一次“故意打爆限流”的演练。很多同学在压测环境里只测“能扛多少量”不测“超量后的表现”结果限流降级的链路从来没被真实触发过上线后一遇到超量才暴露问题。我在项目上吃过这个亏之后所有限流配置的验收标准都固定为两条一是在阈值范围内系统能稳定运行二是超出阈值后拒绝效果和降级响应符合预期。能同时满足这两条限流才算真正可用。