SpringBoot?實(shí)現(xiàn)流控的操作方法
概述
限流 簡(jiǎn)言之就是當(dāng)請(qǐng)求達(dá)到一定的并發(fā)數(shù)或速率,就對(duì)服務(wù)進(jìn)行等待、排隊(duì)、降級(jí)、拒絕服務(wù)等操作。
限流算法
我們先簡(jiǎn)單捋一捋限流算法
SpringBoot接口限流的實(shí)現(xiàn)方法小結(jié)
計(jì)數(shù)器限流
漏桶算法
把水比作是請(qǐng)求,漏桶比作是系統(tǒng)處理能力極限,水先進(jìn)入到漏桶里,漏桶里的水按一定速率流出,當(dāng)流出的速率小于流入的速率時(shí),由于漏桶容量有限,后續(xù)進(jìn)入的水直接溢出(拒絕請(qǐng)求),以此實(shí)現(xiàn)限流
令牌桶算法
可以簡(jiǎn)單地理解為醫(yī)去銀行辦理業(yè)務(wù),只有拿到號(hào)以后才可以進(jìn)行業(yè)務(wù)辦理。
系統(tǒng)會(huì)維護(hù)一個(gè)令牌(token)桶,以一個(gè)恒定的速度往桶里放入令牌(token),這時(shí)如果有請(qǐng)求進(jìn)來(lái)想要被處理,則需要先從桶里獲取一個(gè)令牌(token),當(dāng)桶里沒(méi)有令牌(token)可取時(shí),則該請(qǐng)求將被拒絕服務(wù)。令牌桶算法通過(guò)控制桶的容量、發(fā)放令牌的速率,來(lái)達(dá)到對(duì)請(qǐng)求的限制。
V1.0
上 guava
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>30.1-jre</version> </dependency>
package com.artisan.controller; import com.artisan.annos.ArtisanLimit; import com.google.common.util.concurrent.RateLimiter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.concurrent.TimeUnit; /** * @author 小工匠 * @version 1.0 * @mark: show me the code , change the world */ @Slf4j @RestController @RequestMapping("/rateLimit") public class RateLimitController { /** * 限流策略 : 1秒鐘1個(gè)請(qǐng)求 */ private final RateLimiter limiter = RateLimiter.create(1); private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @SneakyThrows @GetMapping("/test") public String testLimiter() { //500毫秒內(nèi),沒(méi)拿到令牌,就直接進(jìn)入服務(wù)降級(jí) boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS); if (!tryAcquire) { log.warn("BOOM 服務(wù)降級(jí),時(shí)間{}", LocalDateTime.now().format(dtf)); return "系統(tǒng)繁忙,請(qǐng)稍后再試!"; } log.info("獲取令牌成功,時(shí)間{}", LocalDateTime.now().format(dtf)); return "業(yè)務(wù)處理成功"; }
我們可以看到RateLimiter的2個(gè)核心方法:create()、tryAcquire()
- acquire() 獲取一個(gè)令牌, 改方法會(huì)阻塞直到獲取到這一個(gè)令牌, 返回值為獲取到這個(gè)令牌花費(fèi)的時(shí)間
- 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) 同上
測(cè)試一下
V2.0 自定義注解+AOP實(shí)現(xiàn)接口限流
1.0的功能實(shí)現(xiàn)了,但是業(yè)務(wù)代碼和限流代碼混在一起,非常的不美觀。
搞依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
搞自定義限流注解
package com.artisan.annos; import java.lang.annotation.*; import java.util.concurrent.TimeUnit; /** * @author 小工匠 * @version 1.0 * @mark: show me the code , change the world */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface ArtisanLimit { /** * 資源的key,唯一 * 作用:不同的接口,不同的流量控制 */ String key() default ""; /** * 最多的訪問(wèn)限制次數(shù) */ double permitsPerSecond(); /** * 獲取令牌最大等待時(shí)間 */ long timeout(); /** * 獲取令牌最大等待時(shí)間,單位(例:分鐘/秒/毫秒) 默認(rèn):毫秒 */ TimeUnit timeunit() default TimeUnit.MILLISECONDS; /** * 得不到令牌的提示語(yǔ) */ String message() default "系統(tǒng)繁忙,請(qǐng)稍后再試."; }
搞AOP
使用AOP切面攔截限流注解
package com.artisan.aop; import com.artisan.annos.ArtisanLimit; import com.artisan.resp.ResponseCode; import com.artisan.resp.ResponseData; import com.artisan.utils.WebUtils; import com.google.common.collect.Maps; import com.google.common.util.concurrent.RateLimiter; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method; import java.util.Map; /** * @author 小工匠 * @version 1.0 * @mark: show me the code , change the world */ @Slf4j @Aspect @Component public class ArtisanLimitAop { /** * 不同的接口,不同的流量控制 * map的key為 ArtisanLimit.key */ private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap(); @Around("@annotation(com.artisan.annos.ArtisanLimit)") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); //拿ArtisanLimit的注解 ArtisanLimit limit = method.getAnnotation(ArtisanLimit.class); if (limit != null) { //key作用:不同的接口,不同的流量控制 String key = limit.key(); RateLimiter rateLimiter = null; //驗(yàn)證緩存是否有命中key if (!limitMap.containsKey(key)) { // 創(chuàng)建令牌桶 rateLimiter = RateLimiter.create(limit.permitsPerSecond()); limitMap.put(key, rateLimiter); log.info("新建了令牌桶={},容量={}", key, limit.permitsPerSecond()); } rateLimiter = limitMap.get(key); // 拿令牌 boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit()); // 拿不到命令,直接返回異常提示 if (!acquire) { log.warn("令牌桶={},獲取令牌失敗", key); this.responseFail(limit.message()); return null; } } return joinPoint.proceed(); } /** * 直接向前端拋出異常 * * @param msg 提示信息 */ private void responseFail(String msg) { HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); ResponseData<Object> resultData = ResponseData.fail(ResponseCode.LIMIT_ERROR.getCode(), msg); WebUtils.writeJson(response, resultData); } }
用上驗(yàn)證
@GetMapping("/test2") @ArtisanLimit(key = "testLimit2", permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS, message = "test2 當(dāng)前排隊(duì)人數(shù)較多,請(qǐng)稍后再試!") public String test2() { log.info("令牌桶test2獲取令牌成功"); return "test2 ok"; }
源碼
https://github.com/yangshangwei/boot2
到此這篇關(guān)于SpringBoot 實(shí)現(xiàn)流控的操作方法的文章就介紹到這了,更多相關(guān)SpringBoot流控內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實(shí)現(xiàn)OTP(動(dòng)態(tài)口令)服務(wù)
OTP是一種動(dòng)態(tài)生成的短時(shí)有效密碼,用于身份驗(yàn)證,通常在登錄或執(zhí)行敏感操作時(shí)提供額外的安全保障,本文主要介紹了Java實(shí)現(xiàn)OTP(動(dòng)態(tài)口令)服務(wù),感興趣的可以了解一下2025-03-03Windows10系統(tǒng)下修改jar中的文件并重新打包成jar文件然后運(yùn)行的操作步驟
這篇文章主要介紹了Windows10系統(tǒng)下修改jar中的文件并重新打包成jar文件然后運(yùn)行的操作步驟,文中通過(guò)圖文結(jié)合的形式給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-08-08一文帶你掌握J(rèn)ava中Scanner類(lèi)的使用
Scanner類(lèi)是java.util包中的一個(gè)類(lèi),常用于控制臺(tái)的輸入,當(dāng)需要使用控制臺(tái)輸入時(shí)即可調(diào)用這個(gè)類(lèi)。本文將通過(guò)一些簡(jiǎn)單的例子為大家介紹一下Java中Scanner類(lèi)的使用,需要的可以參考一下2023-04-04Spring Boot容器加載時(shí)執(zhí)行特定操作(推薦)
這篇文章主要介紹了Spring Boot容器加載時(shí)執(zhí)行特定操作及spring內(nèi)置的事件,需要的朋友可以參考下2018-01-01在SpringBoot中整合數(shù)據(jù)源的示例詳解
這篇文章主要介紹了在SpringBoot中如何整合數(shù)據(jù)源,本文介紹了如何在SpringBoot項(xiàng)目中整合常見(jiàn)的數(shù)據(jù)源,包括JdbcTemplate、MyBatis和JPA,并探討了如何配置和使用多數(shù)據(jù)源,需要的朋友可以參考下2023-06-06關(guān)于Maven依賴沖突解決之exclusions
這篇文章主要介紹了關(guān)于Maven依賴沖突解決之exclusions用法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12解決Request.getParameter獲取不到特殊字符bug問(wèn)題
這篇文章主要介紹了解決Request.getParameter獲取不到特殊字符bug問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07