SpringBoot?實(shí)現(xiàn)流控的操作方法

概述
限流 簡言之就是當(dāng)請求達(dá)到一定的并發(fā)數(shù)或速率,就對服務(wù)進(jìn)行等待、排隊(duì)、降級、拒絕服務(wù)等操作。
限流算法
我們先簡單捋一捋限流算法
SpringBoot接口限流的實(shí)現(xiàn)方法小結(jié)
計(jì)數(shù)器限流
漏桶算法

把水比作是請求,漏桶比作是系統(tǒng)處理能力極限,水先進(jìn)入到漏桶里,漏桶里的水按一定速率流出,當(dāng)流出的速率小于流入的速率時(shí),由于漏桶容量有限,后續(xù)進(jìn)入的水直接溢出(拒絕請求),以此實(shí)現(xiàn)限流
令牌桶算法

可以簡單地理解為醫(yī)去銀行辦理業(yè)務(wù),只有拿到號以后才可以進(jìn)行業(yè)務(wù)辦理。
系統(tǒng)會維護(hù)一個(gè)令牌(token)桶,以一個(gè)恒定的速度往桶里放入令牌(token),這時(shí)如果有請求進(jìn)來想要被處理,則需要先從桶里獲取一個(gè)令牌(token),當(dāng)桶里沒有令牌(token)可取時(shí),則該請求將被拒絕服務(wù)。令牌桶算法通過控制桶的容量、發(fā)放令牌的速率,來達(dá)到對請求的限制。
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è)請求
*/
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),沒拿到令牌,就直接進(jìn)入服務(wù)降級
boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS);
if (!tryAcquire) {
log.warn("BOOM 服務(wù)降級,時(shí)間{}", LocalDateTime.now().format(dtf));
return "系統(tǒng)繁忙,請稍后再試!";
}
log.info("獲取令牌成功,時(shí)間{}", LocalDateTime.now().format(dtf));
return "業(yè)務(wù)處理成功";
}我們可以看到RateLimiter的2個(gè)核心方法:create()、tryAcquire()
- acquire() 獲取一個(gè)令牌, 改方法會阻塞直到獲取到這一個(gè)令牌, 返回值為獲取到這個(gè)令牌花費(fèi)的時(shí)間
- acquire(int permits) 獲取指定數(shù)量的令牌, 該方法也會阻塞, 返回值為獲取到這 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) 同上
測試一下

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 "";
/**
* 最多的訪問限制次數(shù)
*/
double permitsPerSecond();
/**
* 獲取令牌最大等待時(shí)間
*/
long timeout();
/**
* 獲取令牌最大等待時(shí)間,單位(例:分鐘/秒/毫秒) 默認(rèn):毫秒
*/
TimeUnit timeunit() default TimeUnit.MILLISECONDS;
/**
* 得不到令牌的提示語
*/
String message() default "系統(tǒ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ù)較多,請稍后再試!")
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)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java Spire.XLS快速創(chuàng)建與讀取Excel的實(shí)戰(zhàn)技巧
在現(xiàn)代軟件開發(fā)中,Excel 文檔的管理和操作是一個(gè)常見的需求,,本文將詳細(xì)介紹如何使用 Spire.XLS for Java 庫,以便輕松地讀寫 Excel 文檔,感興趣的小伙伴可以了解下2025-12-12
Springboot如何實(shí)現(xiàn)對配置文件中的明文密碼加密
這篇文章主要介紹了Springboot如何實(shí)現(xiàn)對配置文件中的明文密碼加密問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12
使用SpringMVC的@Validated注解驗(yàn)證的實(shí)現(xiàn)
這篇文章主要介紹了使用SpringMVC的@Validated注解驗(yàn)證的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
Java 中 getClass() 方法的使用與原理深入分析(對象類型信息)
在 Java 編程中,getClass() 是一個(gè)非常重要的方法,它用于獲取對象的運(yùn)行時(shí)類信息,無論是調(diào)試代碼、反射操作,還是類型檢查,getClass() 都扮演著關(guān)鍵角色,本文將深入探討 getClass() 的使用方法、底層原理以及實(shí)際應(yīng)用場景,感興趣的朋友一起看看吧2024-12-12
SpringBoot操作spark處理hdfs文件的操作方法
本文介紹了如何使用Spring Boot操作Spark處理HDFS文件,包括導(dǎo)入依賴、配置Spark信息、編寫Controller和Service處理地鐵數(shù)據(jù)、運(yùn)行項(xiàng)目以及觀察Spark和HDFS的狀態(tài),感興趣的朋友跟隨小編一起看看吧2025-01-01
Java批量向PDF文件中添加圖像水印實(shí)現(xiàn)細(xì)節(jié)
這篇文章主要為大家介紹了Java批量向PDF文件中添加圖像水印實(shí)現(xiàn)細(xì)節(jié),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05

