import java.util.Date;
import java.util.Deque;
import java.util.concurrent.LinkedBlockingDeque;
/**
* 滑动窗口限流
* 使用方式:
* 1.使用时创建一个SlideCounterLimiter对象
* 2.请求进入调用requestAccess()方法,根据访问结果判断是否被限制访问
*
* 设计思路:
* 1.滑动窗口被分割成N个子窗口,创建一个限制长度为N的队列
* 2.队列首部是最老的子窗口,队列尾部是最新的子窗口
* 3.每个请求都需要找到自己所在的子窗口,如果没有就创建,并加入到队列,如果队列已满
* 就把队首最老的子窗口踢除,踢出后统计的请求数要减去被踢出的子窗口接收的请求数
* 4.所有子窗口是时间上连续的
* 5.对所有的请求进行区分,第一次请求和后续任意一次请求
* 6.第一次请求,需要首先初始化(创建一个空的队列,请求时间作为开始时间创建第一个子窗口,被访问总次数设置0次),第
* 次请求必然在第一个子窗口时间内,子窗口的受访次数+1,被访问总次数+1。
* 7.后续任意一次请求,可分为三种情况
* 8.第一种情况,请求进入直接被统计入队尾的子窗口
* 9.第二种情况,请求进入后需要以队尾子窗口不断创建后续子窗口并加入队列,直到请求时间可以被统计入新建的子窗口内
* 10.第三种情况,是对第二种情况的优化,当请求访问离上一次请求间隔太过久远,可以直接初始化作为一个新的开始,不必
* 用步骤2的处理方式,以减少性能消耗
* 11.对于上述第二种情况,因为是请求驱动的滑动效果,请求时间是不可预估的,对于一定时间间隔内的请求,可能需要向队
* 列补齐断层的子窗口,建议不要使用过大的分割数
*
*/
public class SlideCounterLimiter {
/**
* 限流窗口时间长度
*/
private long periodTime;
/**
* 限流窗口访问量上限
*/
private long limitTimes;
/**
* 限流窗口分割成子窗口数
*/
private int partSize;
/**
* 子窗口时间长度
*/
private long subPeriodTime;
/**
* 最新子窗口起始时间(用于判断是否生产子窗口及计算子窗口起始结束时间)
*/
private long currentTimeStart;
/**
* 最新子窗口结束时间(用于判断是否生产子窗口及计算子窗口起始结束时间)
*/
private long currentTimeEnd;
/**
* 双向队列
* 1.存放子窗口
* 2.长度为子窗口数
* 3.最新子窗口尾部添加,最老子窗口头部删除,实现滑动
*/
private Deque<SubCounterLimiter> deque;
/**
* 滑动窗口总访问数
* 1.记录滑动窗口目前访问总量
* 2.窗口滑动时,减去最老子窗口的访问量
* 3.访问成功 + 1
*/
private long acceptTimes;
public SlideCounterLimiter(long periodTime, long limitTimes, int partSize) {
this.periodTime = periodTime;
this.limitTimes = limitTimes;
this.partSize = partSize;
this.subPeriodTime = periodTime/partSize;
}
/**
* 子窗口类
*/
private class SubCounterLimiter{
/**
* 子窗口起始时间
*/
private long subTimeStart;
/**
* 子窗口结束时间
*/
private long subTimeEnd;
/**
* 子窗口接受访问数
*/
private long subAcceptTimes;
public SubCounterLimiter(long subTimeStart, long subTimeEnd) {
this.subTimeStart = subTimeStart;
this.subTimeEnd = subTimeEnd;
this.subAcceptTimes = 0L;
}
public long getSubTimeStart() {
return subTimeStart;
}
public long getSubTimeEnd() {
return subTimeEnd;
}
public long getSubAcceptTimes() {
return subAcceptTimes;
}
public void setSubTimeStart(long subTimeStart) {
this.subTimeStart = subTimeStart;
}
public void setSubTimeEnd(long subTimeEnd) {
this.subTimeEnd = subTimeEnd;
}
public void setSubAcceptTimes(long subAcceptTimes) {
this.subAcceptTimes = subAcceptTimes;
}
}
/**
* 步骤1.区分第一次请求和后续请求,第一次请求执行初始化方法
* 步骤2.后续请求,分三种情况:
* <1>.请求时间与最新一次子窗口的结束时间相差超过一个periodTime,执行初始化方法
* 作为一次新的开始
* <2>.请求时间与最新一次子窗口的结束时间相差少于一个periodTime且不在最新子窗口
* 的时间段内,则创建新的子窗口且加入队列,并且队列已满时挤出最老的子窗口和a-
* -cceptTimes减去挤出子窗口的访问量,直到请求时间在最新的子窗口时间段内
* <3>.请求时间在最新一次子窗口内
* 步骤3.步骤1和步骤2保证了当前请求在最新子窗口时间段内
* 步骤4.访问次数进行判断,是否可以访问,成功访问则总访问量+1,子窗口访问量+1
*/
public synchronized boolean requestAccess(){
long requestTime = new Date().getTime();//请求时间
if(deque==null){//第一次请求
init(requestTime);
}else{//后续任意一次请求
if(requestTime>=periodTime+deque.getLast().getSubTimeEnd()){
init(requestTime);
}else if(requestTime>=deque.getLast().getSubTimeEnd() && requestTime<periodTime+deque.getLast().getSubTimeEnd()){
recur(requestTime);
}
}
System.out.println("限流时间段:["+deque.getFirst().getSubTimeStart()+"]至["+deque.getLast().getSubTimeEnd() +"]");
System.out.println("访问时间:"+requestTime);
System.out.println("目前访问量:"+acceptTimes);
if(acceptTimes<limitTimes){
acceptTimes++;
deque.getLast().setSubAcceptTimes(deque.getLast().getSubAcceptTimes()+1);
System.out.println("访问结果:成功!");
return true;
}else{
System.out.println("访问结果:失败,系统忙,稍后再试!");
return false;
}
}
/**
* 1.访问量初始化
* 2.队列初始化
* 3.创建第一个子窗口
* 4.队列加入子窗口
*/
private void init(long requestTime){
System.out.println("初始化...");
acceptTimes = 0L;
deque = new LinkedBlockingDeque<SubCounterLimiter>(partSize);
currentTimeStart = requestTime;
currentTimeEnd = currentTimeStart + subPeriodTime;
SubCounterLimiter initFirst = createSubCounterLimiter(currentTimeStart,currentTimeEnd);
deque.add(initFirst);
}
/**
* 1.递归获得子窗口,并更新入队列
* 2.直到请求时刻在获得子窗口时间区间内,该子窗口作为最新子窗口,递归创建子窗口停止
*/
private void recur(long requestTime){
currentTimeStart = deque.getLast().getSubTimeEnd();
currentTimeEnd = currentTimeStart + subPeriodTime;
SubCounterLimiter last = createSubCounterLimiter(currentTimeStart,currentTimeEnd);
while(!deque.offerLast(last)){
acceptTimes = acceptTimes - deque.getFirst().getSubAcceptTimes();
deque.removeFirst();
}
if(requestTime>=deque.getLast().getSubTimeEnd()){
recur(requestTime);
}
}
/**
* 创建子窗口
*/
private SubCounterLimiter createSubCounterLimiter(long timeStart,long timeEnd) {
return new SubCounterLimiter(timeStart, timeEnd);
}
}