常见的限流算法有哪些?固定窗口、滑动窗口、漏桶、令牌桶怎么区别?
简化版
限流(Rate Limiting)是「限制单位时间内的请求量,保护系统不被流量打垮」——超过阈值的请求被拒绝或排队。四种经典算法:① 固定窗口计数器——把时间切成固定窗口(如每秒),每个窗口内计数,超过阈值就拒绝;简单但有「窗口临界问题」(窗口切换瞬间可能通过 2 倍流量);② 滑动窗口——把窗口切成更细的小格子、随时间滑动统计,解决了固定窗口的临界问题,更平滑;③ 漏桶(Leaky Bucket)——请求像水进桶,桶以恒定速率漏水(处理),桶满则拒绝;特点是「强制恒定输出速率」,能削峰、平滑流量,但应对突发流量不灵活(即使桶有空间也匀速处理);④ 令牌桶(Token Bucket)——以恒定速率往桶里放令牌,请求要拿到令牌才能通过,桶能攒令牌;特点是「允许一定突发」(桶里攒的令牌能让突发请求一次性通过),又能限制平均速率,是最常用的。核心区别:固定/滑动窗口是「计数」思路;漏桶「恒定输出、削峰」;令牌桶「恒定放令牌、允许突发」——令牌桶最灵活最常用。
详细版
四种算法对比:
| 算法 | 思路 | 突发流量 | 特点 |
|---|---|---|---|
| 固定窗口 | 每个时间窗口计数 | 有临界问题 | 简单,但窗口边界可能 2 倍流量 |
| 滑动窗口 | 细分窗口滑动统计 | 较平滑 | 解决临界问题,实现稍复杂 |
| 漏桶 | 恒定速率漏水处理 | 不允许突发(匀速) | 强制平滑输出、削峰 |
| 令牌桶 | 恒定放令牌、拿令牌通过 | 允许突发(攒令牌) | 最灵活、最常用 |
固定窗口的临界问题:
限流 100/秒。第 1 秒的后 0.5 秒来 100 个(通过),
第 2 秒的前 0.5 秒又来 100 个(通过)
→ 这 1 秒内(跨窗口)实际通过了 200 个!(超了 2 倍)
令牌桶(允许突发): 漏桶(强制匀速):
以 10/秒放令牌,桶容量 100 桶匀速漏 10/秒
平时攒着令牌 进来多少不管,出去恒定 10/秒
突发时:一次拿走攒的 100 个令牌 突发 100 个:桶存着,还是 10/秒慢慢处理
→ 允许瞬时突发到 100 → 输出永远平滑 10/秒
之后回到 10/秒(放令牌速率)
⚠️ 令牌桶和漏桶最容易搞混,记住核心区别:「令牌桶允许突发、漏桶强制匀速」。漏桶是「出口恒定」——不管进来多少请求,都以固定速率处理(桶满则丢弃),像一个匀速漏水的桶,输出永远平滑;它的优点是能削峰、保护下游(下游收到的永远是恒定速率),缺点是不灵活(即使系统有能力处理突发,漏桶也强制匀速,浪费了突发处理能力)。令牌桶是「入口攒令牌」——以恒定速率往桶里放令牌,请求消耗令牌,桶能攒令牌;平时不忙时令牌攒着,突发来了可以一次性消耗攒下的令牌、瞬时通过一批,之后回到放令牌的速率;它既限制了平均速率(放令牌速率),又允许一定突发(攒的令牌),更符合实际需求(真实流量常有突发)。所以令牌桶是最常用的(如 Guava RateLimiter、Sentinel 都用它),漏桶用于「必须强制恒定输出」的场景。
完整版教学
一、为什么需要限流
先理解限流的必要性:
系统的处理能力是有限的:
一个服务能扛的 QPS 有上限(受 CPU、内存、下游依赖限制)
超过上限:请求堆积、响应变慢、资源耗尽 → 雪崩
流量可能突然暴增:
秒杀、热点事件、爬虫、恶意攻击(DDoS)
→ 瞬时流量远超系统承受能力
如果不限流:
流量打垮系统 → 服务不可用 → 甚至拖垮整个链路(雪崩)
限流的作用:
限制单位时间内的请求量,超过阈值的拒绝/排队
→ 保护系统在承受能力内运行("我只处理我能处理的,多的拒绝")
→ 牺牲部分请求(被限流的),保住整体可用(不被打垮)
限流 vs 熔断 vs 降级:
限流:控制入口流量(不让太多进来)
熔断:下游故障时,快速失败(不再调用故障的下游)
降级:资源不足时,牺牲非核心功能
→ 限流是"入口保护",主动控制流量
限流的必要性:系统处理能力有限(QPS 有上限、超过则请求堆积响应变慢资源耗尽雪崩),流量可能突增(秒杀/热点/攻击)。不限流会「流量打垮系统、甚至雪崩」。限流的作用是「限制单位时间请求量、超阈值拒绝/排队、保护系统在承受能力内运行」(牺牲部分请求保住整体可用)。限流 vs 熔断 vs 降级:限流控制入口流量、熔断下游故障快速失败、降级牺牲非核心功能。理解「限流必要性:系统能力有限+流量可能突增、不限流会打垮系统雪崩、限流限制请求量保护系统(牺牲部分保整体);限流是入口保护 vs 熔断下游故障 vs 降级牺牲非核心」,就理解了限流的必要性。
二、固定窗口计数器
固定窗口——最简单,但有临界问题:
固定窗口计数器:
把时间切成固定的窗口(如每 1 秒一个窗口)
每个窗口内维护一个计数器
每来一个请求:计数器 +1
计数器 ≤ 阈值 → 通过
计数器 > 阈值 → 拒绝
窗口结束 → 计数器清零,开始新窗口
实现简单:
一个计数器 + 一个时间窗口,好实现
★ 临界问题(固定窗口的缺陷):
限流 100/秒
第 1 秒的最后 0.5 秒:来 100 个请求(当前窗口计数 100,通过)
第 2 秒的前 0.5 秒:又来 100 个(新窗口,计数从 0 开始,通过)
→ 在"第 1 秒后半 + 第 2 秒前半"这连续 1 秒内
实际通过了 200 个请求!(超了限流 2 倍)
→ 窗口边界处可能瞬时通过 2 倍流量
原因:
固定窗口在边界处"突变"(计数器清零)
跨越边界的两个半窗口的流量叠加,可能超限
所以固定窗口简单,但边界不平滑(有临界问题)
固定窗口计数器——把时间切成固定窗口(每 1 秒),每窗口计数(≤阈值通过、>阈值拒绝,窗口结束清零)。实现简单(一个计数器+时间窗口)。临界问题(缺陷):限流 100/秒,第 1 秒后 0.5 秒来 100 个(通过)、第 2 秒前 0.5 秒又来 100 个(新窗口计数从 0、通过),这连续 1 秒内实际通过 200 个(超 2 倍)。原因是窗口边界处计数器突变清零、跨边界的两个半窗口流量叠加。理解「固定窗口:时间切窗口每窗口计数超阈值拒绝、简单;★临界问题:窗口边界处两个半窗口流量叠加可能瞬时通过 2 倍、因边界计数器突变清零」,就掌握了固定窗口。
三、滑动窗口
滑动窗口——解决固定窗口的临界问题:
滑动窗口:
把大窗口切成更细的小格子,窗口随时间"滑动"
限流 100/秒,把 1 秒切成 10 个 100ms 的小格子
每个小格子有自己的计数
统计"当前时刻往前 1 秒"的所有小格子计数之和
≤ 100 → 通过,> 100 → 拒绝
随时间推进,窗口向前滑动(丢弃最老的格子、加入新格子)
为什么解决临界问题:
固定窗口:边界处突变(整个计数器清零)
滑动窗口:窗口平滑滑动(只丢弃最老的一小格)
→ "当前往前 1 秒"始终是连续统计的
→ 跨越边界的流量也被正确计入 → 不会有 2 倍突变
粒度越细越平滑:
格子越多(如 1 秒切 100 个 10ms 格子),统计越精确、越平滑
但格子越多,存储和计算成本越高
实现:
滑动窗口计数器(数组存各小格计数)
或滑动日志(记每个请求的时间戳,统计窗口内的数量,更精确但更耗内存)
所以滑动窗口比固定窗口平滑,代价是实现和存储稍复杂
滑动窗口——解决固定窗口的临界问题:把大窗口切成更细的小格子、窗口随时间滑动。限流 100/秒切成 10 个 100ms 小格,统计「当前往前 1 秒」所有小格之和(≤100 通过),随时间滑动(丢最老格、加新格)。为什么解决临界:固定窗口边界突变清零、滑动窗口平滑滑动(只丢最老一小格),「当前往前 1 秒」始终连续统计、跨边界流量正确计入、不会 2 倍突变。粒度越细越平滑但成本越高。实现:滑动窗口计数器或滑动日志(记时间戳更精确但耗内存)。理解「滑动窗口:大窗口切小格随时间滑动、统计当前往前 1 秒之和、解决临界问题(平滑滑动不突变、跨边界正确计入)、粒度越细越平滑但成本高」,就掌握了滑动窗口。
四、漏桶算法
漏桶(Leaky Bucket)——强制恒定输出、削峰:
漏桶算法:
想象一个底部有洞的桶:
请求像水一样流进桶
桶底以"恒定速率"漏水(处理请求)
桶满了 → 溢出(拒绝多余请求)
特点:
① 出口恒定——不管进来多少,都以固定速率处理
② 桶起缓冲作用——突发进来的先存桶里,慢慢匀速处理
③ 桶满则拒绝
效果:输出流量永远是平滑的恒定速率
进来忽高忽低,出去永远匀速
优点:
① 强制平滑输出(削峰)——保护下游(下游收到恒定速率)
② 稳定——输出速率可控、不波动
缺点:
① 不允许突发——即使系统有能力、桶有空间,也强制匀速处理
→ 浪费了系统的突发处理能力
② 突发请求要排队等待(延迟增加)
适用:
需要"强制恒定输出速率"的场景
如:调用有严格速率限制的下游 API(下游要求匀速)
需要平滑流量、保护下游不被突发冲击
漏桶 = 强制匀速 = 削峰但不灵活
漏桶(Leaky Bucket)——底部有洞的桶:请求像水流进桶、桶以恒定速率漏水(处理)、桶满溢出(拒绝)。特点:出口恒定(不管进来多少都固定速率处理)、桶缓冲(突发先存桶里慢慢匀速处理)、桶满拒绝。效果:输出永远平滑恒定速率。优点:强制平滑输出削峰(保护下游)、稳定。缺点:不允许突发(即使系统有能力也强制匀速、浪费突发处理能力)、突发请求排队增加延迟。适用「必须强制恒定输出」(调用有严格速率限制的下游 API、平滑流量保护下游)。理解「漏桶:请求流进桶、恒定速率漏水处理、桶满拒绝、输出永远平滑恒定;优点削峰保护下游、缺点不允许突发(强制匀速浪费突发能力);适用需强制恒定输出保护下游」,就掌握了漏桶。
五、令牌桶算法
令牌桶(Token Bucket)——允许突发、最常用:
令牌桶算法:
一个桶,以"恒定速率"往桶里放令牌(如 10 个/秒)
桶有容量上限(如 100,攒满就不再放)
每来一个请求:要从桶里拿一个令牌
拿到令牌 → 通过
桶里没令牌 → 拒绝(或等待)
特点(关键):
① 平时放令牌,请求少时令牌攒在桶里(最多攒到桶容量)
② 突发来了:可以一次性消耗攒下的令牌 → 瞬时通过一批
③ 令牌消耗完 → 回到"放令牌速率"(如 10/秒)
效果:
① 限制了平均速率(放令牌的速率,如 10/秒)
② 允许一定突发(桶里攒的令牌,最多突发到桶容量)
对比漏桶(核心区别):
漏桶:出口恒定,不允许突发(强制匀速)
令牌桶:入口攒令牌,允许突发(攒的令牌一次性用)
→ 令牌桶更灵活(既限平均速率,又允许突发)
为什么令牌桶最常用:
真实流量常有突发(不是绝对匀速)
令牌桶允许"平时攒、突发用",符合实际
→ 既保护系统(限平均速率),又不浪费突发能力
实现:
Guava RateLimiter(令牌桶)、Sentinel、Redis + Lua 等
→ 主流限流工具大多用令牌桶
令牌桶(Token Bucket)——以恒定速率往桶放令牌(10/秒),桶有容量上限,请求要拿令牌才通过(没令牌拒绝/等待)。特点:平时放令牌攒在桶里、突发来了一次性消耗攒下的令牌瞬时通过一批、消耗完回到放令牌速率。效果:限制平均速率(放令牌速率)+ 允许突发(攒的令牌)。对比漏桶(核心区别):漏桶出口恒定不允许突发(强制匀速)、令牌桶入口攒令牌允许突发(更灵活)。为什么最常用:真实流量常有突发,令牌桶「平时攒、突发用」符合实际(既保护系统又不浪费突发能力)。实现:Guava RateLimiter、Sentinel、Redis+Lua。理解「令牌桶:恒定速率放令牌、请求拿令牌通过、平时攒突发用(一次性消耗攒的令牌瞬时通过一批);限平均速率+允许突发;对比漏桶:令牌桶允许突发(入口攒令牌)vs 漏桶强制匀速(出口恒定);最常用(符合真实突发流量)」,就掌握了令牌桶。
六、选择与实践
综合对比,给出选择和实践:
四种算法的选择:
简单场景、能接受临界问题 → 固定窗口(最简单)
要平滑、解决临界问题 → 滑动窗口
要强制恒定输出、削峰保护下游 → 漏桶
要限平均速率又允许突发(最常用)→ 令牌桶
漏桶 vs 令牌桶(最常问):
漏桶:强制匀速输出(不允许突发)—— 保护下游、削峰
令牌桶:允许突发(攒令牌)—— 更灵活、更常用
→ 实际中令牌桶用得多(真实流量有突发)
限流的维度:
① 单机限流:单个实例限流(Guava RateLimiter、Sentinel 单机)
② 分布式限流:整个集群限流(Redis + Lua、Sentinel 集群)
→ 分布式要共享计数/令牌(用 Redis)
限流的粒度:
全局限流、按接口限流、按用户/IP 限流、按参数(热点参数)限流
主流工具:
Guava RateLimiter:单机令牌桶
Sentinel:限流为核心(QPS/线程数、单机/集群、热点参数)
Redis + Lua:分布式限流(原子性)
网关限流(Spring Cloud Gateway、Nginx limit_req)
实践建议:
① 令牌桶是通用首选(限平均 + 允许突发)
② 分布式限流用 Redis + Lua(保证原子性)
③ 网关做统一入口限流
④ 限流后要有友好降级(返回"稍后重试",别裸报错)
四种算法选择:简单能接受临界用固定窗口、要平滑用滑动窗口、要强制恒定输出削峰用漏桶、要限平均速率又允许突发(最常用)用令牌桶。漏桶 vs 令牌桶(最常问):漏桶强制匀速削峰保护下游、令牌桶允许突发更灵活更常用。限流维度:单机限流(Guava RateLimiter/Sentinel)、分布式限流(Redis+Lua/Sentinel 集群,共享计数用 Redis)。粒度:全局/按接口/按用户 IP/按热点参数。工具:Guava RateLimiter(单机令牌桶)、Sentinel(限流核心)、Redis+Lua(分布式)、网关限流。实践:令牌桶首选、分布式用 Redis+Lua、网关统一限流、限流后友好降级。理解「选择:简单用固定窗口/平滑用滑动窗口/削峰用漏桶/允许突发用令牌桶(最常用);漏桶匀速 vs 令牌桶允许突发;单机 vs 分布式限流(Redis+Lua);工具 Guava/Sentinel/Redis;实践令牌桶首选+分布式 Redis+网关限流+友好降级」,就掌握了选择和实践。
记忆钩子:「限流算法四种:①固定窗口计数器(时间切窗口每窗口计数超阈值拒绝,简单但★临界问题:窗口边界两个半窗口流量叠加可能瞬时 2 倍)②滑动窗口(大窗口切小格随时间滑动、统计当前往前 1 秒之和、解决临界问题更平滑)③漏桶(请求流进桶、恒定速率漏水处理、桶满拒绝、★强制匀速输出削峰保护下游、缺点不允许突发)④令牌桶(恒定速率放令牌、请求拿令牌通过、★允许突发:平时攒令牌突发一次性用、限平均速率又允许突发、最常用 Guava/Sentinel);漏桶 vs 令牌桶核心区别:漏桶强制匀速(出口恒定)、令牌桶允许突发(入口攒令牌);分布式限流用 Redis+Lua」。
七、常见误区与追问
- 误区:固定窗口计数器能精确限流。 有临界问题——窗口边界处,前一个窗口的后半段和后一个窗口的前半段流量会叠加,可能在连续的一个窗口时长内通过约 2 倍的流量;要解决用滑动窗口(平滑统计「当前往前一个窗口」的流量)。
- 误区:漏桶和令牌桶都能应对突发流量。 核心区别在这——漏桶强制恒定输出(不管进来多少都匀速处理,不允许突发,即使系统有能力也匀速);令牌桶允许突发(平时攒令牌、突发时一次性消耗攒下的令牌瞬时通过一批);令牌桶才能应对突发,漏桶是强制匀速。
- 误区:漏桶比令牌桶好,因为输出更平滑。 各有适用——漏桶输出平滑(削峰、保护下游收到恒定速率),但不允许突发(浪费系统的突发处理能力);令牌桶允许突发、更灵活,符合真实流量常有突发的特点,是最常用的;「更平滑」不等于「更好」,看是否需要强制恒定输出。
- 误区:单机限流和分布式限流一样。 不同——单机限流每个实例各自限(如 Guava RateLimiter,10 个实例每个限 100/秒,总共可能 1000/秒);分布式限流是整个集群统一限(如 Redis+Lua 共享计数/令牌,10 个实例共享一个 100/秒的额度);要控制集群总流量必须用分布式限流。
- 追问:令牌桶和漏桶算法有什么区别? 漏桶:请求进桶、桶以恒定速率漏水(处理)、桶满拒绝,输出永远是平滑的恒定速率,不允许突发(即使桶有空间也匀速处理);令牌桶:以恒定速率往桶放令牌、请求拿到令牌才通过、桶能攒令牌,平时不忙时攒令牌、突发来了可以一次性消耗攒下的令牌瞬时通过一批,既限平均速率(放令牌速率)又允许一定突发(攒的令牌);核心区别是「漏桶强制匀速、令牌桶允许突发」,令牌桶更灵活更常用。
- 追问:固定窗口的临界问题是怎么产生的,怎么解决? 固定窗口在边界处计数器突变清零——限流 100/秒,如果第 1 秒的后半段来 100 个(当前窗口通过)、第 2 秒的前半段又来 100 个(新窗口计数从 0 开始通过),那么在「跨越边界的连续 1 秒」内实际通过了 200 个(超 2 倍);解决用滑动窗口,把窗口切成更细的小格、随时间平滑滑动,统计「当前时刻往前一个窗口」的流量(连续统计、不突变),跨边界的流量也被正确计入。
- 追问:分布式环境下怎么做限流? 需要共享计数/令牌状态——常用 Redis + Lua 脚本:把限流的计数或令牌桶状态存在 Redis 里,用 Lua 脚本保证「读取计数 + 判断 + 增加」的原子性(避免并发下计数不准),所有实例都访问同一个 Redis 做限流判断;或用 Sentinel 的集群限流(token server 统一发令牌);关键是状态共享 + 操作原子性。
八、加强记忆
限流(Rate Limiting)= 限制单位时间内的请求量、超阈值拒绝/排队,保护系统不被打垮。四种经典算法:① 固定窗口计数器——时间切固定窗口、每窗口计数超阈值拒绝(简单,但临界问题:窗口边界两个半窗口流量叠加可能瞬时通过 2 倍);② 滑动窗口——大窗口切细小格、随时间滑动、统计「当前往前一个窗口」之和(解决临界问题、更平滑,实现稍复杂);③ 漏桶(Leaky Bucket)——请求流进桶、桶以恒定速率漏水处理、桶满拒绝(强制匀速输出、削峰保护下游,缺点不允许突发、即使系统有能力也匀速);④ 令牌桶(Token Bucket)——恒定速率往桶放令牌、请求拿令牌才通过、桶能攒令牌(允许突发:平时攒令牌、突发时一次性消耗攒下的令牌瞬时通过一批,既限平均速率又允许突发,最常用,Guava RateLimiter/Sentinel)。漏桶 vs 令牌桶(核心区别):漏桶强制匀速(出口恒定)、令牌桶允许突发(入口攒令牌)——令牌桶更灵活、符合真实突发流量。单机限流(Guava/Sentinel)vs 分布式限流(Redis+Lua 共享状态保证原子性)。一句话「限流四算法:固定窗口(每窗口计数,有临界问题瞬时2倍)、滑动窗口(小格滑动解决临界更平滑)、漏桶(恒定速率漏水、强制匀速削峰、不允许突发)、令牌桶(恒定放令牌、允许突发攒令牌一次性用、限平均又允许突发、最常用);漏桶强制匀速 vs 令牌桶允许突发;分布式限流用 Redis+Lua」。