本文的 原始地址 ,传送门
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
10wqps自适应限流?怎么解决的?
你们项目中,怎么限流的?
所以,这里尼恩给大家做一下系统化、体系化的梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”。也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典PDF》V145版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,后台回复:领电子书
问题说明:
考察对限流算法的掌握情况
限流:是大厂面试、高P面试的核心面试题
注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请到文末《技术自由圈》公号获取
为什么要限流
简单来说:
限流在很多场景中用来限制并发和请求量,比如说秒杀抢购,保护自身系统和下游系统不被巨型流量冲垮等。
以微博为例,例如某某明星公布了恋情,访问从平时的50万增加到了500万,系统的规划能力,最多可以支撑200万访问,那么就要执行限流规则,保证是一个可用的状态,不至于服务器崩溃,所有请求不可用。
限流的思想
在保证可用的情况下尽可能多增加进入的人数,其余的人在排队等待,或者返回友好提示,保证里面的进行系统的用户可以正常使用,防止系统雪崩。
日常生活中,有哪些需要限流的地方?
像我旁边有一个国家景区,平时可能根本没什么人前往,但是一到五一或者春节就人满为患,这时候景区管理人员就会实行一系列的政策来限制进入人流量,
为什么要限流呢?
假如景区能容纳一万人,现在进去了三万人,势必摩肩接踵,整不好还会有事故发生,这样的结果就是所有人的体验都不好,如果发生了事故景区可能还要关闭,导致对外不可用,这样的后果就是所有人都觉得体验糟糕透了。
本地限流/本地限流的四大算法
限流算法很多,常见的有四大算法,分别是计数器算法、滑动窗口算法、 漏桶算法、令牌桶算法,下面逐一讲解。
限流的手段通常有计数器、漏桶、令牌桶。注意限流和限速(所有请求都会处理)的差别,视
业务场景而定。
(1)计数器:
也叫做 固定窗口算法。
在一段时间间隔内(时间窗/时间区间),处理请求的最大数量固定,超过部分不做处理。
(2)滑动窗口算法
滑动窗口限流是对固定窗口限流算法的一种改进。
在固定窗口限流中,时间窗口是固定划分的,而滑动窗口限流将时间窗口划分为多个更小的子窗口,随着时间的推移,窗口会不断地滑动。
在统计请求数量时,会统计当前滑动窗口内所有子窗口的请求总和,当请求数量超过预设的阈值时,就拒绝后续的请求。
(3)漏桶:
漏桶大小固定,处理速度固定,但请求进入速度不固定(在突发情况请求过多时,会丢弃过多的请求)。
(4)令牌桶:
令牌桶的大小固定,令牌的产生速度固定,但是消耗令牌(即请求)速度不固定(可以应对一些某些时间请求过多的情况);
每个请求都会从令牌桶中取出令牌,如果没有令牌则丢弃该次请求。
计数器限流(固定窗口限流)
图解:计数器限流原理
在一段时间间隔内(时间窗/时间区间),处理请求的最大数量固定,超过部分不做处理。
计数器限流 也叫做 固定窗口算法, 是一种简单直观的限流算法,其原理是将时间划分为固定大小的窗口,在每个窗口内限制请求的数量或速率。
计数器算法是限流算法里最简单也是最容易实现的一种算法。
计数器限流 具体实现时,可以使用一个计数器来记录当前窗口内的请求数,并与预设的阈值进行比较。
计数器限流 的原理如下:
-
将时间划分固定大小窗口,例如每秒一个窗口。
-
在每个窗口内,记录请求的数量。
-
当有请求到达时,将请求计数加一。
-
如果请求计数超过了预设的阈值(比如3个请求),拒绝该请求。
-
窗口结束后,重置请求计数。

举个例子,比如我们规定对于A接口,我们1分钟的访问次数不能超过100个。
那么我们可以这么做:
- 在一开 始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内,那么说明请求数过多,拒绝访问;
- 如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter,就是这么简单粗暴。

计数器限流(固定窗口限流)限流的实现
package com.crazymaker.springcloud.ratelimit;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
// 计速器 限速
@Slf4j
public class CounterLimiter
{
// 起始时间
private static long startTime = System.currentTimeMillis();
// 时间区间的时间间隔 ms
private static long interval = 1000;
// 每秒限制数量
private static long maxCount = 2;
//累加器
private static AtomicLong accumulator = new AtomicLong();
// 计数判断, 是否超出限制
private static long tryAcquire(long taskId, int turn)
{
long nowTime = System.currentTimeMillis();
//在时间区间之内
if (nowTime < startTime + interval)
{
long count = accumulator.incrementAndGet();
if (count <= maxCount)
{
return count;
} else
{
return -count;
}
} else
{
//在时间区间之外
synchronized (CounterLimiter.class)
{
log.info("新时间区到了,taskId{}, turn {}..", taskId, turn);
// 再一次判断,防止重复初始化
if (nowTime > startTime + interval)
{
accumulator.set(0);
startTime = nowTime;
}
}
return 0;
}
}
//线程池,用于多线程模拟测试
private ExecutorService pool = Executors.newFixedThreadPool(10);
@Test
public void testLimit()
{
// 被限制的次数
AtomicInteger limited = new AtomicInteger(0);
// 线程数
final int threads = 2;
// 每条线程的执行轮数
final int turns = 20;
// 同步器
CountDownLatch countDownLatch = new CountDownLatch(threads);
long start = System.currentTimeMillis();
for (int i = 0; i < threads; i++)
{
pool.submit(() ->
{
try
{
for (int j = 0; j < turns; j++)
{
long taskId = Thread.currentThread().getId();
long index = tryAcquire(taskId, j);
if (index <= 0)
{
// 被限制的次数累积
limited.getAndIncrement();
}
Thread.sleep(200);
}
} catch (Exception e)
{
e.printStackTrace();
}
//等待所有线程结束
countDownLatch.countDown();
});
}
try
{
countDownLatch.await();
} catch (InterruptedException e)
{
e.printStackTrace();
}
float time = (System.currentTimeMillis() - start) / 1000F;
//输出统计结果
log.info("限制的次数为:" + limited.get() +
",通过的次数为:" + (threads * turns - limited.get()));
log.info("限制的比例为:" + (float) limited.get() / (float) (threads * turns));
log.info("运行的时长为:" + time);
}
}
计数器限流(固定窗口限流)的优点
-
实现简单:
固定窗口算法的实现相对简单,易于理解和部署。
-
稳定性较高:
对于突发请求能够较好地限制和控制,稳定性较高。
-
易于实现速率控制:
固定窗口算法可以很容易地限制请求的速率,例如每秒最多允许多少个请求。
计数器限流(固定窗口限流)的严重问题
1. 临界问题(突刺现象)
- 问题描述:固定窗口限流在窗口切换的瞬间,可能会出现流量的突刺,导致在短时间内有大量请求通过,从而突破限流阈值,对系统造成冲击。
- 示例:假设设置每分钟允许 60 个请求,一个固定窗口的时间范围是从 0 分 0 秒到 1 分 0 秒。在 0 分 59 秒时,已经有 59 个请求通过了限流,此时还没有达到阈值。当时间进入 1 分 0 秒时,新的窗口开始,计数器重置为 0。在这一瞬间,又可以有 60 个请求通过,那么在 0 分 59 秒到 1 分 0 秒这极短的时间内,系统可能会收到多达 119 个请求,远远超过了每分钟 60 个请求的限流阈值。
- 影响:短时间内的大量请求可能会使系统资源耗尽,如 CPU 使用率过高、内存溢出等,从而导致系统性能下降甚至崩溃。
如果对临界问题(突刺现象)不了解,我们看下图:

从上图中我们可以看到,假设有一个恶意用户,他在0:59时,瞬间发送了100个请求,并且1:00又瞬间发送了100个请求,那么其实这个用户在 1秒里面,瞬间发送了200个请求。
我们刚才规定的是1分钟最多100个请求(规划的吞吐量),也就是每秒钟最多1.7个请求,用户通过在时间窗口的重置节点处突发请求, 可以瞬间超过我们的速率限制。
用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。
除了临界问题(突刺现象),还有其他问题:比如无法应对突发流量、限流精度低等等。
2. 无法应对突发流量
- 无法应对突发流量 的问题描述:
固定窗口限流只能对固定时间窗口内的平均流量进行限制,无法灵活应对突发的流量高峰。
即使在大部分时间内流量较低,但只要在某个窗口内流量突然增加并超过阈值,后续请求就会被拒绝,这可能会影响用户体验。
- 无法应对突发流量 的示例:
一个在线商城在正常情况下,每分钟的请求量大约为 20 个,但在促销活动开始的瞬间,可能会在几秒钟内产生大量的请求。
如果固定窗口限流的阈值设置为每分钟 50 个请求,那么在促销活动开始时,由于请求量突然增加,超过了阈值,后续的很多请求都会被拒绝,导致用户无法正常访问商城页面。
- 无法应对突发流量 的影响:
用户可能会因为频繁收到请求被拒绝的提示而感到不满,从而降低对系统的信任度,甚至可能会流失用户。
3. 限流精度低
- 限流精度低问题描述:
固定窗口的时间粒度通常是固定的,无法根据实际情况进行更细粒度的限流控制。例如,如果设置的时间窗口是 1 分钟,那么只能对每分钟的请求量进行统计和限制,无法对更短时间内的流量进行精确控制。
- 限流精度低示例:
在一些对流量控制要求较高的场景中,如金融交易系统,可能需要对每秒甚至更短时间内的请求量进行精确控制。但固定窗口限流只能以分钟为单位进行限流,无法满足这种高精度的限流需求。
- 限流精度低影响:
可能会导致系统在某些情况下无法及时对流量进行调整,从而影响系统的稳定性和可靠性。
滑动窗口限流
滑动窗口限流是对固定窗口限流算法的一种改进。在固定窗口限流中,时间窗口是固定划分的,而滑动窗口限流将时间窗口划分为多个更小的子窗口,随着时间的推移,窗口会不断地滑动。
在统计请求数量时,会统计当前滑动窗口内所有子窗口的请求总和,当请求数量超过预设的阈值时,就拒绝后续的请求。
这样可以更平滑地处理流量,减少固定窗口限流中可能出现的临界问题(突刺现象)。
滑动窗口算法在固定窗口的基础上,将一个计时窗口分成了若干个小窗口,然后每个小窗口维护一个独立的计数器。
当请求的时间大于当前窗口的最大时间时,则将计时窗口向前平移一个小窗口。
平移时,将第一个小窗口的数据丢弃,然后将第二个小窗口设置为第一个小窗口,同时在最后面新增一个小窗口,将新的请求放在新增的小窗口中。
同时要保证整个窗口中所有小窗口的请求数目之后不能超过设定的阈值。

从图中不难看出,滑动窗口算法就是固定窗口的升级版。
将计时窗口划分成一个小窗口,滑动窗口算法就退化成了固定窗口算法。
而滑动窗口算法其实就是对请求数进行了更细粒度的限流,窗口划分的越多,则限流越精准。
图解:滑动窗口限流原理
上文已经说明当遇到时间窗口的临界突变时,固定窗口算法可能无法灵活地应对流量的变化。
滑动窗口限流 的原理如下:
-
窗口大小:
确定一个固定的窗口大小,例如1秒。
-
请求计数:
在窗口内,每次有请求到达时,将请求计数加1。
-
限制条件:
如果窗口内的请求计数超过了设定的阈值,即超过了允许的最大请求数,就拒绝该请求。
-
窗口滑动:
随着时间的推移,窗口会不断滑动,移除过期的请求计数,以保持窗口内的请求数在限制范围内。
-
动态调整:
在滑动窗口算法中,我们可以根据实际情况调整窗口的大小。当遇到下一个时间窗口之前,我们可以根据当前的流量情况来调整窗口的大小,以适应流量的变化。

滑动窗口限流代码实现
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
public class SlidingWindowRateLimiter {
// 时间窗口大小(毫秒)
private final long windowSize;
// 允许的最大请求数
private final int maxRequests;
// 子窗口数量
private final int subWindows;
// 每个子窗口的大小(毫秒)
private final long subWindowSize;
// 存储每个子窗口的请求计数
private final ConcurrentHashMap<Long, AtomicInteger> windowCounts;
// 存储子窗口的时间戳队列
private final ConcurrentLinkedQueue<Long> windowTimestamps;
public SlidingWindowRateLimiter(long windowSize, int maxRequests, int subWindows) {
this.windowSize = windowSize;
this.maxRequests = maxRequests;
this.subWindows = subWindows;
this.subWindowSize = windowSize / subWindows;
this.windowCounts = new ConcurrentHashMap<>();
this.windowTimestamps = new ConcurrentLinkedQueue<>();
}
public synchronized boolean allowRequest() {
long currentTime = System.currentTimeMillis();
// 移除过期的子窗口
while (!windowTimestamps.isEmpty() && currentTime - windowTimestamps.peek() > windowSize) {
long expiredTimestamp = windowTimestamps.poll();
windowCounts.remove(expiredTimestamp);
}
// 计算当前时间所在的子窗口时间戳
long currentSubWindow = currentTime - (currentTime % subWindowSize);
// 获取或创建当前子窗口的计数器
AtomicInteger currentCount = windowCounts.computeIfAbsent(currentSubWindow, k -> {
windowTimestamps.offer(currentSubWindow);
return new AtomicInteger(0);
});
// 计算当前滑动窗口内的总请求数
int totalRequests = windowCounts.values().stream().mapToInt(AtomicInteger::get).sum();
if (totalRequests < maxRequests) {
// 请求通过,增加当前子窗口的计数
currentCount.incrementAndGet();
return true;
}
return false;
}
public static void main(String[] args) {
// 时间窗口为 1000 毫秒,允许最大请求数为 100,子窗口数量为 10
SlidingWindowRateLimiter limiter = new SlidingWindowRateLimiter(1000, 100, 10);
for (int i = 0; i < 120; i++) {
if (limiter.allowRequest()) {
System.out.println("Request " + i + " allowed");
} else {
System.out.println("Request " + i + " denied");
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
调整窗口大小
为了实现根据提供的 API 方法调整窗口大小的功能, 需要对原有的 SlidingWindowRateLimiter 类进行一些修改。
主要思路是将 windowSize 和 subWindowSize 变为可变的属性,并提供一个公共方法来调整窗口大小,同时在调整窗口大小后,需要重新计算过期的子窗口。
以下是优化后的代码:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
public class SlidingWindowRateLimiter {
// 时间窗口大小(毫秒)
private long windowSize;
// 允许的最大请求数
private final int maxRequests;
// 子窗口数量
private final int subWindows;
// 每个子窗口的大小(毫秒)
private long subWindowSize;
// 存储每个子窗口的请求计数
private final ConcurrentHashMap<Long, AtomicInteger> windowCounts;
// 存储子窗口的时间戳队列
private final ConcurrentLinkedQueue<Long> windowTimestamps;
public SlidingWindowRateLimiter(long windowSize, int maxRequests, int subWindows) {
this.windowSize = windowSize;
this.maxRequests = maxRequests;
this.subWindows = subWindows;
this.subWindowSize = windowSize / subWindows;
this.windowCounts = new ConcurrentHashMap<>(

最低0.47元/天 解锁文章
3785

被折叠的 条评论
为什么被折叠?



