本文介绍了两种单机固定时间窗口限流算法。
前言 首先限流的算法有很多种,比如固定时间窗口限流算法、滑动时间窗口算法、漏桶算法、令牌桶算法,其余的算法后续我们会一一介绍。
一、限流场景
站在服务调用方的角度来说,限流的场景大体分为两类。
对外提供服务
用户通过某种形式调用到了我们的服务,比如提供了web服务。
(1)用户增长速度太快。请求量一下子上来了,接口扛不住压力
(2)爬虫
(3)热点事件。比如公司上市成功曝光度增加,从而导致访问用户量增加。
(4)刷单。有的公司甚至主动为用户提供刷单工具,就会导致服务的请求量暴增。
对内提供服务
当前存在A、B、C、D四个服务,A服务同时被B、C、D服务调用,如果B服务的请求量暴增,导致A服务宕机,同时就会导致C、D服务也无法使用,这种情况就不太合适。
二、固定时间窗口限流算法原理
- 时间线划分为多个独立且固定大小窗口;
- 落在每一个时间窗口内的请求就将计数器加1;
- 如果计数器超过了限流阈值,则后续落在该窗口的请求都会被拒绝。但时间达到下一个时间窗口时,计数器会被重置为0。 固定时间内只允许通过一定数据量请求,超出的请求数则会被拦截。
优点:
逻辑简单、维护成本比较低
缺点:
这种方式下我们假定的是请求均匀分布,如果请求数据只集中在时间窗口内的某一个时间段,那么就可能超出承受范围。
三、代码实现
(1)简单实现
/**
* @author weiwei.zhang
* 固定时间窗口
*/
public class FixTimeWindow {
// 时间窗口大小,单位毫秒
public static final Integer DURATION = 1 * 1000;
// 允许的最大请求次数
public static final Integer MAX_COUNT = 100;
// 当前请求次数
public static Integer curCount = 0;
// 当前时间窗口的开始时间
public static Long endTime = System.currentTimeMillis();
public boolean limit(int count){
long curTime = System.currentTimeMillis();
//不在当前时间窗口处理
if(curTime>endTime){
endTime = curTime+DURATION;
curCount = count;
return true;
}else{
//在当前时间窗口
curCount = curCount+count;
return curCount<MAX_COUNT;
}
}
}
(2)按skuId进行限流
/**
* @author weiwei.zhang
* 创建接口
*/
public interface Window {
//限流数量
long threshold = 100;
boolean addCounter(int skuId,int count);
}
/**
* @author weiwei.zhang
* 统计
*/
@Data
public class CounterHelper {
private LongAdder counter;
private Long lastTime;
public CounterHelper() {
this.counter = new LongAdder();
this.lastTime = System.currentTimeMillis();
}
}
/**
* @author weiwei.zhang
* 固定时间窗口
*/
public class FixedWindow implements Window {
private Map<Integer,CounterHelper> conMap = new ConcurrentHashMap<>();
private int intervalInMs = 1000;
@Override
public boolean addCounter(int skuId, int count) {
//初始化
CounterHelper counterHelper = conMap.computeIfAbsent(skuId, (k) -> new CounterHelper());
long curTime = System.currentTimeMillis();
Long lastTime = counterHelper.getLastTime();
LongAdder counter = counterHelper.getCounter();
//判断是否超出当前窗口
if(curTime-lastTime>intervalInMs){
counterHelper.setLastTime(curTime);
counter.reset();
}
counter.add(count);
//判断是否超出限流数
if(counter.sum() > threshold){
counterHelper.setLastTime(curTime);
counter.reset();
System.out.println("达到tps阈值。。。");
return true;
}
return false;
}
public static void main(String[] args) throws InterruptedException {
FixedWindow fixedWindow = new FixedWindow();
//fixedWindow.normalTest();
fixedWindow.bugTest();
}
/**
* @throws InterruptedException
* 正常的测试
*/
public void normalTest() throws InterruptedException {
for (int i = 0; i < 10000; i++) {
addCounter(1,1);
Thread.sleep(11);
}
}
/**
* bug desc: 1____2____3
* 假设某时刻CounterHelper里的lastTime=1,
* 在1.5至2来了90个请求,
* 2s时刻由于curTime-lastTime>1000,计数器被清空,
* 接着2s至2.5又来了90个请求仍然不会触发阈值
* 1.5至2.5一共来了180个请求, 明显不符合我们预期, 这是固定窗口的一个弊端
*/
public void bugTest() throws InterruptedException {
addCounter(1,1);
//休眠500ms,让下面90个请求打到1.5至2这个区间
Thread.sleep(500);
//时刻1.5处
for (int i = 0; i < 90; i++) {
addCounter(1,1);
}
//在休眠500ms,让下面90个请求打到2-2.5这个区间
Thread.sleep(500);
//时刻2处
for (int i = 0; i < 90; i++) {
addCounter(1,1);
}
//最终没有达到阈值,不符合预期, 使用滑动窗口解决 todo
//固定窗口
// |____|____|: 第一个窗口的后半部分和第二个窗口的前半部分加起来 可能超过阈值
// ,但是却感知不了,这是因为固定窗口滑动的粒度太大了,可以使用滑动窗口缩小滑动的粒度
}
}