Springboot 小巧简便的限流器使用 RateLimiter
创始人
2024-02-04 21:34:18
0

前言

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

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

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

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

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


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

正文

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

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

开搞:
 

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

        com.google.guavaguava18.0org.springframework.bootspring-boot-starter-web

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

import 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 {@AutowiredMyRateLimiter myRateLimiter;@RequestMapping("/userRegister")@ResponseBodypublic 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")@ResponseBodypublic 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())+ "]";}}
}

简析: 

 

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

限流效果: 

 

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
一帆风顺二龙腾飞三阳开泰祝福语... 本篇文章极速百科给大家谈谈一帆风顺二龙腾飞三阳开泰祝福语,以及一帆风顺二龙腾飞三阳开泰祝福语结婚对应...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...