聊到架构,很多人第一反应是微服务、网关、分布式事务那些大词,但我做了这么多年系统以后越来越觉得,限流才是架构里最不能省的那张底牌。限流这个事,网上搜“架构”会蹦出一堆概念,什么限流电路、Redis实现令牌桶限流、AOP基于注解的接口限流、Sentinel集群限流,看起来散,其实都是一件事的不同侧面。这篇文章我打算把限流从原理到落地、从单机到分布式完整拆一遍,把我踩过的坑、压测时调过的参数、线上出过的诡异问题都写出来。适合正在做后端开发、准备面试系统设计、或者接手了一个没有限流的老系统正准备往里加保护的人。
1. 为什么“限流”是架构里的兜底牌——先搞懂它在防什么
1.1 架构书讲得再花哨,流量一冲就现原形
热搜词里有一堆“架构”,Transformer架构、微服务架构、六边形架构、CNN逻辑架构……这些当然重要,但它们解决的是“系统长什么样”的问题。限流解决的是“系统扛不住时怎么办”的问题。很多人设计架构时画了一张漂亮的拓扑图,服务怎么拆分、消息怎么流转、数据库怎么读写分离,全都安排得明明白白,结果上线第一天遇到一次热点流量,整个系统直接雪崩。为什么?因为节奏不对,上游的突发流量、下游的慢响应、某个接口被脚本刷爆,这些都不是靠调整服务划分能解决的。
我刚工作那会儿也天真地以为,限流嘛,就是计数器加个判断,无非是代码里写个if,统计一下请求数,超过了就拒绝。后来线上出了好几次事故才明白,限流真正的难点在于:你要在“用户体验”和“系统稳定性”之间找到那个平衡点。限得紧了,正常用户被误伤,产品经理找上门;限得松了,数据库连接池被打满,值班电话被打爆。
1.2 三类典型冲击:突发流量、慢请求堆积、上游抖动
限流防的到底是什么?我把它归纳成三类,理解了这三类,后面选算法、定参数才不会拍脑袋。
第一类,突发流量。典型场景就是秒杀、抢红包、热点新闻带来的瞬时访问。这种流量特点是一瞬间冲得很高,持续几秒到几分钟就回落。如果系统按峰值去扩容,平时会浪费大量资源,不按峰值扩容,一瞬间就扛不住。限流在这里的作用是给流量“削峰”,把超过处理能力的那部分请求挡在外面或者排队慢慢放进来。
第二类,慢请求堆积。这类最容易被忽视。假设你的系统正常每秒能处理1000个请求,单个请求平均50毫秒扛得住。但某天一个下游接口突然变成500毫秒返回,你如果不对流量做控制,线程池里的线程全被卡住等响应,新的请求进来全部排队,最终所有接口都变慢,整个应用进入假死状态。这个场景下限流的核心是“优先保证系统可用”,哪怕丢弃一部分请求,也要保住主流程不崩。
第三类,上游抖动。在大流量网站里,一个接口往往是很多上层服务共用的。比如你负责会员服务,突然有个活动页疯狂调用会员信息接口,把你这边的资源打满,结果所有依赖会员数据的页面全部跟着卡。这种场景下,单靠业务代码里的错误处理不够,必须在入口处做保护,把“那边流量超标”的影响限制在局部。这其实是限流、熔断、降级三个机制配合的事情,限流管的是“不让过多流量进来”,熔断管的是“下游已经不行了,快速失败”,降级管的是“面子保不住,先保里子”。三者经常一起出现,但根子上的第一道闸门是限流。
1.3 用“水库泄洪”理解限流的调度逻辑
我一直喜欢用一个比喻跟新人解释限流:你的系统像一座水库,上游河流就是流量。水库的泄洪闸门开多大,取决于下游河道的承受能力,而不是上游来了多少水。如果上游暴雨,水库硬扛,坝体就得垮;如果下游河道窄,你却把闸门全开,下游城市就得淹。限流算法本质就是那个闸门的控制策略。固定窗口像是一个死板的调度员,每秒只放100个;令牌桶像是有个蓄水池,平时攒着令牌,突发时可以把攒的全放出去;漏桶像是一个固定的水管,不管上游怎么颠,下游始终匀速出水。理解了这层关系,你就知道限流不是简单地“拒绝请求”,而是“安排请求的节奏”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 五种限流算法选型对比:从固定窗口到令牌桶的博弈
2.1 固定窗口与滑动窗口:阈值判断里的边界问题
先聊最基础的固定窗口。把时间切成固定长度的窗口,比如1秒,每个窗口内维护一个计数器,请求来了加1,超过阈值就拒绝,下一个窗口清零。这个方案实现简单到极致,Java里一个AtomicInteger就能搞定。但它有一个非常经典的问题:临界突发。假设你设定的阈值是每分钟1000次,窗口切分在0分0秒到0分59秒、1分0秒到1分59秒。用户在0分59秒前发了1000个请求,1分0秒后又发了1000个请求,那么从宏观的1分钟跨度看(0:59:30到1:00:30),系统实际上放过了2000个请求。这2000个请求落在同一个秒级时段内,DB瞬间就可能被打爆。
滑动窗口就是解决这个问题的。它不是固定地平铺窗口,而是让窗口的起点跟着当前请求的时间移动,比如看“最近1分钟”而不是“当前这一分钟”。实现上,滑动窗口需要对时间切片,比如把1分钟切成60个小格,每个小格统计请求数,然后维护一个总和,每进来一个请求,就滑到对应的最新格子,同时把已经滑出窗口的格子的计数扣掉。这样临界突发的问题就被解决了,代价是内存占用和实现复杂度都上去了。我用过Guava的EventRecorder,发现滑动窗口在客户端做还可以,在分布式高并发下对存储的读写在极端情况下还是有压力的。
2.2 漏桶与令牌桶:恒定速率还是允许突发
漏桶算法,名字很形象。请求像水一样倒进一个桶里,桶的底部有一个固定速率的漏孔,水以恒定的速度流出去。不管上游怎么突发,下游看到的永远是匀速流量。漏桶非常适合保护下游对速率有硬性要求的场景,比如你调用的第三方支付接口只允许每秒20笔,漏桶就能保证永远不超过这个速率。但它的问题也很明显:不能应对突发消费。就算桶是满的,下游也只能按固定的速率处理,用户如果一时间涌进来大批请求,除了排队就是丢掉,响应时延会急剧升高。
令牌桶的逻辑刚好反过来。系统以恒定的速率往桶里放令牌,请求来的时候要先拿一个令牌才能被处理,拿不到就直接拒绝或者排队。桶有容量上限,说明允许攒令牌。如果系统长时间空闲,桶里攒满了令牌,等突发流量一到,它可以一次性消耗大量令牌,把攒着的能力都吐出来。这正是很多互联网接口想要的:平时保持低速率,遇到秒杀这种瞬时流量时又能顶上一阵子。
这个区别在选型时
