两种固定时间窗口限流算法

726 阅读3分钟

本文介绍了两种单机固定时间窗口限流算法。

前言 首先限流的算法有很多种,比如固定时间窗口限流算法、滑动时间窗口算法、漏桶算法、令牌桶算法,其余的算法后续我们会一一介绍。

一、限流场景

站在服务调用方的角度来说,限流的场景大体分为两类。

对外提供服务

用户通过某种形式调用到了我们的服务,比如提供了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

        //固定窗口
        // |____|____|: 第一个窗口的后半部分和第二个窗口的前半部分加起来 可能超过阈值
        // ,但是却感知不了,这是因为固定窗口滑动的粒度太大了,可以使用滑动窗口缩小滑动的粒度

    }

}