并发编程案例分析——高性能限流器Guava RateLimiter(四十六)
简述
- Guava RateLimiter可以解决高并发场景下的限流问题,Guava 是 Google 开源的 Java 类库,提供了一个工具类 RateLimiter
- 何时用到限流:比如有一个线程池,每秒只能处理两个任务,如果提交任务过快,可能会导致系统不稳定,这个时候就需要限流
- 案例:


经典限流算法:令牌桶算法
- Guava 采用的是令牌桶算法,其核心是要想通过限流器,必须拿到令牌。
- 令牌以固定的速率添加到令牌桶中,如果限流的速率是 r/ 秒,则令牌每 1/r 秒会添加一个;
- 如果令牌桶已满,则新的令牌会被丢弃;
- 请求能够通过限流器的前提是令牌桶中有令牌。
- 容量 b:burst 的简写,限流器允许的最大突发流量。
- Java实现:可以使用java实现,但是会存在一定的问题,比如使用生产者-消费者模式实现:一个生产者线程定时向阻塞队列中添加令牌,而试图通过限流器的线程则作为消费者线程,只有从阻塞队列中获取到令牌,才允许通过限流器。
- 实现并不难,但是并发量上去之后,系统压力已经濒临极限,此时定时器的精度误差会非常大,同时定时器本身也会创建调度线程,也会对系统性能造成影响
令牌桶容量为1时令牌桶算法实现
- 需要两个属性,一个是下一个令牌产生时间next,一个是发放令牌间隔:纳秒 interval
- 关键在于预占令牌reserve() ,这个方法会为请求令牌的线程预分配令牌,同时返回该线程能够获取令牌的时间。
- 如果线程请求令牌的时间在下一令牌产生时间之后,那么该线程立刻就能够获取令牌;申请令牌acquire()
- 反之,如果请求时间在下一令牌产生时间之前,那么该线程是在下一令牌产生的时间获取令牌。由于此时下一令牌已经被该线程预占,所以下一令牌产生的时间需要多等一个间隔。

如果令牌桶的容量大于 1时令牌桶算法实现
- 四个主属性,当前桶中的令牌数量storedPermits,令牌桶的容量maxPermits,下一令牌产生时间next,发放令牌间隔interval
- 按照令牌桶算法,令牌要首先从令牌桶中出,所以我们需要按需计算令牌桶中的数量,当有线程请求令牌时,先从令牌桶中出。
- 增加了一个 resync() 方法,在这个方法中,如果线程请求令牌的时间在下一令牌产生时间之后,会重新计算令牌桶中的令牌数,新产生的令牌的计算公式是:(now-next)/interval。reserve() 方法中,则增加了先从令牌桶中出令牌的逻辑,不过需要注意的是,如果令牌是从令牌桶中出的,那么 next 就无需增加一个 interval 了。


总结
- 经典的限流算法有两个,一个是令牌桶算法(Token Bucket),另一个是漏桶算法(Leaky Bucket)。
- 令牌桶算法是定时向令牌桶发送令牌,请求能够从令牌桶中拿到令牌,然后才能通过限流器;
- 而漏桶算法里,请求就像水一样注入漏桶,漏桶会按照一定的速率自动将水漏掉,只有漏桶里还能注入水的时候,请求才能通过限流器。
- 这两个算法可以相互验证实现
- 示例代码就是对 Guava RateLimiter 的简化,Guava RateLimiter 扩展了标准的令牌桶算法,而且还支持预热功能。
- 对于按需加载的缓存来说,预热后缓存能支持 5 万 TPS 的并发,但是在预热前 5 万 TPS 的并发直接就把缓存击垮了,所以如果需要给该缓存限流,限流器也需要支持预热功能在初始阶段,限制的流速 r 很小,但是动态增长的。
- 预热功能的实现非常复杂,Guava 构建了一个积分函数来解决这个问题。
