RateLimiter

com.google.common.util.concurrent.RateLimiter 谷歌下的限流工具,采用的是令牌桶算法

一些限流算法

1、令牌桶算法

原理:系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。 当桶满时,新添加的令牌被丢弃或拒绝。允许一定程度的突发流量,若桶内令牌个数满足,并发请求可以同时获取,主要用于需要平滑突发限流的场景。

2、漏桶算法

原理:一个固定容量的漏桶,按照常量固定速率流出水滴,如果桶是空的,则不需流出水滴;可以以任意速率流入水滴到漏桶,如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。即:请求可以任意速率进来,但是会以固定的速率被处理,不允许并发处理,主要适用需要平滑突发流入速率的场景。

3、计数器限流

原理:是比较常用的,主要用来限制总并发数,比如数据库连接池大小、线程池大小、程序访问并发数等都是使用计数器算法。

可以使用java.util.concurrent.atomic.AtomicInteger来控制并发次数,如:

public class CountAtomicTest {

private static AtomicInteger count = new AtomicInteger(0);

public static void main() {
    if (count.get() >= 5) {
        System.out.println("请求用户过多,请稍后再试!");
    } else {
        count.incrementAndGet();
        try {
            //处理核心逻辑
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            count.decrementAndGet();
        }
    }
 }
}

也可以通过使用信号量 java.util.concurrent.Semaphore 来控制并发执行次数,如:

public class CountSemaphoreTest {

    private static Semaphore semphore = new Semaphore(5);

    public static void main() {
        if(semphore.getQueueLength()>50){
            System.out.println("当前等待排队的任务数大于50,请稍候再试...");
        }
        try {
            semphore.acquire();
            // 处理核心逻辑
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semphore.release();
        }
    }
}

二者区别:atomicInteger 将超过计数控制的请求直接拒绝,semaphore 将超过计数控制的请求放阻塞队列,阻塞队列超过一定大小可以拒绝请求。

com.google.common.util.concurrent.RateLimiter 用法简述

实现过程主要应用令牌桶算法

1、单线程场景,如下:

 public static void main(String[] args){

    //1s只放5个令牌 即200ms投一个令牌
    final RateLimiter rateLimiter = RateLimiter.create(5);
    try {
        Thread.sleep(500);
    }catch (Exception e){
        e.printStackTrace();
    }
    for (int i = 0; i < 50; i++) {
        long time1 = System.currentTimeMillis();
        //尝试获取令牌,返回等待的时间(s为单位)
        System.out.println(i + "尝试中...,需要等待:" + rateLimiter.acquire() * 1000);
        System.out.println(i + "已获得令牌,耗时:" + (System.currentTimeMillis() - time1));
        try {
            Thread.sleep(10);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    System.out.println("所有已完成");
}

运行结果如下:

0尝试中...,需要等待:0.0
0已获得令牌,耗时:0
1尝试中...,需要等待:0.0
1已获得令牌,耗时:0
2尝试中...,需要等待:0.0
2已获得令牌,耗时:0
3尝试中...,需要等待:54.418
3已获得令牌,耗时:57
4尝试中...,需要等待:184.753
4已获得令牌,耗时:186
5尝试中...,需要等待:186.215
5已获得令牌,耗时:185
6尝试中...,需要等待:190.492
6已获得令牌,耗时:190
7尝试中...,需要等待:190.467
7已获得令牌,耗时:190
8尝试中...,需要等待:190.593
8已获得令牌,耗时:191
9尝试中...,需要等待:189.596
9已获得令牌,耗时:190
10尝试中...,需要等待:189.513
10已获得令牌,耗时:190
11尝试中...,需要等待:189.646
11已获得令牌,耗时:190
12尝试中...,需要等待:189.552
12已获得令牌,耗时:190
13尝试中...,需要等待:189.494
13已获得令牌,耗时:189
14尝试中...,需要等待:190.537
14已获得令牌,耗时:191
15尝试中...,需要等待:189.649
15已获得令牌,耗时:189
16尝试中...,需要等待:189.445
16已获得令牌,耗时:189
17尝试中...,需要等待:188.459
17已获得令牌,耗时:190
18尝试中...,需要等待:189.344
18已获得令牌,耗时:189
19尝试中...,需要等待:190.525
19已获得令牌,耗时:191
20尝试中...,需要等待:189.38
20已获得令牌,耗时:189
...
...

刚开始程序启动初始化 rateLimiter 开始每隔 200ms投令牌,所以前几个基本很快拿到了令牌,再往后基本是恒定速率拿到令牌。

2、并发场景如下:

public static void main(String[] args){
    //用countDownLatch模拟并发
    final CountDownLatch startLatch = new CountDownLatch(1);
    final CountDownLatch runLatch = new CountDownLatch(10);
    ExecutorService exe = Executors.newFixedThreadPool(10);
    //1s只放5个令牌
    final RateLimiter rateLimiter = RateLimiter.create(5);

    for (int i = 0; i < 10; i++) {
        final int j = i;
        exe.submit(() -> {
            try {
                //等待统一发号施令
                startLatch.await();
                long time1 = System.currentTimeMillis();
                System.out.println(j + "尝试中...,需要等待:" + rateLimiter.acquire() * 1000);
                System.out.println(j + "已获得令牌,耗时:" + (System.currentTimeMillis() - time1));
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                runLatch.countDown();
            }

        });

    }
    try {
        //等待700ms 使令牌桶中先有几个令牌
        Thread.sleep(700);
        System.out.println("即将发号施令");
        //发号施令
        startLatch.countDown();
        //等待所有完成
        runLatch.await();
        System.out.println("所有已完成");
    }catch (Exception e){
        e.printStackTrace();
    }
}

运行结果如下:

即将发号施令
0尝试中...,需要等待:0.0
0已获得令牌,耗时:0
4尝试中...,需要等待:0.0
4已获得令牌,耗时:0
8尝试中...,需要等待:0.0
8已获得令牌,耗时:0
1尝试中...,需要等待:0.0
1已获得令牌,耗时:0
5尝试中...,需要等待:23.738
5已获得令牌,耗时:26
9尝试中...,需要等待:222.748
9已获得令牌,耗时:224
2尝试中...,需要等待:402.048
2已获得令牌,耗时:401
6尝试中...,需要等待:600.35
6已获得令牌,耗时:599
3尝试中...,需要等待:800.318
3已获得令牌,耗时:800
7尝试中...,需要等待:1000.302
7已获得令牌,耗时:998
所有已完成

可以看到 0,4,8,1基本同时拿到令牌,后面的基本等待 200ms 才能取到令牌继续执行。只要令牌桶中有足够数量的令牌,可以允许一定程度的并发,比如秒杀场景的限流。

了解RateLimiter 的更多方法:http://ifeve.com/guava-ratelimiter/

你可能感兴趣的:(RateLimiter)