QPS(Query Per Second)意思为“每秒查询率”
若区间大小为1s,QPS即是该时间区间内的所有请求
在限流的业务场景下
滑动窗口就是 [当前时间-1s,当前时间] 的区间
对于限流,需要保证该区间内的请求不大于设置的QPS设置的阈值,如果超过则拦截请求
实现
时间是连续的,可以划分为无数个小区间
区间越小,准确率越高,性能越差
比如把1s划分为5个200ms的区间,则每次只需要获取当前时间在内的前5个200ms区间的请求量作为QPS
1s分为5个区间,每个区间200ms的窗口,前闭后开
103ms 200ms 301ms分别发生一次请求
103 / 200 = 0
200 / 200 = 1
301 / 200 = 1
103跨过了0块区间,200 301都跨过了1个区间
由于我们把区间分为了5块
103 / 200 % 5 = 0
200 / 200 % 5 = 1
301 / 200 % 5 = 1
103落在第0区间 200 301落在第1区间
700 / 200 = 3 跨过3个区间 3 % 5 = 3 落在下标3
1300 / 200 = 6 跨过6个区间 6 % 5 = 1 落在下标1
由此得
当前时间 / 单个区间时间长度 = 跨过N个区间
N % 1s划分的区间数 = 当前区间所在下标
实现
package com.company;
import java.util.Arrays;
public class RollWindow {
//窗口总长度
int windowLength = 1000;
//qps限制
int limit;
//小区间长度
int partLen;
//小区间个数
int partNum;
Part[] parts;
public RollWindow(int partLen, int limit) {
this.partLen = partLen;
this.partNum = windowLength / partLen;
this.limit = limit;
this.parts = new Part[partNum];
}
public static void main(String[] args) throws InterruptedException {
RollWindow rollWindow = new RollWindow(500, 4);
int cnt = 0;
for (int i = 0; i < 100; i++) {
long currentTimeMillis = System.currentTimeMillis();
int totalReqCnt = rollWindow.totalReqCnt(currentTimeMillis);
System.out.println(String.format("第%s次请求 时间:[%s] 已经请求cnt:[%s] 被限制:[%s]", i +1 , currentTimeMillis, totalReqCnt, totalReqCnt >= rollWindow.limit ));
Thread.sleep(100);
if(!(totalReqCnt >= rollWindow.limit)) {
cnt++;
rollWindow.getCurrentPart(currentTimeMillis).addReq();
}
}
System.out.println("通过了" + cnt);
}
public boolean isLimit(long currentTimeMillis){
return totalReqCnt(currentTimeMillis) >= limit;
}
public int totalReqCnt(long currentTimeMillis){
//替换掉1s前的数据
getCurrentPart(currentTimeMillis);
int sum = Arrays.stream(parts).filter(part -> part != null && currentTimeMillis - part.partStartTime <= 1000).mapToInt(part -> part.reqCnt).sum();
return sum;// >= limit;
}
public int getIndex(long currentTimeMillis){
long crossPartNum = currentTimeMillis / partLen;
return (int)(crossPartNum % partNum);
}
public Part getCurrentPart(long currentTimeMillis){
int index = getIndex(currentTimeMillis);
Part part = parts[index];
if(part == null){
part = new Part(partLen, currentTimeMillis);
parts[index] = part;
}else {
if(part.partStartTime < (currentTimeMillis - currentTimeMillis % partLen)){
part.reset(currentTimeMillis);
}
}
return part;
}
}
class Part{
long partStartTime;
int reqCnt;
int partLen;
public Part(int partLen, long currentTimeMillis) {
this.partLen = partLen;
reset(currentTimeMillis);
}
public void reset(long currentTimeMillis) {
//获取区间得开始时间戳 是小区间长度得整数倍
this.partStartTime = currentTimeMillis - currentTimeMillis % partLen;
this.reqCnt = 0;
}
public void addReq() {
reqCnt++;
}
}
Thread.sleep(100); 10次为1s 持续10s 结果在40左右徘徊
该实现没有做并发处理
spring-cloud-alibaba-sentine中的FolwSlot大致实现如此