四种限流方法

1、计数器固定窗口算法(计数器)

  规定单位时间处理的请求数量,比如规定一个接口一分钟只能访问10次。使用固定窗口计数器:给定一个变量counter来记录处理这个请求数量,当一分钟之内超过10次,全部拒绝。

等到1分钟后就讲counter回归为0,重新开始计数。

  缺点:如果在59秒发10个请求,在1分一秒的时候再发10个请求,就会出现问题!!。

import java.util.concurrent.atomic.AtomicInteger;

public class 计数器固定窗口算法 {
    private int windowSize;//窗口大小
    private int limit;//窗口内限流大小
    private AtomicInteger count;//当前计数器

    public 计数器固定窗口算法(int windowSize, int limit) {
        this.windowSize = windowSize;
        this.limit = limit;
        count = new AtomicInteger(0);
        //开启一个线程达到窗口结束时清空count;
        new Thread(() -> {
            while (true) {
                count.set(0);
                try {
                    Thread.sleep(windowSize);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    //请求到达后先调用本方法,若返回true,则请求通过,否则限流
    public boolean tryAcuire() {
        int newCount = count.addAndGet(1);
        if (newCount > limit) {
            return false;//大于目前的阈值
        } else {
            return true;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //测试
        计数器固定窗口算法 counterTest = new 计数器固定窗口算法(100, 20);
        int count = 0;
        for (int i = 0; i < 50; i++) {
            if (counterTest.tryAcuire()) {
                count++;
            }
        }
        System.out.println("第一波50个请求通过" + count + "限流" + (50 - count));
        Thread.sleep(1000);
        count=0;
        for (int i = 0; i < 50; i++) {
            if (counterTest.tryAcuire()) {
                count++;
            }
        }
        System.out.println("第二波50个请求通过" + count + "限流" + (50 - count));

    }


}

2、计数器滑动窗口算法

 

public class 计数器滑动窗口算法 {
    private int windowSize;//窗口大小,毫秒为单位
    private int limit;//窗口内限流大小
    private int splitNum;//切分小窗口的数目大小
    private int[] counters;//当前小窗口的计数数组
    private int index;//当前小窗口计数器的索引
    private long startTime;//窗口开始时间

    public 计数器滑动窗口算法(int windowSize, int limit, int splitNum) {
        this.windowSize = windowSize;
        this.limit = limit;
        this.splitNum = splitNum;
        counters = new int[splitNum];
        startTime = System.currentTimeMillis();
    }

    //请求达到后先调用本方法,若返回true,则请求通过,否则限流
    public synchronized boolean tryAcquire() {
        long curTime = System.currentTimeMillis();
        long windowsNum = Math.max(curTime - windowSize - startTime, 0) / (windowSize / splitNum); //计算滑动小窗口的数量
        slideWindow(windowsNum);
        int count = 0;
        for (int i = 0; i < splitNum; i++) {
            count += counters[i];
        }
        if (count >= limit) {
            return false;
        } else {
            counters[index]++;
            return true;
        }
    }

    public synchronized void slideWindow(long windowsNum) {
        if (windowsNum == 0) {
            return;
        }
        long slideNum = Math.min(windowsNum, splitNum);
        for (int i = 0; i < slideNum; i++) {
            index = (index + 1) % splitNum;
            counters[index] = 0;
        }
        startTime = startTime + windowsNum * (windowSize / splitNum);//更新滑动窗口时间

    }

    public static void main(String[] args) throws InterruptedException {
//每秒20个
        int limit = 20;
        计数器滑动窗口算法 counterSlideWindowLimiter = new 计数器滑动窗口算法(1000, limit, 20);
        int count = 0;
//        Thread.sleep(3000);
        //计数器滑动窗口算法模拟100组间隔30ms的50次请求
        System.out.println("计数器滑动窗口算法测试开始...");
        System.out.println("开始模拟100组间隔150ms的50次请求...");
        int failCount = 0;
        for (int j = 0; j < 100; j++) {
            count = 0;
            for (int i = 0; i < 50; i++) {
                if (counterSlideWindowLimiter.tryAcquire()) {
                    count++;
                }
            }
            Thread.sleep(150);
            //模拟50次请求,看多少能通过
            for (int i = 0; i < 50; i++) {
                if (counterSlideWindowLimiter.tryAcquire()) {
                    count++;
                }
            }
            if (count > limit) {
                System.out.println("时间窗口内放过的请求超过阈值,放过的请求数" + count + ",限流:" + limit);
                failCount++;
            }
            Thread.sleep((int) (Math.random() * 100));
        }
        System.out.println("计数器滑动窗口算法测试结束,100组间隔150ms的50次请求模拟完成,限流失败组数: " + failCount);
        System.out.println("==========================================================================");
        //计数器固定窗口算法模拟100组间隔30ms的50次请求
        System.out.println("计数器固定窗口算法测试开始...");
        //模拟100组间隔30ms的50次请求
        计数器滑动窗口算法 counterLimiter = new 计数器滑动窗口算法(1000, 20, 20);
        System.out.println("开始模拟100组间隔150ms的50次请求...");
        failCount = 0;
        for (int j = 0; j < 100; j++) {
            count = 0;
            for (int i = 0; i < 50; i++) {
                if (counterLimiter.tryAcquire()) {
                    count++;
                }
            }
            Thread.sleep(150);
            //模拟50次请求,看多少能通过
            for (int i = 0; i < 50; i++) {
                if (counterLimiter.tryAcquire()) {
                    count++;
                }
            }
            if (count > limit) {
                System.out.println("时间窗口内放过的请求超过阈值,放过的请求数" + count + ",限流:" + limit);
                failCount++;
            }
            Thread.sleep((int) (Math.random() * 100));
        }
        System.out.println("计数器固定窗口算法测试结束,100组间隔150ms的50次请求模拟完成,限流失败组数:" + failCount);
    }
}

  把时间以一定比例分片,把1分钟分为60个窗口。每隔1秒移动一次,每次窗口一秒只能处理不大于60(请求)/60(窗口)的请求,如果当前窗口的请求总和超过了限制数量不再处理其他请求。如果当前请求总和超过了限制的数量就不再处理请他请求。各种越大滑动窗口滚动越平滑。

  缺点:滑动的时间复杂度是O(N),因为才用list来做数据结构,查询、修改需要逐个查询链表指针,效率比令牌通更低。滑动窗口需要存储更多的数据,存储N个数据,过期时间为S(S为一个窗口的计算周期),随着窗口细粒度越高,存储数据也越大。相当于是每个小周期就是个小窗口,这样就能避免计数器的毛刺现象

3、漏洞算法

import java.util.Date;
import java.util.LinkedList;

public class LeakyBucketLimiter {
    private int capacity; //漏斗容量
    private int rate; //漏斗速率
    private int left; //剩余容量
    private LinkedList<Request> requestList;

    private LeakyBucketLimiter() {
    }

    public LeakyBucketLimiter(int capacity, int rate) {
        this.capacity = capacity;
        this.rate = rate;
        this.left = capacity;
        requestList = new LinkedList<>();
        //开启一个定时线程,以固定的速率将漏斗中的请求流出,进行处理
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    if (!requestList.isEmpty()) {
                        Request request = requestList.removeFirst();
                        handleRequest(request);
                    }
                    try {
                        Thread.sleep(1000 / rate); //睡眠
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    /**
     * 处理请求
     *
     * @param request
     */
    private void handleRequest(Request request) {
        request.setHandleTime(new Date());
        System.out.println(request.getCode() + "号请求被处理,请求发起时间: "
                + request.getLaunchTime() + ",请求处理时间: " + request.getHandleTime() + ",处理耗时: "
                + (request.getHandleTime().getTime() - request.getLaunchTime().getTime() + "ms"));
    }

    private synchronized boolean tryAcquire(Request request) {
        if (left <= 0) {
            return false;
        } else {
            left--;
            requestList.addLast(request);
            return true;
        }
    }

    /**
     * 请求类,属性包含编号字符串,请求达到时间和请求处理时间
     */
    static class Request {
        private int code;
        private Date launchTime;
        private Date handleTime;

        private Request() {
        }

        public Request(int code, Date launchTime) {
            this.launchTime = launchTime;
            this.code = code;
        }

        public int getCode() {
            return code;
        }

        public void setCode(int code) {
            this.code = code;
        }

        public Date getLaunchTime() {
            return launchTime;
        }

        public void setLaunchTime(Date launchTime) {
            this.launchTime = launchTime;
        }

        public Date getHandleTime() {
            return handleTime;
        }

        public void setHandleTime(Date handleTime) {
            this.handleTime = handleTime;
        }
    }

    public static void main(String[] args) {
        LeakyBucketLimiter leakyBucketLimiter = new LeakyBucketLimiter(5, 2);
        for (int i = 1; i <= 10; i++) {
            Request request = new Request(i, new Date());
            if (leakyBucketLimiter.tryAcquire(request)) {
                System.out.println(i + "号请求被接受");
            } else {
                System.out.println(i + "号请求被拒绝");
            }
        }
    }
}

  可以把请求的动作比作成注水到桶里中,处理请求的过程比喻成漏捅漏水。我们往桶中任意速率流入水,以一定的速率流出水。当水桶超过流量就丢弃。用一个队列来保存请求,然后定期从队列中拿出请求就可以。

  缺点:导致其无法应对准点秒杀刚开始的流量洪峰。

4、令牌桶算法

import java.util.Date;

public class TokenBucketLimiter {
    private int capacity; //令牌桶容量
    private int rate; //令牌产生速率
    private int tokenAmount; //令牌数量
    public TokenBucketLimiter(int capacity, int rate) {
        this.capacity = capacity;
        this.rate = rate;
        tokenAmount = capacity;
        new Thread(new Runnable() {
            @Override
            public void run() {
                //以恒定的速率放令牌
                while (true) {
                    synchronized (this) {
                        tokenAmount++;
                        if (tokenAmount > capacity) {
                            tokenAmount = capacity;
                        }
                    }
                    try {
                        Thread.sleep(1000 / rate);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
    public synchronized boolean tryAcquire(Request request) {
        if (tokenAmount > 0) {
            tokenAmount--;
            handleRequest(request);
            return true;
        } else {
            return false;
        }
    }
    /**
     * 处理请求
     *
     * @param request
     */
    private void handleRequest(Request request) {
        request.setHandleTime(new Date());
        System.out.println(request.getCode() + "号请求被处理,请求发起时间: "
                + request.getLaunchTime() + ",请求处理时间: " + request.getHandleTime() + ",处理耗时: "
                + (request.getHandleTime().getTime() - request.getLaunchTime().getTime() + "ms"));
    }
    /**
     * 请求类,属性包含编号字符串,请求达到时间和请求处理时间
     */
    static class Request {
        private int code;
        private Date launchTime;
        private Date handleTime;
        private Request() {
        }
        public Request(int code, Date launchTime) {
            this.launchTime = launchTime;
            this.code = code;
        }
        public int getCode() {
            return code;
        }
        public void setCode(int code) {
            this.code = code;
        }
        public Date getLaunchTime() {
            return launchTime;
        }
        public void setLaunchTime(Date launchTime) {
            this.launchTime = launchTime;
        }
        public Date getHandleTime() {
            return handleTime;
        }
        public void setHandleTime(Date handleTime) {
            this.handleTime = handleTime;
        }
    }
    public static void main(String[] args) {
        TokenBucketLimiter tokenBucketLimiter = new TokenBucketLimiter(5, 2);
        for (int i = 1; i <= 10; i++) {
            Request request = new Request(i, new Date());
            if (tokenBucketLimiter.tryAcquire(request)) {
                System.out.println(i + "号请求被接受");
            }else {
                System.out.println(i + "号请求被拒绝");
            }
        }
    }
}

   令牌桶可以说是生产者消费者模式,一个生产者以固定的速度生产令牌,如果桶满了就停止生产,每一个请求都是一个消费者,需要从桶里面拿到令牌才能进一步被处理。

总结:

1、两种漏洞算法能够强行限制数据的传输速率,而令牌通算法能够限制数据在平局传输外,还能在某种程度上应对突发流量。

2、漏捅的关键至于控制桶的大小,决定了系统能够容纳的等待请求。令牌通的关键在于控制“令牌生成的速率”,它决定了整个系统的流量速率。

posted @ 2022-08-21 22:56  雷雷提  阅读(425)  评论(0编辑  收藏  举报