什么是限流?常见的限流算法有哪些?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 概念理解:面试官真正想看的是你有没有搞清楚它在高并发系统里的定位——和熔断、降级、隔离怎么区分,为什么要限流,不限流会怎样。

  2. 算法掌握深度:限流算法就那么几个(固定窗口、滑动窗口、漏桶、令牌桶),但每个算法的原理、优缺点、适用场景你能不能讲清楚,能不能画出图来,这才是拉开差距的地方。

  3. 工程落地能力:单机限流怎么做?分布式限流怎么做?SentinelGuava RateLimiterRedis + Lua 这些方案你用过哪些,踩过什么坑。这道题答得好不好,直接体现你有没有真正做过高并发系统。

核心答案

限流(Rate Limiting),简单说就是 在系统可能被打爆之前,主动控制进入系统的请求速率,保证系统在承受能力范围内稳定运行。它和熔断、降级一起被称为高可用的 "三板斧"。

举个接地气的例子:你开了家奶茶店,店里就 3 个店员,正常 1 分钟能做 10 杯。突然有一天排队来了 200 个人,全挤到吧台前面嚷嚷 "我的呢我的呢",店员手忙脚乱反而一杯都做不出来。限流就是门口摆个牌子:"每分钟只接 10 单,多了请排队或改天再来",保证这 10 单能稳稳地交付。

常见的限流算法有 4 种

算法 核心思路 优点 缺点 典型应用
固定窗口计数器 把时间切成固定窗口,窗口内计数 实现最简单 有 "临界点" 突刺问题 简单接口限流
滑动窗口计数器 把窗口切成更小的格子,滑动统计 解决临界突刺 实现稍复杂,精度依赖格子数 Sentinel 默认算法
漏桶算法 请求像水滴进桶,固定速率漏出 流量绝对平滑 无法应对合理突发 API 网关流量整形
令牌桶算法 固定速率往桶里放令牌,请求拿到令牌才通过 支持突发流量 实现相对复杂 Guava RateLimiter、Sentinel

下面挨个拆开讲。

高并发限流机制
高并发限流机制

深度解析

一、固定窗口计数器(Fixed Window Counter)

最直白的算法。把时间切成等长的窗口(比如每 1 分钟一个窗口),每个窗口维护一个计数器,来一个请求就 +1,超过阈值就拒绝。窗口结束,计数器清零。

固定窗口限流示意图
固定窗口限流示意图

上图是固定窗口的示意图。每个窗口相互独立,计数到 100 就拒绝后续请求,下一个窗口重新清零开始计数。

  • 优点:实现极其简单,一个 AtomicInteger 加个时间戳就能搞定
  • 致命缺点——临界突刺:在窗口切换的瞬间,流量可能翻倍。比如限流 100/分钟,0:59 秒突然来了 100 个请求,1:01 秒又来了 100 个请求——虽然两个窗口都没超阈值,但短短 2 秒内系统承受了 200 个请求,这就是经典的 "临界点" 问题。

固定窗口算法适合对精度要求不高的场景,比如简单的接口调用统计。

固定窗口临界突刺
固定窗口临界突刺

二、滑动窗口计数器(Sliding Window Counter)

滑动窗口就是来治这个毛病的。把一个大窗口切成多个小格子(bucket),每个格子独立计数,统计时 滑动地 把当前时间所在的若干小格子加起来。

滑动窗口限流示意图
滑动窗口限流示意图

上图展示了滑动窗口的逻辑:把 1 分钟拆成 4 个 15 秒的格子,统计当前窗口时,把当前时间往前回溯 1 分钟内的所有格子都加进来。窗口随着时间往前滑动,老格子失效,新格子加入。

  • 统计当前 QPS 时,是这样算的:把当前所在的小格子 + 前面几个还没过期的小格子加起来。窗口越细,统计越精准,但内存占用也越高
  • 优点:窗口滑动着统计,临界点这个毛病基本治住了(不会再出现瞬间翻倍的流量)
  • 缺点:格子越多精度越高,但内存和计算开销也越大,需要权衡
  • 典型实现Sentinel 默认就是用滑动窗口统计 QPS,它的 LeapArray 类把窗口切成两段(默认采样 2 个格子)

滑动窗口限流算法
滑动窗口限流算法

三、漏桶算法(Leaky Bucket)

漏桶的核心思想是 "恒定速率出水"。请求像水滴一样倒进桶里,桶底以固定速率漏出(处理)。桶满了就拒绝新请求。

漏桶限流算法
漏桶限流算法

上图的漏桶模型可以这样理解:

  • 入水:外部请求以任意速率到达,进入桶中

  • :容量有限,存的是还没处理的请求

  • 出水:底部以恒定速率处理请求,不管外面挤成什么样

  • 溢出:桶满了就拒绝新请求

  • 优点:能实现 绝对的流量整形——不管流量多汹涌,下游看到的永远是平滑的恒定速率

  • 缺点无法应对突发流量。即使系统此刻完全空闲,也只能按固定速率处理,突发请求被强行拖慢。这对某些需要 "能快就快" 的场景不友好

  • 典型应用:API 网关的流量整形(如 Nginx 的 limit_req 模块就是漏桶思想)

四、令牌桶算法(Token Bucket)

令牌桶是工程里用得最多的算法。核心思想是 "按固定速率往桶里放令牌,请求拿到令牌才能通过"

令牌桶限流算法
令牌桶限流算法

令牌桶的工作机制:

  • 令牌生成:后台以恒定速率往桶里放令牌

  • 令牌桶:容量有限,满了就丢弃新令牌

  • 请求处理:请求来的时候,先从桶里拿令牌,拿到了就通过,拿不到就拒绝或排队

  • 关键特性——突发支持:如果桶里攒了很多令牌(比如桶容量 100,当前攒了 80),突然来一批请求,可以瞬间处理掉这 80 个。这就是令牌桶最大的优势

  • 优点支持突发流量——只要桶里有令牌,请求就能立刻通过;同时通过令牌生成速率限制长期平均速率

  • 缺点:实现比漏桶复杂一点,参数调优要结合实际流量

  • 典型实现

    • GuavaRateLimiterSmoothBursty 平滑突发、SmoothWarmingUp 预热)
    • Sentinel 也支持令牌桶(WarmUpRateLimiterController

五、四种算法横向对比

维度 固定窗口 滑动窗口 漏桶 令牌桶
实现复杂度 极简 中等 中等 中等
临界突刺 严重 基本解决 完全平滑 允许突发
突发流量 不支持 不支持 不支持 支持
流量整形 强(恒定) 中(平均恒定)
分布式友好

一句话总结选型:追求平滑用漏桶,追求弹性用令牌桶,追求简单用固定窗口,工程实践多用滑动窗口和令牌桶。

漏桶令牌桶限流算法
漏桶令牌桶限流算法

六、工程落地——单机和分布式

单机限流在 JVM 内就能搞定,最常用的是 Guava RateLimiter

// 创建一个每秒放 10 个令牌的限流器(SmoothBursty)
RateLimiter limiter = RateLimiter.create(10);

// 方式一:阻塞等待
limiter.acquire(); // 拿到令牌立刻返回,拿不到就等

// 方式二:尝试获取(非阻塞)
if (limiter.tryAcquire()) {
    // 拿到令牌,处理请求
    handleRequest();
} else {
    // 拿不到,直接拒绝
    throw new RateLimitException("系统繁忙,请稍后重试");
}

代码注释说明:

  • RateLimiter.create(10) 创建一个令牌桶,每秒生成 10 个令牌
  • .acquire() 是阻塞式获取,会等待到拿到令牌为止,适合可以忍受延迟的内部调用
  • .tryAcquire() 是非阻塞式获取,立刻返回结果,适合网关或接口入口

但单机限流只能管住自己这台机器。集群环境下,如果 10 台机器各自限流 100 QPS,加起来就是 1000 QPS——很可能超出下游承受能力。这时候需要 分布式限流

分布式限流的经典实现是 Redis + Lua

-- key: 限流标识(如接口名 + 用户ID)
-- limit: 阈值
-- expire: 窗口过期时间(秒)
local current = redis.call('incr', KEYS[1])
if current == 1 then
    redis.call('expire', KEYS[1], ARGV[2])
end
if current > tonumber(ARGV[1]) then
    return 0  -- 限流
else
    return 1  -- 放行
end

Lua 脚本保证 "incr + expire + 判断" 是一个原子操作,避免并发下的竞态条件。生产环境更推荐用现成的轮子:

  • Sentinel:阿里开源,支持滑动窗口、令牌桶、预热、熔断降级一体,控制台可视化管理规则
  • Resilience4j:Spring 官方推荐,轻量级,函数式风格,支持 RateLimiter、CircuitBreaker 等
  • Nginx limit_req:网关层漏桶限流,挡在最前面

单机分布式限流
单机分布式限流

面试高频追问

  1. 追问一:限流、熔断、降级有什么区别?

    • 限流是 "主动防御"(流量进来前就挡),熔断是 "被动兜底"(下游已经挂了,主动切断调用),降级是 "有损服务"(返回兜底数据或简化逻辑)。三者配合使用,限流在入口,熔断在调用链,降级在末端。
  2. 追问二:令牌桶和漏桶到底怎么选?

    • 看业务是否能接受突发。比如秒杀场景下,瞬间高峰是合理的,用令牌桶攒令牌应对;如果是保护下游弱小的服务,希望下游永远平滑接收,用漏桶。
  3. 追问三:分布式限流为什么一定要用 Redis + Lua?

    • 因为分布式环境下的限流必须是 原子操作——多个节点同时来判断阈值,必须有一个集中的计数器。Redis 提供了这个集中存储,Lua 脚本保证 incr + expire + 判断不会被并发打断。
  4. 追问四:Sentinel 的滑动窗口是怎么实现的?

    • SentinelLeapArray 类管理窗口,默认把 1 秒切成 2 个 500ms 的格子(sampleCount=2intervalInMs=1000)。每个格子是一个 WindowWrap,独立持有 Bucket。统计 QPS 时聚合当前所在窗口内的有效格子。
  5. 追问五:如何选择单机限流还是分布式限流?

    • 入口网关层(保护整个集群)用分布式;应用内部(保护单个服务的处理能力)用单机。两者可以叠加,不冲突。

常见面试变体

  • "限流和熔断的区别是什么?"
  • "为什么需要限流?不限流会怎样?"
  • "Sentinel 内部用的是哪种限流算法?"
  • "如何设计一个支持动态调整阈值的限流方案?"
  • "分布式环境下,如何做全局限流?"

记忆口诀

四种算法一句话:固定窗口最简单(临界突刺)、滑动窗口拆格子(缓解突刺)、漏桶匀出流(绝对平滑不支持突发)、令牌桶匀进令牌(支持突发用得最多)。

选型口诀:追求平滑选漏桶,追求弹性选令牌,追求简单用固定,工程实战两板斧——滑动 + 令牌。

总结

限流是高可用系统的第一道防线。把四种算法的原理、优缺点搞透是基本功,再结合业务场景合理选型(单机用 Guava、集群用 Redis + Lua、企业级用 Sentinel)才是面试官真正想看的。面试时画图讲清原理,再结合实际项目讲讲为什么选这个算法、怎么调参的,基本稳过。