一、常见的限流策略的原理

计数器算法:计数器算法是使用计数器在周期内累加访问次数,当达到设定的限流值时,触发限流策略。下一个周期开始时,进行清零,重新计数。此算法在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性和线程安全即可轻松实现

令牌桶算法:系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则  拒绝服务。 当桶满时,新添加的令牌被丢弃或拒绝。

漏桶算法:访问请求到达时直接放入漏桶,如当前容量已达到上限(限流值),则进行丢弃(触发限流策略)。漏桶以固定的速率进行释放访问请求(即请求通过),直到漏桶为空。

二、各种限流策略优劣比较

1.计数器算法

优点:分布式中实现难度低

缺点:不能平滑限流,存在临界问题,前一个周期的最后几秒和下一个周期的开始几秒时间段内访问量很大但没超过周期量计数量时,但短时间请求量依旧很高。

2.令牌桶和漏桶区别:主要区别在于“漏桶算法”能够强行限制数据的传输速率,而“令牌桶算法”在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,因此它适合于具有突发特性的流量。

三、Java常用实现

    1.Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法(Token Bucket)来完成限流,非常易于使用

四、代码示例

令牌桶算法示例

//qps:每秒钟处理完请求的次数;tps:每秒钟处理完的事务次数
    RateLimiter rateLimiter = RateLimiter.create(10);

    public static volatile Integer sucNum = 0;//记录 一秒处理 20并发成功次数

    public void doSomething(String threadName){
        if (rateLimiter.tryAcquire()){//rateLimiter.acquire()
            //尝试获得令牌.为true则获取令牌成功
            System.out.println(threadName+"-正常处理");
            sucNum += 1;
            System.out.println("成功次数:"+sucNum);
        }else{
            System.out.println(threadName+"-处理失败");
        }

    }

    public static void main(String args[]) throws IOException {
        CountDownLatch latch = new CountDownLatch(1);
        Random random = new Random(10);
        TokenDemo tokenDemo = new TokenDemo();
        for (int i=0;i<20;i++){
            new Thread(()->{
                try {
                    latch.await();
                    Thread.sleep(random.nextInt(1000));
                    tokenDemo.doSomething(Thread.currentThread().getName());
                }catch (InterruptedException e){
                    e.printStackTrace();
                }

            },"线程"+i).start();
        }
        latch.countDown();

        System.in.read();

    }

漏桶算法示例

private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);

    // 桶的容量
    public int capacity = 10;
    // 当前水量
    public int water = 0;
    //水流速度/s
    public int rate = 4;
    // 最后一次加水时间
    public long lastTime = System.currentTimeMillis();



    public void acquire() {
        scheduledExecutorService.scheduleWithFixedDelay(() -> {
            long now = System.currentTimeMillis();
            //计算当前水量
            water = Math.max(0, (int) (water - (now - lastTime) * rate /1000));
            int permits = (int) (Math.random() * 8) + 1;
            System.out.println("请求数:" + permits + ",当前桶余量:" + (capacity - water));
            lastTime = now;
            if (capacity - water < permits) {
                // 若桶满,则拒绝
                System.out.println("限流了");
            } else {
                // 还有容量
                water += permits;
                System.out.println("剩余容量=" + (capacity - water));
            }
        }, 0, 500, TimeUnit.MILLISECONDS);
    }
    public static void main(String args[]) throws IOException {
        LeakyBucket limiter = new LeakyBucket();
        limiter.acquire();
    }