首先,解釋一下標(biāo)題。
我們有一個(gè)需求:能為每個(gè)接口單獨(dú)設(shè)置一個(gè)限流值。那么每個(gè)接口都需要增加相應(yīng)的代碼,只有自己寫一個(gè)注解,使用成本才低,對(duì)業(yè)務(wù)代碼的侵入也低。
一、整體思路
- 自定義一個(gè)注解,里面有個(gè)限流值的變量;
- 在需要的接口上,加上該注解,并設(shè)置好限流值,比如:@RateLimit(5);
- 寫一個(gè)針對(duì)該注解的切面,before()階段進(jìn)行限流判斷和限流處理。
二、開始編寫代碼
- 自定義注解
@Inherited
@Documented
@Target({ElementType.METHOD}) //方法級(jí)別
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
/**
* 為了方便管理各個(gè)接口的限流值,這里是設(shè)置對(duì)應(yīng)application.properties里的屬性key,key對(duì)應(yīng)的value才是限流值
*/
String limitKey() default "api.default.limit";
}
- 每個(gè)API上使用該注解
@ApiOperation(value = "查詢產(chǎn)品詳情")
@RequestMapping(value = "/{productId}", method = RequestMethod.GET)
@RateLimit(limitKey = "xxxxx.xxxx")
public Result<Product> queryProduct(@PathVariable("productId") Integer productId) {
return new Result<>(xxClient.queryProduct(productId));
}
- 針對(duì)該注解的切面(限流的核心)
@Component
@Aspect
@Slf4j
public class RateLimitAspect {
/**
* 針對(duì)含有該注解的地方進(jìn)行切入
*/
@Pointcut("@annotation(com.xxx.xxx.RateLimit)")
public void serviceLimit() {
}
@Around("serviceLimit()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//限流判斷,和限流處理
}
切面的大體框架有了,我們現(xiàn)在就剩具體的限流邏輯代碼
三、Guava RateLimiter
限流有很多算法,包括令牌桶、漏桶。根據(jù)需求,我們選擇的是令牌桶。
令牌桶:系統(tǒng)會(huì)以一個(gè)恒定的速度往桶里放入令牌,而如果請(qǐng)求需要被處理,則需要先從桶里獲取一個(gè)令牌,當(dāng)桶里沒有令牌可取時(shí),則拒絕服務(wù)。
而令牌桶的具體實(shí)現(xiàn),如果我們自己完成,就有些復(fù)雜了,所以這里我選擇了Guava依賴包里的RateLimiter。先看代碼吧
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>
@Autowired
private Environment env;
//用來存放不同接口的RateLimiter(key為接口名稱,value為RateLimiter)
private ConcurrentHashMap<String, RateLimiter> rateLimitMap = new ConcurrentHashMap<>();
@Around("serviceLimit()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object obj = null;
//獲取攔截的簽名
Signature sig = joinPoint.getSignature();
//獲取攔截的方法名
MethodSignature msig = (MethodSignature) sig;
//返回被織入增加處理目標(biāo)對(duì)象
Object target = joinPoint.getTarget();
//為了獲取注解信息
Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
//獲取注解信息
RateLimit annotation = currentMethod.getAnnotation(RateLimit.class);
//在application.properties中獲取注解每秒加入桶中的token
String limitNum = env.getProperty(annotation.limitKey());
// 注解所在方法名區(qū)分不同的限流策略
String functionName = msig.toShortString();
//如果值為0,則為不進(jìn)行限流
if (limitNum == null || limitNum.equals("0")) {
obj = joinPoint.proceed();
} else {
//獲取rateLimiter
if(!rateLimitMap.containsKey(functionName)){
//使用最簡(jiǎn)潔的方法來創(chuàng)建RateLimiter,RateLimiter.create(double xx),如果有需要,可自行設(shè)置RateLimiter其他屬性
rateLimitMap.put(functionName, RateLimiter.create(Double.parseDouble(limitNum)));
}
RateLimiter rateLimiter = rateLimitMap.get(functionName);
//嘗試獲得一個(gè)令牌
if (rateLimiter.tryAcquire(1)) {
//執(zhí)行方法
obj = joinPoint.proceed();
} else {
//拒絕了請(qǐng)求(服務(wù)降級(jí)),這是自己定義的異常類
throw new CommonException("拒絕了訪問open api方法的請(qǐng)求", FieldCodeEnum.OPEN_API, null, DetailCodeEnum.ACCESS, ErrorCodeEnum.OUT_OF_LIMIT);
}
}
return obj;
}
api.default.limit=5
四、完成
可以使用PostMan軟件發(fā)起壓力測(cè)試請(qǐng)求,比如1秒鐘發(fā)出去10個(gè),結(jié)果會(huì)是6個(gè)通行了,但后4個(gè)被拒絕了。
明明我們?cè)O(shè)置的限流值是5,為何6個(gè)通行了呢?見官方描述:
When the incoming request rate exceeds {@code permitsPerSecond} the
rate limiter will release one permit every {@code (1.0 / permitsPerSecond)} seconds.
這就是 RateLimiter 的預(yù)加載功能。
以上只是我搭建了一個(gè)限流的基礎(chǔ)框架,大家可以繼續(xù)完善它,以實(shí)現(xiàn)自己的需要。
歡迎大家一起交流相關(guān)知識(shí)