基于Java編寫一個(gè)限流工具類RateLimiter
限流工具類RateLimiter
原理:令牌桶算法
有一個(gè)桶,桶的容量固定。系統(tǒng)以恒定的速度往桶里放令牌,令牌數(shù)不超過桶的容量。
用戶發(fā)送請(qǐng)求進(jìn)來(lái),需要先從桶里獲取一個(gè)令牌才能通過,獲取后桶里令牌數(shù)減一。如果系統(tǒng)兩秒一個(gè)往桶里放令牌,用戶請(qǐng)求一秒一次,那么當(dāng)令牌被取空后,操作就需要等待,會(huì)被限流。
可以應(yīng)對(duì)突發(fā)流量,當(dāng)桶里有足夠多的令牌,可以一次處理多個(gè)請(qǐng)求
1.導(dǎo)入guava依賴包
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>30.1-jre</version> </dependency>
RateLimiter的集個(gè)核心方法:create()
、tryAcquire()
- acquire() 獲取一個(gè)令牌, 會(huì)阻塞當(dāng)前線程,直到獲取到一個(gè)令牌。該方法返回值類型為
double
,表示當(dāng)前線程需要等待的時(shí)間(單位:秒),這個(gè)時(shí)間取決于令牌桶中令牌的剩余數(shù)量和發(fā)放速率。當(dāng)執(zhí)行rateLimiter.acquire()
方法時(shí),如果令牌桶中還有剩余的令牌,則該方法會(huì)立即返回,返回值為 0,表示當(dāng)前線程無(wú)需等待即可獲取到一個(gè)令牌。如果令牌桶中沒有令牌,那么該方法就會(huì)阻塞當(dāng)前線程,直到令牌桶中有令牌可用或者線程被中斷。 - acquire(int permits) 獲取指定數(shù)量的令牌, 該方法也會(huì)阻塞, 返回值為獲取到這 N 個(gè)令牌花費(fèi)的時(shí)間
- tryAcquire() 判斷時(shí)候能獲取到令牌, 如果不能獲取立即返回 false
- tryAcquire(int permits) 獲取指定數(shù)量的令牌, 如果不能獲取立即返回 false
- tryAcquire(long timeout, TimeUnit unit) 判斷能否在指定時(shí)間內(nèi)獲取到令牌, 如果不能獲取立即返回 false
- tryAcquire(int permits, long timeout, TimeUnit unit) 判斷能否在指定時(shí)間內(nèi)獲取到指定數(shù)量的令牌, 如果不能獲取立即返回 false
- create(double permitsPerSecond) 創(chuàng)建每秒放入指定數(shù)量的令牌桶。SmoothBursty模式
- create(5.0, 1, TimeUnit.SECONDS) 每秒5個(gè)令牌,預(yù)熱時(shí)間1秒,SmoothWarmingUp模式
2.代碼
public void limitTest(){ DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); //創(chuàng)建令牌桶,一秒一個(gè),容量為1 RateLimiter rateLimiter = RateLimiter.create(1); //獲取放令牌的速率 System.out.println("放令牌的速率:"+rateLimiter.getRate()); for (int i=0;i<5;i++){ //獲取令牌,會(huì)阻塞,返回等待的時(shí)間 double acquire = rateLimiter.acquire(); System.out.println("第"+i+" 個(gè)令牌獲取到的時(shí)間:"+LocalDateTime.now().format(dtf)+",等待時(shí)間:"+acquire); } //是否會(huì)立即獲取到令牌 boolean tryAcquire = rateLimiter.tryAcquire(); }
3.AOP+RateLimiter+注解,實(shí)現(xiàn)限流
1.創(chuàng)建注解
public @interface Limit { // 資源主鍵 String key() default ""; //最多訪問次數(shù),代表請(qǐng)求總數(shù)量 double permitsPerSeconds(); // 時(shí)間:即timeout時(shí)間內(nèi),只允許有permitsPerSeconds個(gè)請(qǐng)求總數(shù)量訪問,超過的將被限制不能訪問 long timeout(); //時(shí)間類型,默認(rèn)秒 TimeUnit timeUnit() default TimeUnit.SECONDS; //提示信息 String msg() default "系統(tǒng)繁忙,請(qǐng)稍后重試"; }
2.AOP切面
@Aspect @Component public class LimitAop { private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap(); private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); @Around("@annotation(com.limit.Limit)") public Object around(ProceedingJoinPoint joinPoint) throws Throwable{ MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); //拿limit的注解 Limit limit = method.getAnnotation(Limit.class); if (limit != null) { // key作用:不同的接口,不同的流量控制,相當(dāng)于每個(gè)接口有一個(gè)對(duì)應(yīng)的令牌桶 String key = limit.key(); RateLimiter rateLimiter; //驗(yàn)證緩存是否有命中key if (!limitMap.containsKey(key)) { //創(chuàng)建令牌桶 rateLimiter = RateLimiter.create(limit.permitsPerSeconds()); limitMap.put(key, rateLimiter); log.info("新建了令牌桶={},容量={}", key, limit.permitsPerSeconds()); } rateLimiter = limitMap.get(key); //拿一個(gè)令牌,拿不到會(huì)一直阻塞 double acquire = rateLimiter.acquire(1); log.info("{},獲取令牌時(shí)間{}", key,LocalDateTime.now().format(dtf)); /* //是否能立即拿到令牌,不能則桶里沒有,還沒到一秒鐘,進(jìn)行限流 boolean acquire = rateLimiter.tryAcquire(); if (!acquire) { log.info("令牌桶={},獲取令牌失敗", key); throw new RuntimeException(limit.msg()); }*/ } return joinPoint.proceed(); } }
3.注解使用
@GetMapping("/limitTest") @Limit(key = "limitTest",permitsPerSeconds = 1,timeout = 1,msg = "觸發(fā)接口限流,請(qǐng)重試") public void limitTest(){ //其它邏輯代碼 //這里打印調(diào)用該接口執(zhí)行的時(shí)間,以便觀察限流 DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); System.out.println(LocalDateTime.now().format(dtf)); }
日志情況:
無(wú)論請(qǐng)求速度多快,一秒后才能處理請(qǐng)求。因?yàn)閞ateLimiter.acquire(1)拿不到,一直等待,會(huì)阻塞。 其它獲取令牌的方法感興趣可以按照上文方法詳情自行測(cè)試。
到此這篇關(guān)于基于Java編寫一個(gè)限流工具類RateLimiter的文章就介紹到這了,更多相關(guān)Java限流工具類內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot使用Logback進(jìn)行日志記錄的代碼示例
在開發(fā)Web應(yīng)用程序時(shí),日志記錄是非常重要的一部分,在SpringBoot中,我們可以使用Logback進(jìn)行日志記錄,Logback是一款高性能、靈活的日志框架,它可以滿足各種不同的日志需求,在本文中,我們介紹了如何在SpringBoot中使用Logback進(jìn)行日志記錄2023-06-06java模擬實(shí)現(xiàn)銀行ATM機(jī)操作
這篇文章主要為大家詳細(xì)介紹了java模擬實(shí)現(xiàn)銀行ATM機(jī)操作,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05Java利用反射動(dòng)態(tài)設(shè)置對(duì)象字段值的實(shí)現(xiàn)
橋梁信息維護(hù)需要做到字段級(jí)別的權(quán)限控制,本文主要介紹了Java利用反射動(dòng)態(tài)設(shè)置對(duì)象字段值的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01Spring Boot文件上傳簡(jiǎn)單實(shí)例代碼
在本篇文章里小編給大家分享的是關(guān)于Spring Boot 文件上傳簡(jiǎn)易教程以及相關(guān)知識(shí)點(diǎn),需要的朋友們參考下。2019-08-08springboot tomcat的maxHttpFormPostSize參數(shù)示例解析
這篇文章主要介紹了springboot tomcat的maxHttpFormPostSize參數(shù)示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08SpringBoot單點(diǎn)登錄實(shí)現(xiàn)過程詳細(xì)分析
這篇文章主要介紹了SpringBoot單點(diǎn)登錄實(shí)現(xiàn)過程,單點(diǎn)登錄英文全稱Single?Sign?On,簡(jiǎn)稱就是SSO。它的解釋是:在多個(gè)應(yīng)用系統(tǒng)中,只需要登錄一次,就可以訪問其他相互信任的應(yīng)用系統(tǒng)2022-12-12Spring定時(shí)任務(wù)輪詢本地?cái)?shù)據(jù)庫(kù)實(shí)現(xiàn)過程解析
這篇文章主要介紹了Spring定時(shí)任務(wù)輪詢本地?cái)?shù)據(jù)庫(kù)實(shí)現(xiàn)過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01java使用java.util.Date獲取指定日期的年、月、日、時(shí)、分、秒
在Java中獲取當(dāng)前時(shí)間和日期是很常見的操作,也是很重要的操作,下面這篇文章主要給大家介紹了關(guān)于java使用java.util.Date獲取指定日期的年、月、日、時(shí)、分、秒的相關(guān)資料,需要的朋友可以參考下2024-01-01