目录
一、引入
二、快速上手
2.1、导入依赖
2.2、第一个示例
三、获取许可
3.1、非阻塞式获取
3.2、阻塞式获取
四、存在的问题
4.1、集群限流
一、引入
在程序中,我们经常会用到限流,比如接口调用的频率限制。
server端提供api给clients进行调用,如果某个client调用api的频率过高,造成server端的负载升高,超过server端的上限,那么很有可能导致server端不可用,从而影响所有的调用方。
限制频率,可以在client端做,也可以在server端做,但是目前一般都是在server端做,同时client一般也会调整调用频率。
至于怎么限流,网上很多的资料,这里就不阐述了,主要介绍一下使用Guava RateLimiter来实现限流。
二、快速上手
2.1、导入依赖
Guava RateLimiter是Guava的一部分,所以直接导入Guava的依赖即可。
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>28.0-jre</version> </dependency>
2.2、第一个示例
简单看一下下面的用法
package cn.ganlixin.guava; import com.google.common.util.concurrent.RateLimiter; import org.junit.Test; import java.time.LocalTime; public class UserRateLimiter { @Test public void testSimple() throws InterruptedException { // 创建一个限流器(每秒限制流量为5个) RateLimiter rateLimiter = RateLimiter.create(5.0); for (int i = 0; i < 10; i++) { if (rateLimiter.tryAcquire()) { System.out.println(LocalTime.now() + " 通过"); } else { System.out.println(LocalTime.now() + " 被限流"); } Thread.sleep(100L); } } }
上面的示例代码中,创建了一个限流器,每秒最多允许5个流量,这个流量有个专业的称呼,叫“许可”(准许调用),也就是5个许可。
调用tryAcquire()会尝试获取1个许可,如果获取到了,返回true,表示未被限流;否则返回false,表示没有获取到许可,被限流了。
如此就可以实现流量控制了。
三、获取许可
获取许可,有非阻塞和阻塞式获取。
if (获得许可) { 执行代码块 1 } else { 执行代码块 2 }
非阻塞式:尝试获取许可,如果获取到许可,则执行代码块1,如果没有获取到就认为被限流,则执行代码块2;
阻塞式:尝试获取许可,获取到则执行代码块1,没有获取到,则阻塞,等待获取到许可后,执行代码块1,注意不会执行代码块2;
3.1、非阻塞获取许可
前面使用介绍了,可以使用RateLimiter类可以使用create方法,创建多个许可,每个许可就是一个令牌,拿到令牌,才可以执行操作(通过),否则就是被限流(阻止)。
使用tryAcquire()方法,是不阻塞的尝试获取令牌,但是他有多个重载方法,有不同的参数。
// 尝试获取1个许可,如果获取到,则返回true,否则返回false boolean tryAcquire(); // 尝试获取多个许可,如果获取到,则返回true,否则返回false boolean tryAcquire(int permits) // 示例tryAcquire(3) // 在timeout时间内,尝试获取1个许可,如果获取到,则返回true,否则返回false tryAcquire(Duration timeout); // 示例:tryAcquire(Duration.ofSeconds(3)) tryAcquire(long timeout, TimeUnit unit); // 示例:tryAcquire(3, TimeUnit.SECONDS); // 在timeout时间内,尝试获取多个许可,如果获取到,则返回true,否则返回false tryAcquire(int permits, Duration timeout) tryAcquire(int permits, long timeout, TimeUnit unit)
3.2、阻塞式获取许可
阻塞式获取,调用的方法是acquire
package cn.ganlixin.guava; import com.google.common.util.concurrent.RateLimiter; import org.junit.Test; import java.time.LocalTime; public class UserRateLimiter { @Test public void testAcquire() { RateLimiter rateLimiter = RateLimiter.create(2); for (int i = 0; i < 10; i++) { double sleep = rateLimiter.acquire(); System.out.println("now: " + LocalTime.now() + " sleep: " + sleep); } } }
运行输出如下:
now: 16:59:51.879 sleep: 0.0 now: 16:59:52.289 sleep: 0.403329 now: 16:59:52.784 sleep: 0.492976 now: 16:59:53.285 sleep: 0.499409 now: 16:59:53.789 sleep: 0.498335 now: 16:59:54.285 sleep: 0.494628 now: 16:59:54.786 sleep: 0.49857 now: 16:59:55.289 sleep: 0.496816 now: 16:59:55.784 sleep: 0.494352 now: 16:59:56.288 sleep: 0.499635
acquire也有重载方法:
// 尝试阻塞获取多个许可 acquire(int permits)
四、存在的问题
Guava解决的问题,一般都是单机的,以限流为例,使用guava限流,只做到了单机限流,但是我们的服务一般都会由多台机器(集群),虽然我们可以通过计算单机和集群的比例,来设置限流数量,但是有几个问题:
1、机器增减时,要保证总流量保持不变,就需要修改每一台机器的流量限制,这个不是很方便;
2、Guava的限流器,并不是公平的,至于什么是公平和非公平,可以参考:
4.1 集群限流
guava的RateLimiter,限流是通过获取“许可”来控制流量的,只不过是单机管理自己的许可。
如果将所有机器的“许可”汇集到一个地方,所有机器都从这个地方获取许可,不就可以实现集群限流吗?
可以使用redis来保存所有机器的“许可”。
这样做,可以实现集群限流,但不能保证单机的流量限制,其实对于现在的微服务来说,请求被平均分给所有机器,是服务平台的问题,可以不用关心这个问题。