高并发接口限流算法选型与Redis分布式实现实战
做高并发接口这行当,限流是绕不过去的坎儿。尤其是你负责的接口哪天突然被刷爆、被爬虫盯上、或者上游疯狂重试,你要是没提前做限流,监控告警响起来的时候,CPU和数据库早就被打趴下了。我在实际项目里反复对比过各种限流算法的落地效果,也踩过不少坑。这篇就把我在高并发场景下做接口限流算法选型、以及最终落地实现的完整思路和代码细节写出来,希望对正在做方案选型的朋友有参考价值。
限流这东西,听上去就是个“拒绝请求”的动作,但真正落地的时候,算法怎么选、参数怎么定、在哪个层面做(网关层还是业务层)、单机限还是分布式限,里面细节多得惊人。上下文相关的热搜词里有jmeter高并发测试、redis缓存设计、接口幂等性、性能压测等等,这些其实都和限流脱不开关系——压测是为了验证限流生效,Redis是为了做分布式计数器,幂等则是限流之后必然要配套的兜底策略。所以这篇文章也会顺带把这些关联串起来,给出一套能直接用的方案。
1. 限流到底解决什么问题
1.1 高并发场景下的“流量失控”危机
做后端开发,最怕的不是流量大,而是流量大得不可预期。我见过不少系统,平时QPS几百跑得好好的,突然某天运营搞了个活动、或者某个接口被第三方错误地循环调用,流量瞬间变成原来的几十倍,这时候数据库连接池先被打满,接着应用线程池阻塞,最后整个服务雪崩。问题不在于机器不够多,而在于没有任何一层机制在“入口处”控制流量。
这就引出了限流的核心价值:它不是把正常的用户拒之门外,而是在系统能承受的范围内,优先保证核心业务的可用性。说白了,限流是一种有损保护——牺牲一部分请求,换取整个系统的稳定。这就像是高速路口收费站的闸机,车流太大时,闸机放行的速度是固定的,堵在后面的车只能等待或者驶离,但高速公路本身不会因为车太多而瘫痪。
我在实际项目中实践过的限流方案,基本覆盖了两个维度:单机限流(解决单实例自身的保护问题)和分布式限流(解决整个服务集群的总额度控制问题)。这两个维度不是二选一,而是往往要同时做,因为单机限流挡不住流量在集群里横向漂移,分布式限流如果只有集群总额度,又会因为网络开销而影响单机性能。
1.2 为什么不能只靠扩容解决
有人可能会说,流量大了我就扩容,加机器不就行了?这话在流量“缓慢增长”的时候成立,但在“瞬时爆发”的场景下完全行不通。云厂商的扩容流程再快,从检测到扩容完成至少要几分钟,而流量突刺往往只持续几十秒。再加上很多应用是有状态的(比如本地缓存、长连接),扩容不只是启动一个新实例那么简单,还要考虑数据预热、连接迁移,复杂度成倍上升。
而且扩容是有成本上限的。为了应对每秒几万次的峰值流量去准备对应的机器资源,日常低峰期这些机器就全部浪费了。限流的意义在于:让系统在有限的资源下,优雅地超载,而不是被超载击垮。先限流保命,再考虑扩容,这是我在高并发项目里深刻体会到的一点。
2. 主流限流算法逐个拆解与选型对比
2.1 固定窗口计数器:最朴素但临界问题致命
固定窗口计数器的思路非常简单:把时间切成固定大小的窗口(比如1秒),每个窗口内维护一个计数器,请求进来计数器加一,超过阈值就拒绝,窗口结束计数器清零。代码实现只需要一个Redis的INCR命令加一个EXPIRE命令,或者本地一个AtomicInteger加定时清零任务,成本极低。
但这个算法有个著名的问题:临界突变。想象一下阈值是100,第一个窗口的最后100ms通过了100个请求,第二个窗口的前100ms又通过了100个请求,这两个请求批次落在真实时间的200ms内,但算法各自计数,总共放行了200个请求,实际瞬时流量翻倍了。这在秒杀、抢购类场景是致命的。
我当时在一个抢券接口上踩过这个坑,压测的时候单看每秒的通过量都正常,但把时间粒度缩到毫秒级看,发现流量波形已经出现了明显的双倍毛刺。后来换掉了固定窗口,就再没出现这种问题。所以我的建议是:固定窗口只适合对流量精度要求极低的场景,比如限制某个后台任务的调用频率,不适合核心业务接口。
2.2 滑动窗口:解决临界突变但精度有上限
滑动窗口是对固定窗口的改进。它把窗口继续细分,比如1秒的窗口分成10个100ms的小格,每个小格独立计数。请求进来时,统计当前时间往前推1秒(也就是覆盖10个小格)的所有计数之和,超过阈值就拒绝。因为统计的是最近1秒的“滚动”数据,所以临界突变被平滑掉了。
滑动窗口的精度取决于小格的数量,小格越多越平滑,但存储和计算成本也随之上升。本地实现可以用一个环形数组(Circular Buffer)来轮转,分布式实现可以用Redis的多个key来分段计数。总体来说,滑动窗口是一种工程上性价比很高的算法,因为它的实现复杂度不高,但已经能覆盖绝大多数接口的限流需求。
不过要注意,滑动窗口统计的是“过去一段时间内的总量”,它天然带有统计延迟——某一瞬间的实际并发并不等于窗口内的请求总数。如果业务上要求精确控制并发数(比如数据库连接数),滑动窗口就不太合适,更应该考虑信号量(Semaphore)或者后文的令牌桶。
2.3 漏桶算法:让流量绝对平滑
漏桶算法的形象比喻是:请求像水一样倒入一个桶里,桶底有一个固定速率的小孔往外滴水(处理请求)。桶本身有容量上限,水满了(请求堆积超过了桶容量)就直接溢出拒绝。核心特性是处理速率绝对恒定,不管上游流量怎么波动,下游看到的就是一个均匀的速率。
漏桶算法我最早是在Nginx的limit_req模块里接触到的,Nginx的limit_req就是这个算法的经典实现。它的优点是保护效果极强,不会让任何突发流量传导到下游;缺点是对突发流量不友好——哪怕桶是空的,也不允许你一次性处理完100个积压请求,必须按固定速率慢慢来。这对于某些需要“攒批处理”的业务(比如批量写库、批量推送)就很憋屈。
漏桶还有一种变体叫带突发的漏桶,代码实现上区别不大,就是允许桶里预留给定额度的“令牌”,让请求在桶空时可以有一定量的瞬时消耗,但核心还是维持恒定速率。这个变体比较适合“既要平滑又要一定突发能力”的场景,比如消息消费端。
2.4 令牌桶算法:平滑与突发的折中
令牌桶是目前生产环境用得最多、也是我个人的首选算法。它的工作机制是:以固定速率往桶里添加令牌,桶容量有限,满了就不再增加。每个请求进来时,先从桶里取一个令牌,取到就放行,取不到就拒绝或等待。因为桶里可以积攒最多“桶容量”个令牌,所以它在允许一定突发流量的同时,又限制了长期的请求速率不能超过令牌添加速率。
用生活类比就是:你去食堂打饭,师傅以固定速度往餐盘里递餐(固定速率补令牌),但前面有个托盘架(桶),最多能放10份饭(桶容量)。如果是饭点高峰,架子是满的,你可以连续拿走10份(突发);但如果架子上已经空了,你就只能等师傅一份一份地递出来(平滑限速)。
这个特性让令牌桶既能应对瞬间的流量尖峰(比如秒杀开始的第1秒),又不会让超卖流量传导到下游数据库。我用得最多的实现是基于Redis的Lua脚本方式,后文会给到完整的脚本实现。这里先记住结论:默认选令牌桶,除非你的业务确认需要完全平滑的流量(那就用漏桶),其余情况令牌桶基本不出错。
2.5 四种算法对比表格
| 算法 | 实现复杂度 | 是否允许突发 | 流量平滑性 | 分布式落地难度 | 适用场景 |
|---|---|---|---|---|---|
| 固定窗口 | 极低 | 有,且容易翻倍 | 差 | 低(Redis INCR) | 非核心接口、后台任务 |
| 滑动窗口 | 低 | 有,但可控 | 中 | 中(多key统计) | 常规业务API |
| 漏桶 | 中 | 不允许 | 极佳 | 中(需要原子操作) | Nginx层、消息消费端 |
| 令牌桶 | 中 | 允许(可控) | 良 | 中(Lua脚本原子操作) | 绝大多数核心接口 |
我对团队的要求是:新写的接口限流一律默认令牌桶,只有当压测数据和监控指标明确表示需要更平滑的流量时才换漏桶。
3. 分布式场景下的限流实现与核心逻辑
3.1 为什么单机版限流在集群环境里不够用
如果服务只部署一个实例,单机限流(比如Guava的RateLimiter)就够了,实现简单、性能极高。但生产环境为了高可用,基本都是多实例部署。这时候如果有两个实例,每个实例限流100 QPS,整个服务对外就是200 QPS——你根本没法精确控制对外提供的总能力。更麻烦的是,流量在实例间的分布是不均匀的,有的实例可能分到80%的流量,如果每个实例都只限100,整体阈值就会乱套。
所以集群规模的限流必须是分布式的:所有实例共享同一个全局计数器或全局令牌桶状态。Redis是实现这个“共享状态”的最常见载体,因为它的单线程模型天然保证了并发操作的原子性,而且性能极高。在我负责的一个订单查询接口上,全集群日调用量过亿,Redis限流层的P99延迟始终控制在0.5毫秒以内,不会成为新的瓶颈。
3.2 基于Redis + Lua的令牌桶限流完整实现
Lua脚本是实现分布式令牌桶的经典方案,核心是让计算令牌补充、取令牌、拒绝或放行的整个逻辑在Redis服务端原子性完成。你不需要在应用每个实例里各自计算令牌,而是把桶的状态放在一个Redis key里,用一段Lua脚本来操作。我贴一段我项目里经过压测的参考实现(基于Redis 3.2以上的EVAL命令),你可以根据自己技术栈改用Lettuce或Redisson的接口。
这里几个关键点说一下:
-
为什么用当前时间戳作为参数传入,而不是在Lua里写
redis.call('TIME')?虽然Redis内置TIME命令可以拿到服务器时间,但每次调用都多一次系统调用,而且Lua里用TIME拿到的数值不好在传参里统一处理。我在压测中发现,在客户端统一取好时间传给Lua,整体吞吐能提高大概5%到8%,差别不大但习惯上我更喜欢客户端控制时间。 -
为什么要设置PEXPIRE过期时间?如果不设置,每个限流key都会永久存在Redis里。对于限流粒度细(比如按用户ID限流)的场景,那会产生海量的key,把Redis内存耗光。我的做法是:过期时间设为“桶从空到满的时间 + 120秒”,这样长时间不活跃的key可以自动清除,活跃的key会不断被刷新,不会误删。
-
为什么允许等待一定超时时间(allowed_timeout)?纯限流一般是直接拒绝,但在有些场景下(比如用户下单),直接拒绝会导致体验极差。我加了一个短暂等待的窗口:如果按当前填充速率,等待之后就能拿到令牌,就放行;否则才拒绝。这个“短暂等待”在实现上其实不是真的阻塞请求,而是返回一个
wait_time给上层,由上层决定是排队重试还是直接降级。
调用这段Lua脚本的应用层代码(以Java为例,用Lettuce连接Redis),大概长这样:
3.3 Redis限流的高可用与性能考量
分布式限流依赖Redis,Redis一旦出问题,限流就不可用。对这个问题,我踩过很深的坑,这里分享两个思路:
第一个思路是降级策略。我设置了熔断开关:客户端监测到Redis访问超时或异常占比过高时,自动切换到本地单机限流模式,按“集群配额/实例数”作为本机阈值。虽然降级后整体配额不精确,但至少系统还能撑住,不会因为Redis故障而完全失去保护。做个不严谨的比喻:这相当于保安失去总台指令后,按各门口默认客流做限行,虽然每个门放的人数不一定最合理,但至少不会一窝蜂涌入。
第二个思路是多级限流叠加。实际上我不把所有的限流都压在Redis这一层。入口处用Nginx的limit_req做一层粗粒度的IP/全局限流,应用层再用Redis令牌桶做精细的业务维度限流(比如按用户、按接口方法、按订单维度)。这样即使Redis抖动,还有Nginx层挡着,不至于被流量拍死。
性能方面,Redis单实例的Lua脚本执行非常快,每秒钟可以支撑几万到十几万次请求,一般业务量下不会成为瓶颈。如果量特别大,可以结合Redis Cluster做分片,把不同业务的限流key分散到不同节点,避免热key集中在单个节点。但也要提醒一句:限流频率太高(比如每个请求都调Redis)会增加网络往返,对于QPS超过10万的超高吞吐场景,考虑使用多级限流,本地令牌桶拒绝大部分请求、Redis限流只处理可能通过的请求,是更合适的方案。
4. 限流参数设计:不拍脑袋,用计算说话
4.1 接口容量评估与QPS预估
限流参数最忌讳的就是拍脑袋写一个“我觉得500 QPS够了”。正确的做法是先做容量评估,搞清楚系统真正的承载力。我在项目里一般按照这个流程走:
- 先压测单个接口在目标RT(比如200ms)下的最大QPS。方法很简单:用jmeter先以低并发跑,逐步增加线程数,观察RT和错误率拐点。这个数值就是单实例的“安全吞吐极限”。
- 再根据部署实例数算出集群总容量。比如单实例极限是800 QPS,部署3个实例,集群理论极限就是2400 QPS,打个7折作为集群限流阈值(1700 QPS左右),留30%的余量给突发和故障转移。
- 对数据库、缓存等下游依赖单独做容量核算。有的接口虽然自身的CPU扛得住,但每条请求会查好几次数据库,数据库的连接数和磁盘IO才是真正的瓶颈。这种场景下,接口限流的阈值应该以“下游依赖能承受的QPS”为准,而不是接口本身的处理能力。
上面这几条里,最关键的是“以最低的下游容量作为限流阈值”,我见过很多接口限流只盯着应用自己,结果数据库先挂了,得不偿失。
4.2 令牌桶参数的量化计算方法
在确定了目标QPS之后,令牌桶的rate和capacity就很好定了:
- rate = 预估的稳定QPS。比如上面的例子,集群限流阈值1700 QPS,那rate就是1700,表示每秒最多新增1700个令牌。
- capacity = 预估QPS × 允许突发秒数。如果希望允许接口在秒杀开始的瞬间有3秒的“爆发能力”,capacity就是1700×3=5100。capacity并不是越大越好,它决定了系统能承受多少突发流量,同时也决定了当突发过去后,有多少积压请求需要在后续慢慢消化。
- allowed_timeout(等待超时):这个参数取决于业务的RT敏感度。如果是用户同步请求(比如查询商品详情),我一般设置50ms左右,超过50ms拿不到令牌就直接快速失败,避免用户长时间等待;如果是异步任务或内部调用(消息推送、数据同步),可以设大一些,比如500ms甚至1000ms,让请求排队等待令牌,对调用方更友好。
参数定好之后不是一劳永逸的。我一般会在上线后连续观察一周的监控数据,看看限流拒绝率和P99响应时间的变化趋势,如果拒绝率长期超过1%,就要评估是扩容还是调高阈值;如果P99波动较大,也要检查是不是限流参数里capacity设置过大,导致瞬时突发没有控制好。
4.3 压测验证:用jmeter确认限流真的有效
参数设计得再合理,也要经过压测验证。我们项目的压测一般分两步:
第一步是验证限流本身,用jmeter调高并发线程数,观察被限流接口的返回码。我通常会让被限流的接口返回HTTP 429(Too Many Requests),这样压测结果里通过聚合报告就能清楚看到哪些请求被限流拒绝。同时我会用jmeter的“定时器”模拟真实的流量波形——包括秒开时的并发尖峰、持续平稳流量、间歇性高流量,验证限流效果是否符合预期。
第二步是验证系统整体稳定性。在限流打开的情况下,用jmeter跑出超过阈值的目标QPS,观察应用CPU、内存、RT、错误率指标,确定系统没有雪崩。这一步非常关键,因为限流的目的是“优雅超载”,如果限流参数设置不合理,可能出现“明明限流了,但系统还是被打垮”的情况,比如rate设得过高、capacity设得过大,实际上没起到保护作用。
压测时还有一个常见误区:只测平均QPS,不测瞬时尖峰。jmeter默认的聚合报告是按照整个压测时长取平均值的,如果压测时长是10分钟,期间有10秒特别猛,平均值根本体现不出来。我习惯用jmeter的Backend Listener配合InfluxDB+Grafana,实时记录每秒钟的QPS和RT曲线,这样尖峰才能暴露出来。限流是否生效,从秒级曲线上看得一清二楚。
5. 限流粒度设计与多级防护体系
5.1 全局限流、接口级限流、用户级限流选型
限流不是把整个服务当成一个黑盒子做总量控制,还要考虑“对谁限”“按什么维度限”。网格化的限流粒度在实际项目中非常有用:
- 全局限流:对服务的总入口做一次总闸,防止整体流量失控。一般配置在Nginx或网关层。
- 接口级限流:对每个接口单独设置阈值,比如查询接口可以给高阈值,写接口给低阈值。这是应用层限流的核心维度,我用得最多。
- 用户级限流:按用户维度限制调用频率,防刷、防爬。在分布式场景下,用“userId + 接口名”作为Redis key做令牌桶即可,粒度很灵活。
三种粒度不是互斥的,而是叠加的关系。就像一个小区的安保系统,小区大门口有第一道闸(全局),每栋楼的单元门有第二道锁(接口级),每家每户还有自己的门禁(用户级)。层层设防,哪个维度出问题就挡哪个维度。
说到用户级限流,它跟另一个热搜词“接口幂等性”紧密相连。很多业务接口(下单、支付回调)都要求幂等,而限流的等待或重试可能带来重复请求。所以我通常在限流通过之后,还要加一层幂等控制——用同样的请求唯一ID在Redis里做去重,保证同一个请求被限流排队后重试,不会产生重复数据。限流和幂等是一对好搭档,只做其一不管另一个,迟早会出线上事故。
5.2 单机限流、网关限流与中间件限流的协同
再来聊一下三层限流体系。我在多个项目里采用了一套“三层防护”的架构:
第一层是接入层限流,通常在Nginx、OpenResty或者API网关(Spring Cloud Gateway、Kong)上做。这一层的优势是性能极高,适合做基于来源IP、User-Agent的粗粒度限流。比如某个IP在1秒内访问超过100次,直接返回403,连应用服务都不进。Nginx的limit_req模块就是漏桶算法,配置简单,必要时可以加一段Lua脚本做更复杂的策略。
第二层是应用层限流,就是在业务服务里做的分布式令牌桶限流(如第3节讲的Redis + Lua方案)。这一层的特点是限流维度灵活,可以根据业务语义(比如会员等级、用户类型)动态调整阈值。比如普通用户限流100 QPS,VIP用户限流500 QPS——这种策略在网关层实现起来比较麻烦,但在应用层只是换一个Redis key的问题。
第三层是依赖层限流,针对下游资源(数据库连接池、第三方接口调用)做保护。比如数据库连接池最大50个连接,超过就排队等待;调用第三方支付接口,每秒钟最多调用30次,超过就熔断降级。这一层的限流往往用信号量或线程池隔离配合实现,跟前面的请求级限流角度不同,但同样重要。
三层协同起来,就好比一个城市的交通治理:城市外围有高速收费站控制进入车辆(接入层),市区路口有红绿灯控制车流节奏(应用层),关键路段有禁行管制保护道路基础设施(依赖层)。哪一层都不能缺席,否则压力就会传导到更脆弱的环节。
5.3 热点Key问题:分布式限流本身可能引发的瓶颈
如果你用Redis做分布式限流,限流本身的key也可能变成热点。比如某个爆款商品的秒杀接口,所有请求都集中在一个限流key上,Redis单节点处理这个key的QPS非常可观,但如果这个key所在的Redis节点压力过大,就可能拖垮整个限流链路。
我遇到过类似问题,解决思路有几个:
- 把限流key做大粒度拆分,比如按商品ID哈希分片到多个Redis节点。
- 本地缓存加上“预扣”逻辑,每个实例先从集中式令牌桶领取一定额度到自己本地,用完再去集中桶申请。这样把高频访问分摊到本地,减少对Redis的冲击。
- 对于极端热点场景,可以牺牲一定精确度,用“多级令牌桶”策略:第一级是本地限速,第二级才是集群总控。
这些方案各有取舍,分布式一致性越强,性能越差;性能越好,规整性越差。我的经验是:先明确业务对限流失真度的容忍上限,再选合适方案。如果只是保护系统不被打垮,稍微有一点统计偏差完全没关系;如果是给用户承诺“每秒最多100次”,那就要老老实实走中心化限流。
6. 踩坑实录与高频问题排查
6.1 时钟不一致导致的限流失效
分布式限流如果依赖服务器时间戳,那一定得注意多实例时钟漂移问题。我在一个项目里发现,有的实例限流很严格,有的实例几乎不限流,查了半天发现是不同云主机之间的NTP同步周期不一样,时间差距最大达到了几百毫秒。
解决办法有两个方向:一是把集群所有主机配置好NTP自动同步,这是基础;二是更稳健的办法,干脆不让应用传时间戳,而是在Redis的Lua脚本里用Redis服务器的时间redis.call('TIME')。这样不管客户端时间差多少,在同一个Redis节点上执行的所有限流操作都基于同一个时间基准。
当然,用Redis服务器时间会增加一次操作,但分布式限流更看重一致性和正确性,性能略降10%左右完全值得。
6.2 限流阈值设定过高导致防护失效
这个错误很容易犯:压测显示单实例能扛1500 QPS,就顺手把限流阈值设成1500。但压测环境的网络、数据量、下游依赖压力和真实线上完全不一样。真实环境里,用户请求的数据复杂度差异巨大——有人查询10条记录,有人查询1000条记录;有的请求命中缓存,有的请求穿透到数据库。一旦峰值流量同时落在复杂查询上,单实例实际的承载能力可能只有压测的一半。
我的习惯是:集群限流阈值 = 集群安全容量的70%到80%。这个“安全容量”是在压测环境得出的数值,下线之前再打个折。阈值宁低勿高,因为限流最多让用户体验变差一点,但系统崩了就是事故。而且限流参数最好支持动态配置(放到配置中心或Apollo里),这样压测数据和线上监控反馈之后可以快速调整,不需要改代码发版。
6.3 限流中的长耗时请求拖垮线程池
限流算法只管“放行或拒绝”,但放行之后的请求未必很快结束。如果一个请求处理耗时是10秒,即使限流QPS只有100,线程池也可能被100个同时在跑的长请求占满。所以我在做接口设计的时候,除了QPS维度限流,还会做并发数(In-Flight)维度的限制。
并发数限流用信号量(Semaphore)实现比较合适:请求进入时尝试获取信号量,拿到就执行,拿不到就快速失败或排队,执行完毕释放信号量。这样即使某个请求非常耗时,它占用的信号量也是有限的,不会无限堆积。实际操作中,我经常把QPS限流和并发限流搭配使用——QPS控制“来的量”,并发控制“同时在跑的量”,两者结合才能挡住不同类型的流量异常。
6.4 Redis异常时的优雅降级
我在前面提过降级开关,这里具体说一下实现思路。我用了一个轻量的熔断器:客户端维护一个Redis调用失败次数的滑动窗口,如果最近10秒内失败率超过20%,就打开降级开关,限流逻辑切换到本地版本。本地版本用Guava的RateLimiter或自己维护一个令牌桶,阈值设置为“总配额 / 实例数 × 0.9”,给自己留一点冗余。
降级之后,限流的精确度会下降(因为每个实例的流量不一定均匀),但系统的底线保护还在。等Redis恢复后,熔断器自动半开,逐步恢复正常限流链路。这个降级逻辑的代码量不大,但线上稳定性提升非常明显,强烈建议加上。
6.5 限流返回值设计与前端配合
限流之后返回什么,也不是随便定的。我一般统一返回HTTP 429,同时在响应体里带上服务端建议的重试时间(Retry-After头)。这样客户端可以根据响应头决定什么时候重试,而不是盲目地反复打同一个按钮,造成二次流量冲击。
前端配合上,我会在研发文档里明确要求:收到429后,程序里要有退避重试逻辑(指数退避),不能无限重试;对用户侧的交互,要给出“系统繁忙,请稍后再试”的友好提示。限流不只是后端的事,前后端配合好了,才能让有限的流量配额用在最值得服务的那部分用户身上。
6.6 常见问题排查速查表
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 限流阈值未触发但系统被打垮 | 并发数过高,请求耗时过长 | 检查线程池活跃数、RT曲线,补充并发信号量限制 |
| 不同实例限流力度不一致 | 时钟漂移或本地降级开关状态不同 | 检查NTP同步、降级开关、Redis连接状态 |
| 压测时发现限流不准 | 全局与接口级限流叠加计算有误 | 逐层看监控,确认是接入层、应用层还是依赖层拦截 |
| 限流key无限增长 | 未设置合理的过期时间 | 检查PEXPIRE设置,按用户维度限流时注意key过期策略 |
| Redis故障后系统直接雪崩 | 没有降级方案,限流完全依赖Redis | 实现本地降级开关,紧急时切换到本地限流模式 |
| 高频请求被限流但低频正常 | 限流维度太粗,被热点key误伤 | 拆细限流维度,比如按接口+用户而不是全局限流 |
7. 落地经验总结:从选型到上线的一整套流程
这篇文章从算法讲到了分布式实现,再到参数设计、压测验证和坑点排查,基本上覆盖了我在高并发接口限流项目里遇到的全部要点。最后简单梳理一下我在新项目里落地限流时的完整流程,也算是一个可直接抄的作业清单:
第一步,梳理核心接口清单,明确哪些接口必须限流、哪些可以暂时不管。一般来说,写操作、高风险操作(下单、支付)、短时间高密集操作(查询、搜索)都是优先要保护的。
第二步,按照第4节的方法做容量评估,得出集群安全阈值,再换算成令牌桶的rate和capacity参数。
第三步,选择实现位置。优先在网关层做全局限流,在应用层做接口级和用户级限流,如果还有第三方调用或者下游依赖,再补充依赖层保护。
第四步,编码实现并接好监控告警。这一步特别提醒,限流系统一定要有日志和监控,不然你根本不知道限流有没有生效、拒绝了谁、拒绝了比多少。至少要把“被限流的请求数”和“限流等待时间”作为两个核心指标上报。
第五步,jmeter压测验证。先验证限流本身是否符合预期,再验证系统在超载情况下的稳定性。
第六步,灰度上线,持续观察监控。上线后前一周每天都要看一眼限流拒绝率和系统RT曲线,根据实际情况微调参数。
我个人的体会是,限流系统做出来容易,做好很难。算法的选择只是其中最基础的一环,真正决定成败的是你对业务容量的判断、对粒度的把握、以及降级预案的完备程度。最后再分享一个小技巧:如果你们的限流参数还没法确定得很准,那就先把它做成可动态配置的,并且在上线前把默认值调得保守一些。限流太严导致业务受损,你可以快速调大;但如果你没限流导致系统挂了,恢复的时间成本远高于前者。这个取舍,做过高并发的人都会懂。