• Springboot 小巧简便的限流器使用 RateLimiter


    前言

    之前,写过一篇基于redis限流,能应用到分布式相关场景:
    (Redis使用系列) Springboot 使用redis实现接口Api限流 十_小目标青年的博客-CSDN博客

    也在很久之前,写过一个使用也非常便捷的,整合current-limiting的:

    Springboot 整合 Current-Limiting 实现接口限流_小目标青年的博客-CSDN博客

    也在很久很久之前,写过一个使用资源数做限流的(可以自己去设计资源令牌的生成等等):

    Springboot 线程同步之Semaphore 的简单使用_小目标青年的博客-CSDN博客


    那么这次,整个平时敲代码也经常用到的,小巧的限流器,玩的也就是guava的 RateLimiter。

    正文

     向技术致敬的最佳方案: 给予技术分享传播者一个点赞、收藏 。

    (方案不是很成熟,但是可以尝试)

    开搞:
     

    ① 引入相关依赖,pom.xml  :

    1. com.google.guava
    2. guava
    3. 18.0
    4. org.springframework.boot
    5. spring-boot-starter-web

    ② (其实就是使用guava的,简单做了一层业务包装)MyRateLimiter.java :

    1. import com.google.common.util.concurrent.RateLimiter;
    2. import org.springframework.stereotype.Component;
    3. @Component
    4. public class MyRateLimiter {
    5. /**
    6. * 账号注册限流器 每秒只发出5个令牌
    7. */
    8. private RateLimiter accountRegisterRateLimiter = RateLimiter.create(5.0);
    9. /**
    10. * 短信发送限流器 每秒只发出3个令牌
    11. */
    12. private RateLimiter smsSendRateLimiter = RateLimiter.create(3.0);
    13. /**
    14. * 尝试获取令牌,返回尝试结果
    15. *
    16. * @return
    17. */
    18. public boolean tryAccountRegisterAcquire() {
    19. return accountRegisterRateLimiter.tryAcquire();
    20. }
    21. /**
    22. * 取令牌,暂时取不到会一直去尝试
    23. * @return
    24. */
    25. public double accountRegisterAcquire() {
    26. return accountRegisterRateLimiter.acquire();
    27. }
    28. /**
    29. * 尝试获取令牌
    30. *
    31. * @return
    32. */
    33. public boolean trySmsSendAcquire() {
    34. return smsSendRateLimiter.tryAcquire();
    35. }
    36. }


    代码简析:

     

    源码解析:


    用的简单,但是我们需要简单看看源码,方便我们可以根据业务场景做相关调整。

     

    简单翻译一下两个用的比较多的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

    1. import java.io.BufferedReader;
    2. import java.io.InputStreamReader;
    3. import java.net.URL;
    4. import java.net.URLConnection;
    5. public class HttpUtil {
    6. /**
    7. * get请求
    8. *
    9. * @param realUrl
    10. * @return
    11. */
    12. public static String sendGet(URL realUrl) {
    13. String result = "";
    14. BufferedReader in = null;
    15. try {
    16. // 打开和URL之间的连接
    17. URLConnection connection = realUrl.openConnection();
    18. // 设置通用的请求属性
    19. connection.setRequestProperty("accept", "*/*");
    20. connection.setRequestProperty("connection", "Keep-Alive");
    21. connection.setRequestProperty("user-agent",
    22. "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
    23. // 建立实际的连接
    24. connection.connect();
    25. // 定义 BufferedReader输入流来读取URL的响应
    26. in = new BufferedReader(new InputStreamReader(
    27. connection.getInputStream()));
    28. String line;
    29. while ((line = in.readLine()) != null) {
    30. result += line;
    31. }
    32. } catch (Exception e) {
    33. System.out.println("发送GET请求出现异常!" + e);
    34. e.printStackTrace();
    35. }
    36. // 使用finally块来关闭输入流
    37. finally {
    38. try {
    39. if (in != null) {
    40. in.close();
    41. }
    42. } catch (Exception e2) {
    43. e2.printStackTrace();
    44. }
    45. }
    46. return result;
    47. }
    48. }

    然后写个模拟的注册接口:
     

    1. import org.springframework.beans.factory.annotation.Autowired;
    2. import org.springframework.stereotype.Controller;
    3. import org.springframework.web.bind.annotation.RequestMapping;
    4. import org.springframework.web.bind.annotation.ResponseBody;
    5. import java.time.LocalDateTime;
    6. import java.time.format.DateTimeFormatter;
    7. @Controller
    8. public class UserController {
    9. @Autowired
    10. MyRateLimiter myRateLimiter;
    11. @RequestMapping("/userRegister")
    12. @ResponseBody
    13. public String userRegister() {
    14. DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
    15. //尝试获取令牌
    16. boolean acquire = myRateLimiter.tryAccountRegisterAcquire();
    17. System.out.println(Thread.currentThread().getName() + " 尝试 获取令牌结果"+acquire);
    18. if (acquire) {
    19. try {
    20. //模拟业务执行500毫秒
    21. Thread.sleep(500);
    22. } catch (InterruptedException e) {
    23. e.printStackTrace();
    24. }
    25. return Thread.currentThread().getName()+"userRegister 拿到令牌很顺利success [" + dtf.format(LocalDateTime.now()) + "]";
    26. } else {
    27. return Thread.currentThread().getName()+"userRegister 被 limit 限制了 [" + dtf.format(LocalDateTime.now())+ "]";
    28. }
    29. }
    30. }

    可以看到这个接口里面,我们当前只 玩了一下 尝试获取令牌函数 tryAcquire   

    OK,我们来 写个多线程调用接口,来看看这时候限流的效果:

     

    1. import org.springframework.web.bind.annotation.RequestMapping;
    2. import org.springframework.web.bind.annotation.RestController;
    3. import java.net.MalformedURLException;
    4. import java.net.URL;
    5. import java.util.concurrent.ExecutorService;
    6. import java.util.concurrent.TimeUnit;
    7. import static com.example.dotest.util.HttpUtil.sendGet;
    8. import static java.util.concurrent.Executors.*;
    9. @RestController
    10. public class TestController {
    11. ExecutorService fixedThreadPool = newFixedThreadPool(10);
    12. @RequestMapping("/test")
    13. public void test() throws MalformedURLException, InterruptedException {
    14. final URL url = new URL("http://localhost:8696/userRegister");
    15. for(int i=0;i<10;i++) {
    16. fixedThreadPool.submit(() -> System.out.println(sendGet(url)));
    17. }
    18. fixedThreadPool.shutdown();
    19. fixedThreadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
    20. }
    21. }

    简析:

     可以看到效果:

     

    可以看到我们的代码,目前用的这个 tryAcquire 函数 ,返回boolean值 ,只要是拿不到令牌我们就直接不做处理了。

    这时候其实也可以考虑这么使用,拿不到的 做一些降级、熔断操作或者重试等待啥的。

    那么,我们如果想,尝试拿的时候拿不到,让线程自己去帮我们继续自旋去等待持续获取令牌呢?

    这时候我们就需要用的是   acquire  函数   

    改造一下刚才的模拟接口:

     

    1. @RequestMapping("/userRegister")
    2. @ResponseBody
    3. public String userRegister() {
    4. DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss");
    5. //尝试获取令牌
    6. boolean tryAcquire = myRateLimiter.tryAccountRegisterAcquire();
    7. System.out.println(Thread.currentThread().getName() + " 尝试 获取令牌结果"+tryAcquire);
    8. double registerAcquireWaitTime = myRateLimiter.accountRegisterAcquire();
    9. System.out.println(Thread.currentThread().getName() + " 坚持 获取令牌,被限制的时间是"+registerAcquireWaitTime);
    10. if (tryAcquire) {
    11. //模拟业务执行500毫秒
    12. try {
    13. Thread.sleep(500);
    14. } catch (InterruptedException e) {
    15. e.printStackTrace();
    16. }
    17. return Thread.currentThread().getName()+"userRegister 拿到令牌很顺利success [" + dtf.format(LocalDateTime.now()) + "]";
    18. } else {
    19. return Thread.currentThread().getName()+"userRegister拿到令牌不是很顺利被 limit 过,但是还是拿到了 [" + dtf.format(LocalDateTime.now())+ "]";
    20. }
    21. }
    22. }

    简析: 

     

    继续调用一下接口,看看这时候限流器的效果:

    限流效果: 

     

  • 相关阅读:
    Java安全之Mojarra JSF反序列化
    华清远见(上海中心)22071
    【Debug危机系列】Embedding层的千层套路
    网站使用谷歌登录 oauth java nuxt auth
    类别不均衡,离群点以及分布改变
    CSS动画-transition/animation
    IDEA项目取消git版本管控并添加svn版本控制
    MyBatis的xml里#{}的参数为null报错、将null作为参数传递报错问题
    NVIDIA TensorRT 简介及使用
    pytest+allure生成测试报告
  • 原文地址:https://blog.csdn.net/qq_35387940/article/details/127923229