目录
一、简介
二、作用
三、限流算法
3.1、固定窗口算法(计数器)
3.1.1、简介
3.1.2、原理
3.1.3、适用场景
3.1.4、代码实现
4.1.5、优劣分析
3.2、滑动窗口算法
3.2.1、简介
3.2.2、原理
3.2.3、适用场景
3.2.4、代码实现
3.2.5、优劣分析
3.3、漏桶算法
3.3.1、简介
3.3.2、特点
3.3.3、原理
3.3.4、适用场景
3.3.5、代码实现
3.3.6、优劣分析
3.4、令牌桶算法
3.4.1、简介
3.4.2、原理
3.4.3、适用场景
3.4.4、代码实现
3.4.5、优劣分析
3.5、滑动日志算法(比较冷门)
3.5.1、简介
3.5.2、原理
3.5.3、适用场景
3.5.4、代码实现
3.5.5、优劣分析
四、总结
限流是一种通过控制请求的速率或数量来保护系统免受过载的技术。流控的精髓是限制单位时间内的请求量,最大程度保障系统的可靠性及可用性。
限流是在高并发环境下,为了保护系统的稳定性和可用性而引入的一种策略。通过限制并发请求的数量或频率,可以防止系统被过多的请求压垮或耗尽资源。
常见的流控算法包括:固定窗口、滑动窗口、漏桶、令牌桶、滑动日志等算法。
固定窗口限流算法(Fixed Window Rate Limiting Algorithm)是一种最简单的限流算法,其原理是在固定时间窗口(单位时间)内限制请求的数量。
固定窗口是最简单的流控算法。即,给定时间窗口,维护一个计数器用于统计访问次数,并实现以下规则:
1.如果访问次数小于阈值,则允许访问,访问次数+1;
2.如果访问次数超出阈值,则限制访问,访问次数不增;
3.如果超过了时间窗口,计数器清零,并重置清零后的首次成功访问时间为当前时间。
/**
* @ClassName: FixedWindowRateLimiter
* @projectName: cat
* @description: 限流算法:固定窗口算法。
* @author: yangwenxue
* @date: 2024/1/25 15:40
* @Version: 1.0
*/
public class FixedWindowRateLimiter {
private static int counter = 0; // 统计请求数
private static long lastAcquireTime = 0L;
private static final long windowUnit = 1000L; // 假设固定时间窗口是1000ms
private static final int threshold = 10; // 窗口阀值是10
/**
* 获取令牌
*
* @return
*/
public synchronized boolean tryAcquire() {
long currentTime = System.currentTimeMillis(); // 获取系统当前时间
if (currentTime - lastAcquireTime > windowUnit) { // 检查是否在时间窗口内
counter = 0; // 计数器清零
lastAcquireTime = currentTime; // 开启新的时间窗口
}
if (counter < threshold) { // 小于阀值
counter++; // 计数器加1
return true; // 获取请求成功
}
return false; // 超过阀值,无法获取请求
}
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
boolean acquire = new FixedWindowRateLimiter().tryAcquire();
System.out.println("获取令牌:" + acquire);
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
优点
缺点
为了解决临界突变问题,可以引入滑动窗口。即:把大的时间窗口拆分成若干粒度更细的子窗口,每个子窗口独立统计,按子窗口时间滑动,统一限流。
当滑动窗口的格子周期划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
将单位时间周期分为n个小周期,分别记录每个小周期内接口的访问次数,并且根据时间滑动删除过期的小周期。它可以解决固定窗口临界值的问题。
同固定窗口的场景,且对流量限制要求较高的场景,需要更好地应对突发流量。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @ClassName: SlidingWindowLimiter
* @projectName: cat
* @description: 限流算法:滑动窗口算法:实现方式一
* @author: yangwenxue
* @date: 2023/10/16 15:23
* @Version: 1.0
*/
public class SlidingWindowLimiter {
/**
* 每个窗口的最大请求数量
*/
public static long threshold = 10;
/**
* 窗口大小,1000ms
*/
public static long windowUnit = 1000;
/**
* 请求集合,用来存储窗口内的请求数量
*/
public static List requestList = new ArrayList<>();
/**
* 限流方法,返回true表示通过
*/
public boolean limit() {
System.out.println("requestList=" + requestList.size());
// 获取系统当前时间
long currentTime = System.currentTimeMillis();
// 统计当前窗口内,有效的请求数量
int sizeOfValid = this.sizeOfValid(currentTime);
// 判断是否超过最大请求数量
if (sizeOfValid < threshold) {
// 把当前请求添加到请求集合里
requestList.add(currentTime);
return true;
}
return false;
}
/**
* 统计当前窗口内,有效的请求数量
*/
private int sizeOfValid(long currentTime) {
int sizeOfValid = 0;
for (Long requestTime : requestList) {
// 判断是否在当前时间窗口内
if (currentTime - requestTime <= windowUnit) {
sizeOfValid++;
}
}
return sizeOfValid;
}
/**
* 清理过期的请求(单独启动一个线程处理)
*/
private void clean() {
// 判断是否超出当前时间窗口内
requestList.removeIf(requestTime -> System.currentTimeMillis() - requestTime > windowUnit);
}
// 测试
public static void main(String[] args) {
SlidingWindowLimiter slidingWindowLimiter = new SlidingWindowLimiter();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
executorService.execute(() -> {
boolean limit = slidingWindowLimiter.limit();
System.out.println(Thread.currentThread().getName() + "==>" + limit);
});
}
executorService.shutdown();
}
}
优势
劣质
基于(出口)流速来做流控。在网络通信中常用于流量整形,可以很好地解决平滑度问题。
将数据包看作是水滴,漏桶看作是一个固定容量的水桶,数据包像水滴一样从桶的顶部流入桶中,并通过桶底的一个小孔以一定的速度流出,从而限制了数据包的流量
对于每个到来的数据包,都将其加入到漏桶中,并检查漏桶中当前的水量是否超过了漏桶的容量。如果超过了容量,就将多余的数据包丢弃。如果漏桶中还有水,就以一定的速率从桶底输出数据包,保证输出的速率不超过预设的速率,从而达到限流的目的。
一般用于保护第三方的系统,比如自身的系统需要调用第三方的接口,为了保护第三方的系统不被自身的调用打垮,便可以通过漏斗算法进行限流,保证自身的流量平稳的打到第三方的接口上。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @ClassName: LeakyBucketLimiter
* @projectName: cat
* @description: 限流算法:漏桶算法
* @author: yangwenxue
* @date: 2023/10/16 10:48
* @Version: 1.0
*/
public class LeakyBucketLimiter {
/**
* 水的流出速率(每秒允许的次数)
*/
private double rate;
/**
* 桶的大小
*/
private double burst;
/**
* 最后更新时间
*/
private long refreshTime;
/**
* 漏桶当前水量
*/
private int water;
/**
* @param rate 水的流出速率
* @param burst 桶的大小
*/
public LeakyBucketLimiter(double rate, double burst) {
this.rate = rate;
this.burst = burst;
}
/**
* 刷新桶的水量
*/
public void refreshWate() {
long now = System.currentTimeMillis();
water = (int) Math.max(0, water - (now - refreshTime) / 1000 * rate);
refreshTime = now;
System.out.println("当前桶余量:" + (burst - water));
}
/**
* 获取令牌
*
* @return
*/
public synchronized boolean tryAcquire() {
refreshWate();
if (water < burst) {
water++;
return true;
} else {
return false;
}
}
// 测试:用一个线程池模拟多个请求,看看是否到达限流的效果
private static LeakyBucketLimiter leakBucket = new LeakyBucketLimiter(80, 200);
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + ":" + leakBucket.tryAcquire());
});
// try {
// Thread.sleep(10);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
executorService.shutdown();
}
}
优势
劣质
基于(入口)流速来做流控的一种限流算法。
该算法维护一个固定容量的令牌桶,每秒钟会向令牌桶中放入一定数量的令牌。当有请求到来时,如果令牌桶中有足够的令牌,则请求被允许通过并从令牌桶中消耗一个令牌,否则请求被拒绝。
一般用于保护自身的系统,对调用者进行限流,保护自身的系统不被突发的流量打垮。如果自身的系统实际的处理能力强于配置的流量限制时,可以允许一定程度的流量突发,使得实际的处理速率高于配置的速率,充分利用系统资源。
import java.math.BigDecimal;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* @ClassName: TokenLimiter
* @projectName: cat
* @description: 限流算法:令牌桶算法(推荐)
* @author: yangwenxue
* @date: 2023/9/26 15:05
* @Version: 1.0
*/
public class TokenLimiter {
/**
* 令牌
*/
public static final String TOKEN = "lp";
/**
* 阻塞队列,用于存放令牌
*/
private ArrayBlockingQueue blockingQueue;
/**
* 令牌桶容量
*/
private int limit;
/**
* 生产令牌的间隔时间,单位:毫秒
*/
private int period;
/**
* 每次生产令牌的个数
*/
private int amount;
public TokenLimiter(int limit, int period, int amount) {
this.limit = limit;
this.period = period;
this.amount = amount;
blockingQueue = new ArrayBlockingQueue<>(limit);
init();
start();
}
/**
* 创建初始化令牌
*/
private void init() {
for (int i = 0; i < limit; i++) {
blockingQueue.add(TOKEN);
}
}
/**
* 添加令牌
*/
private void addToken() {
for (int i = 0; i < limit; i++) {
// 溢出则返回false
blockingQueue.offer(TOKEN);
}
}
/**
* 获取令牌,如果令牌桶为空则返回false
*
* @return
*/
public synchronized boolean tryAcquire() {
int size = blockingQueue.size();
System.out.println("token剩余数量====>" + size);
BigDecimal total = new BigDecimal(limit);
BigDecimal use = new BigDecimal(size);
BigDecimal ss = use.divide(total, 2, BigDecimal.ROUND_HALF_UP);
System.out.println("使用率:"+ ss.multiply(new BigDecimal(100)) + "%");
//队首元素出队
return blockingQueue.poll() == null ? false : true;
}
/**
* 生产令牌
*/
public void start() {
Executors.newScheduledThreadPool(1).scheduleAtFixedRate(() -> {
addToken();
}, 500, //第一次执行任务延迟时间
this.period, //连续执行任务的周期,也就是间隔时间
TimeUnit.MILLISECONDS
);
}
// 测试
public static void main(String[] args) throws InterruptedException {
int period = 500;
//先生产3个令牌,减少4个令牌,再每500ms生产3个令牌
TokenLimiter tokenLimiter = new TokenLimiter(3, period, 3);
for (int i = 0; i < 4; i++) {
new Thread(() -> {
while (true) {
String name = "线程[" + Thread.currentThread().getName() + "]";
if (tokenLimiter.tryAcquire()) {
System.out.println(name + "拿到令牌");
} else {
System.out.println(name + "没有拿到令牌");
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
优势
劣质
滑动日志限速算法需要记录请求的时间戳,通常使用有序集合来存储,我们可以在单个有序集合中跟踪用户在一个时间段内所有的请求。
滑动日志算法可以用于实现限流功能,即控制系统在单位时间内处理请求的数量,以保护系统免受过载的影响。以下是滑动日志算法用于限流的原理:
通过滑动日志算法进行限流,可以实现对单位时间内的请求进行精确控制。它基于实时统计的方式,能够动态地适应请求流量的变化,并且在内存使用上比较高效。同时,通过调整时间窗口的长度和阈值的设置,可以灵活地控制限流的精度和灵敏度。
对实时性要求高,且需要精确控制请求速率的高级限流场景。
import java.util.LinkedList;
import java.util.List;
/**
* @ClassName: SlidingLogRateLimiter
* @projectName: cat
* @description: 限流算法:滑动日志算法(比较冷门)
* @author: yangwenxue
* @date: 2024/1/25 16:35
* @Version: 1.0
*/
public class SlidingLogRateLimiter {
private int requests; // 请求总数
private List timestamps; // 存储请求的时间戳列表
private long windowDuration; // 窗口持续时间,单位:毫秒
private int threshold; // 窗口内的请求数阀值
/**
* 构造函数
*
* @param threshold 窗口内的请求数阀值
* @param windowDuration 窗口持续时间,单位:毫秒
*/
public SlidingLogRateLimiter(int threshold, long windowDuration) {
this.requests = 0;
this.timestamps = new LinkedList<>();
this.windowDuration = windowDuration;
this.threshold = threshold;
}
public synchronized boolean tryAcquire() {
long currentTime = System.currentTimeMillis(); // 获取当前时间戳
// 删除超过窗口持续时间的时间戳
while (!timestamps.isEmpty() && currentTime - timestamps.get(0) > windowDuration) {
timestamps.remove(0);
requests--;
}
if (requests < threshold) { // 判断当前窗口内请求数是否小于阀值
timestamps.add(currentTime); // 将当前时间戳添加到列表
requests++; // 请求总数增加
System.out.println("当前请求总量:" + requests);
return true; // 获取请求成功
}
return false; // 超过阀值,无法获取请求
}
/**
* 测试
* @param args
*/
public static void main(String[] args) {
SlidingLogRateLimiter slidingLogRateLimiter = new SlidingLogRateLimiter(10, 1000);
for (int i = 0; i < 200; i++) {
boolean acquire = slidingLogRateLimiter.tryAcquire();
System.out.println(acquire);
try {
Thread.sleep(60);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
优势
劣质
以上就是常见的五种限流的原理、特点、算法、适用场景介绍,其中比较推荐令牌桶算法。除了上述实现,还有常用工具Guava的RateLimiter(单机),以及阿里巴巴的限流和熔断降级组件sentinel(单机或者分布式),这里不做过多赘述。