news 2026/8/6 3:00:49

高并发下的流量控制:四种限流算法解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
高并发下的流量控制:四种限流算法解析

高并发下的流量控制:四种限流算法解析

目录

  • 为什么需要限流
  • 固定窗口计数器
  • 滑动窗口
  • 漏桶
  • 令牌桶
  • 四种算法对比
  • 怎么选
  • 小结

很多接口在正常运行时压力并不大,但当请求量突然上涨时,系统的瓶颈很快就会暴露出来。这种情况可能发生在活动期间用户集中访问,也可能是某个接口被异常流量持续调用。如果入口没有任何流量控制,请求会持续进入业务系统,最终可能导致线程池耗尽、数据库连接池打满,甚至影响正常请求的处理。

为什么需要限流

一种常见的解决方式是在系统入口增加限流机制。

限流并不是单纯拒绝请求,而是在系统入口增加流量控制,根据当前处理能力决定请求是否继续进入。

例如,一个服务经过压测后发现稳定处理能力约为 500 QPS,那么入口层就需要限制请求进入速度,避免超过系统承载范围。以下介绍四种限流算法:

固定窗口计数器

固定窗口是最基础的限流算法之一。它将时间划分为多个固定长度的窗口,并分别统计每个窗口内的请求数量。

比如限制每分钟最多 100 个请求,系统会维护一个一分钟窗口:

00:00 - 01:00 count = 80 01:00 - 02:00 count = 35

请求到达后,先确定当前所属窗口,然后增加该窗口的计数。如果计数超过限制,则拒绝请求。

publicclassFixedWindowLimiter{privatefinalintlimit;privatefinallongwindowSize;privatelongwindowStart;privateintcount;publicsynchronizedbooleantryAcquire(){longnow=System.currentTimeMillis();if(now-windowStart>=windowSize){windowStart=now;count=0;}if(count>=limit){returnfalse;}count++;returntrue;}}

实现非常简单,通常只需要维护一个计数器和窗口开始时间。但固定窗口存在一个明显问题:窗口边界可能导致流量突增

假设限制一分钟最多 100 个请求。如果请求集中在窗口末尾和下一个窗口开始的位置,固定窗口可能允许短时间内通过两倍于限制的请求。

对于普通查询接口或者后台管理接口,这种误差通常可以接受。但对于支付、交易等对流量精度要求较高的场景,窗口突变可能带来风险。

滑动窗口

固定窗口的问题在于它只看当前窗口内的计数,不关心窗口边界附近的请求。滑动窗口的思路是:任何时刻都统计最近一段时间内的请求数量,窗口随着时间"滑动"。

日志版

最准确的做法是保存每一次请求的时间戳。当前时间 12:01:30,窗口大小一分钟,那就统计 12:00:30 到 12:01:30 之间有多少请求。

publicclassSlidingWindowLogLimiter{privatefinalintlimit;privatefinallongwindowSizeMs;privatefinalLinkedList<Long>timestamps=newLinkedList<>();publicsynchronizedbooleantryAcquire(){longnow=System.currentTimeMillis();longwindowStart=now-windowSizeMs;while(!timestamps.isEmpty()&&timestamps.getFirst()<=windowStart){timestamps.removeFirst();}if(timestamps.size()<limit){timestamps.addLast(now);returntrue;}returnfalse;}}

精度没问题,但内存开销是个现实问题。如果接口 QPS 是 10000,一分钟窗口意味着链表里常驻 60 万个时间戳。单机限流还好,分布式限流拿 Redis 存的话,这个内存成本就比较高了。

计数器版

工程中更常用的是折中方案:把窗口切成几个小段,只保存每个小段的计数,用加权计算近似滑动窗口内的总量。

计算公式:

请求数 ≈ 上一个窗口计数 × (1 - 当前窗口已过时间占比) + 当前窗口计数

比如当前时间是 01:00:30,上一个窗口计数 80,当前窗口计数 30:

已过时间占比 = 30秒 / 60秒 = 0.5 请求数 ≈ 80 × 0.5 + 30 = 70

70 没超过阈值 100,放行。这种方式只需要两个计数器和一个时间戳,内存固定,精度比固定窗口高,大部分场景够用。

实际项目中,Sentinel 的限流统计用的就是滑动窗口计数器的思路,把一个窗口切成多个样本(sample),每个样本记录计数和起始时间,滑动时丢弃过期样本、加入新样本。

漏桶

前面两种方案都是在"数请求数量"。漏桶换个角度:控制请求的处理速率

水从桶上方灌进去,桶底有个小孔,水以固定速率往外漏。灌水速度不管多快,漏水速度始终恒定。桶满了,水溢出。

漏桶的特点是输出速率恒定,不管上游怎么突发,下游看到的永远是匀速流量。这种能力叫流量整形

实现上和令牌桶很像,也是记录"上次处理时间"和"当前水量",但逻辑是反过来的:令牌桶是往里加令牌,漏桶是从里往外漏水。

publicclassLeakyBucketLimiter{privatefinalintcapacity;privatefinaldoubleleakRate;// 每秒漏出几个privatedoublewater;privatelonglastLeakTime;publicsynchronizedbooleantryAcquire(){longnow=System.currentTimeMillis();doubleleaked=(now-lastLeakTime)/1000.0*leakRate;water=Math.max(0,water-leaked);lastLeakTime=now;if(water<capacity){water+=1;returntrue;}returnfalse;}}

漏桶适合的场景是下游处理能力固定。比如数据库连接池最多处理 100 个并发,不管上游来了多少请求,都得排成匀速队列进去,否则连接池直接被打满。

但漏桶也有个明显的缺点:它不区分"突发的合理请求"和"恶意刷流量"。系统明明有余力处理短时突发,漏桶也会把输出削平,让请求在外面排队。对用户来说就是"明明系统没压力,我的请求却被限流了"。

令牌桶

令牌桶解决的正是漏桶"过于保守"的问题。

思路是反过来的:桶里装的不是请求,而是令牌。系统以固定速率往桶里放令牌,桶有容量上限。请求到达时拿一个令牌就放行,拿不到就拒绝。

和漏桶的关键区别在这里:如果桶里积累了令牌,突发请求可以一次性消耗掉所有存量

举个例子。令牌桶容量 50,每秒生成 10 个令牌。平时流量不大,桶里慢慢积了 50 个令牌。突然来了一波活动,瞬间到了 50 个请求。这 50 个请求各自拿到一个令牌,全部放行。漏桶做不到这一点——它会把 50 个请求排成匀速队列,慢慢处理。

publicclassTokenBucketLimiter{privatefinalintcapacity;privatefinaldoublerefillRate;privatedoubletokens;privatelonglastRefillTime;publicTokenBucketLimiter(intcapacity,doublerefillRate){this.capacity=capacity;this.refillRate=refillRate;this.tokens=capacity;this.lastRefillTime=System.currentTimeMillis();}publicsynchronizedbooleantryAcquire(){longnow=System.currentTimeMillis();doublenewTokens=(now-lastRefillTime)/1000.0*refillRate;tokens=Math.min(capacity,tokens+newTokens);lastRefillTime=now;if(tokens>=1){tokens-=1;returntrue;}returnfalse;}}

Java 生态里最常用的令牌桶实现是 Guava 的RateLimiter

// 每秒 100 个请求RateLimiterlimiter=RateLimiter.create(100.0);if(limiter.tryAcquire()){// 放行}else{// 拒绝}

Guava 的RateLimiter还有一个容易被忽略的特性:预消费(warmup)RateLimiter.create(100.0)创建的是平滑限流器,令牌生成速率恒定。但RateLimiter.create(100.0, Duration.ofSeconds(10))创建的是预热限流器,启动阶段令牌生成速率会从低到高逐渐爬升,10 秒后达到满速。

预热的意义在于:服务刚启动时,各种缓存都是冷的,JIT 还没优化,处理能力比稳态差很多。如果一开始就放满流量,很容易被直接打挂。预热限流器让流量逐渐增加,给服务一个缓冲期。这个细节在很多限流文章里不会提到,但线上踩过坑的人都知道它的重要性。

四种算法对比

维度固定窗口滑动窗口漏桶令牌桶
突发处理边界处可能 2 倍突变精确控制严格匀速允许突发(受桶容量限制)
输出速率不稳定不稳定恒定平均恒定,允许瞬时波动
内存开销极低日志版高,计数器版低
实现复杂度最简单中等中等中等
典型应用简单接口防护Sentinel、API 网关流量整形、带宽控制Guava RateLimiter、API 限流

四种算法怎么选

实际项目中,限流方案的选择通常不是从算法本身出发,而是先分析系统需要解决什么问题。

如果只是限制接口调用频率,例如后台管理接口防刷、Open API 调用次数控制,固定窗口通常已经足够。它实现简单,性能开销低,窗口边界带来的误差在这类场景下影响有限。

如果需要精确控制用户、IP 等维度的访问频率,例如限制某个用户一分钟最多调用 60 次,滑动窗口会更加合适。相比固定窗口,它能够减少窗口边界导致的流量突增问题。像 Sentinel 这类限流组件,也提供了基于滑动窗口的统计方式。

如果重点是保护下游资源,例如数据库连接池、第三方接口调用配额等,更关注的是控制请求进入速度。漏桶可以将不稳定的流量转换为稳定的输出,避免下游服务被瞬间压垮。

如果希望限制长期平均速率,同时允许业务存在一定程度的突发流量,令牌桶通常是更常见的选择。它既能控制整体访问速度,又能利用桶中积累的令牌承接短时间流量波动。

实际生产环境中,限流通常也不是只使用一种算法。比如一个 API 网关可能会同时设置多层限制:全局使用令牌桶控制整体 QPS,用户维度使用滑动窗口限制访问频率,针对特殊接口再增加固定窗口作为保护措施。

小结

每种限流算法都在精确度、突发处理能力和实现复杂度之间做了不同取舍。实际选择时,需要结合系统面对的问题:是需要应对突发流量,还是保护下游资源,或者限制单个用户的访问频率。明确业务目标后,算法选择自然会更加清晰。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/6 2:58:03

Redis五大核心数据结构解析与实战应用

1. Redis数据结构的重要性与学习路径Redis作为当今最流行的内存数据库之一&#xff0c;其卓越性能很大程度上源于精心设计的数据结构体系。我接触Redis已有7年时间&#xff0c;从最初只是简单使用String类型做缓存&#xff0c;到现在能够根据业务场景灵活组合各种数据结构&…

作者头像 李华
网站建设 2026/8/6 2:55:23

Python 多继承 MRO、Pillow 与 Tkinter 模块详解

1. Python 多继承与 MRO&#xff08;方法解析顺序&#xff09;在 Python 中&#xff0c;一个类可以继承自多个父类&#xff0c;这就是多继承。当多个父类中存在同名方法或属性时&#xff0c;Python 需要一套规则来决定调用哪一个&#xff0c;这套规则就是 方法解析顺序&#xf…

作者头像 李华
网站建设 2026/8/6 2:54:49

一文读懂 RAG 混合检索:从切块、BM25、RRF 到 Qdrant 代码实践

当大模型需要回答企业制度、产品文档、技术手册等私有知识时&#xff0c;真正决定回答质量的&#xff0c;往往不只是模型有多强&#xff0c;而是系统能否先找到正确、完整、可追溯的资料。 一、RAG 到底解决什么问题 RAG 的全称是 Retrieval-Augmented Generation&#xff0c;…

作者头像 李华
网站建设 2026/8/6 2:52:51

依赖注入模式:从硬编码到松耦合的架构设计实践

1. 依赖注入模式&#xff1a;从“硬编码”到“软连接”的架构跃迁如果你写过一些规模稍大的代码&#xff0c;肯定遇到过这样的场景&#xff1a;一个类OrderService需要调用PaymentProcessor来处理支付&#xff0c;于是你顺手就在OrderService的构造函数里new了一个PaymentProce…

作者头像 李华