正文
向技术致敬的最佳方案: 给予技术分享传播者一个点赞、收藏 。
(方案不是很成熟,但是可以尝试)
开搞:
① 引入相关依赖,pom.xml :
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
② (其实就是使用guava的,简单做了一层业务包装)MyRateLimiter.java :
vimport com.google.common.util.concurrent.RateLimiter; import org.springframework.stereotype.Component; @Component public class MyRateLimiter { /** * 账号注册限流器 每秒只发出5个令牌 */ private RateLimiter accountRegisterRateLimiter = RateLimiter.create(5.0); /** * 短信发送限流器 每秒只发出3个令牌 */ private RateLimiter smsSendRateLimiter = RateLimiter.create(3.0); /** * 尝试获取令牌,返回尝试结果 * * @return */ public boolean tryAccountRegisterAcquire() { return accountRegisterRateLimiter.tryAcquire(); } /** * 取令牌,暂时取不到会一直去尝试 * @return */ public double accountRegisterAcquire() { return accountRegisterRateLimiter.acquire(); } /** * 尝试获取令牌 * * @return */ public boolean trySmsSendAcquire() { return smsSendRateLimiter.tryAcquire(); } }
代码简析:
源码解析:
用的简单,但是我们需要简单看看源码,方便我们可以根据业务场景做相关调整。
简单翻译一下两个用的比较多的create函数里面的注释:
方法 一
public static RateLimiter create(double permitsPerSecon);
咱大白话翻译(其实源码上有注释,还有举例):
保证每秒处理不超过 permitsPerSecond个请求。
如果每秒请求数爆炸,超过我们设置的permitsPerSecond 数量,会慢慢处理。
如果每秒请求书很少,这个permitsPerSecond相当于令牌,会囤积起来,最多囤积permitsPerSecond个。
方法 二
public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) ;
咱大白话翻译(其实源码上有注释,还有举例):
保证了平均每秒不超过permitsPerSecond个请求。
但是这个创建出来的限流器有一个热身期(warmup period)。
热身期内,RateLimiter会平滑的将其释放令牌的速率加大,直到起达到最大速率。
同样,如果RateLimiter在热身期没有足够的请求(unused),则起速率会逐渐降低到冷却状态。
玩过阿里的那个 Sentinel组件的话,应该对这种热身限流策略不会陌生,其实限流策略都是这几种,万变不离其宗。
设计这个的意图是为了满足那种资源提供方需要热身时间,
而不是每次访问都能提供稳定速率的服务的情况(比如带缓存服务,需要定期刷新缓存的)
参数warmupPeriod和unit决定了其从冷却状态到达最大速率的时间。
再来简单看看 尝试获取令牌的 tryAcquire函数
//尝试获取一个令牌,立即返回尝试结果
public boolean tryAcquire();
//尝试获取 permits 个令牌,立即返回尝试结果
public boolean tryAcquire(int permits);
//尝试获取一个令牌,带超时时间传参
public boolean tryAcquire(long timeout, TimeUnit unit);
//尝试获取permits个令牌,带超时时间传参
public boolean tryAcquire(int permits, long timeout, TimeUnit unit);
再看看 获取令牌的 acquire函数
//默认 permits是 1 ,也就是默认拿1个令牌
public double acquire();
//令牌自己定,权重大一点的业务,也许需要拿3个令牌才能执行一次(举例)
public double acquire(int permits);
可以看到返回值是个dubbo ,其实这是一个等待的时间:
rateLimiter.acquire()该方法会阻塞线程,直到令牌桶中能取到令牌为止才继续向下执行,并返回等待的时间。
好了,开始结合实际案例玩一把 。
模拟场景,我们提供一个 账号注册接口,注册接口需要限流。
然后我们再模拟一个并发接口,多线程去调度 注册接口,模拟出 注册接口被短时间并发调用的场景,看看限流器RateLimiter 玩出来的效果。
首先写个简单的HTTP GET 请求调用函数:
HttpUtil.java
import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; public class HttpUtil { /** * get请求 * * @param realUrl * @return */ public static String sendGet(URL realUrl) { String result = ""; BufferedReader in = null; try { // 打开和URL之间的连接 URLConnection connection = realUrl.openConnection(); // 设置通用的请求属性 connection.setRequestProperty("accept", "*/*"); connection.setRequestProperty("connection", "Keep-Alive"); connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); // 建立实际的连接 connection.connect(); // 定义 BufferedReader输入流来读取URL的响应 in = new BufferedReader(new InputStreamReader( connection.getInputStream())); String line; while ((line = in.readLine()) != null) { result += line; } } catch (Exception e) { System.out.println("发送GET请求出现异常!" + e); e.printStackTrace(); } // 使用finally块来关闭输入流 finally { try { if (in != null) { in.close(); } } catch (Exception e2) { e2.printStackTrace(); } } return result; } }
然后写个模拟的注册接口:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @Controller public class UserController { @Autowired MyRateLimiter myRateLimiter; @RequestMapping("/userRegister") @ResponseBody public String userRegister() { DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"); //尝试获取令牌 boolean acquire = myRateLimiter.tryAccountRegisterAcquire(); System.out.println(Thread.currentThread().getName() + " 尝试 获取令牌结果"+acquire); if (acquire) { try { //模拟业务执行500毫秒 Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } return Thread.currentThread().getName()+"userRegister 拿到令牌很顺利success [" + dtf.format(LocalDateTime.now()) + "]"; } else { return Thread.currentThread().getName()+"userRegister 被 limit 限制了 [" + dtf.format(LocalDateTime.now())+ "]"; } } }
可以看到这个接口里面,我们当前只 玩了一下 尝试获取令牌函数 tryAcquire 。
OK,我们来 写个多线程调用接口,来看看这时候限流的效果:
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.net.MalformedURLException; import java.net.URL; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import static com.example.dotest.util.HttpUtil.sendGet; import static java.util.concurrent.Executors.*; @RestController public class TestController { ExecutorService fixedThreadPool = newFixedThreadPool(10); @RequestMapping("/test") public void test() throws MalformedURLException, InterruptedException { final URL url = new URL("http://localhost:8696/userRegister"); for(int i=0;i<10;i++) { fixedThreadPool.submit(() -> System.out.println(sendGet(url))); } fixedThreadPool.shutdown(); fixedThreadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS); } }
简析:
可以看到效果:
可以看到我们的代码,目前用的这个 tryAcquire 函数 ,返回boolean值 ,只要是拿不到令牌我们就直接不做处理了。
这时候其实也可以考虑这么使用,拿不到的 做一些降级、熔断操作或者重试等待啥的。
那么,我们如果想,尝试拿的时候拿不到,让线程自己去帮我们继续自旋去等待持续获取令牌呢?
这时候我们就需要用的是 acquire 函数 。
改造一下刚才的模拟接口:
@RequestMapping("/userRegister") @ResponseBody public String userRegister() { DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"); //尝试获取令牌 boolean tryAcquire = myRateLimiter.tryAccountRegisterAcquire(); System.out.println(Thread.currentThread().getName() + " 尝试 获取令牌结果"+tryAcquire); double registerAcquireWaitTime = myRateLimiter.accountRegisterAcquire(); System.out.println(Thread.currentThread().getName() + " 坚持 获取令牌,被限制的时间是"+registerAcquireWaitTime); if (tryAcquire) { //模拟业务执行500毫秒 try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } return Thread.currentThread().getName()+"userRegister 拿到令牌很顺利success [" + dtf.format(LocalDateTime.now()) + "]"; } else { return Thread.currentThread().getName()+"userRegister拿到令牌不是很顺利被 limit 过,但是还是拿到了 [" + dtf.format(LocalDateTime.now())+ "]"; } } }
简析:
继续调用一下接口,看看这时候限流器的效果:
限流效果: