1. 令牌桶限流算法
令牌桶會以一個恆定的速率向固定容量大小桶中放入令牌,當有瀏覽來時取走一個或者多個令牌,當發生高並發情況下拿到令牌的執行業務邏輯,沒有獲取到令牌的就會丟棄獲取服務降級處理,提示一個友好的錯誤信息給用戶。
2. RateLimiter簡單實現
maven依賴
<!-- guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
本人使用的是SpringBoot 2.0.4.RELEASE,Jdk1.8環境下編寫,部分代碼貼出:
存儲的最大令牌數maxPermits = maxBurstSeconds * permitsPerSecond;使用RateLimiter.create 創建時,maxBurstSeconds = 1,也就是maxPermits = permitsPerSecond
/** * 以1r/s往桶中放入令牌 */
private RateLimiter limiter = RateLimiter.create(1.0);
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@GetMapping("/indexLimiter")
public String indexLimiter() {
// 如果用戶在500毫秒內沒有獲取到令牌,就直接放棄獲取進行服務降級處理
boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS);
if (!tryAcquire) {
log.info("Error ---時間:{},獲取令牌失敗.", sdf.format(new Date()));
return "系統繁忙,請稍后再試.";
}
log.info("Success ---時間:{},獲取令牌成功.", sdf.format(new Date()));
return "success";
}
調用結果如下:
使用RateLimiter注意的地方:
允許先消費,后付款,意思就是它可以來一個請求的時候一次性取走幾個或者是剩下所有的令牌甚至多取,但是后面的請求就得為上一次請求買單,它需要等待桶中的令牌補齊之后才能繼續獲取令牌。
3.自定義注解實現基於接口限流
仔細看會發現上面的簡單實現會造成我每個接口都要寫一次限流方法代碼很冗余,所以采用aop來使用自定義注解來實現。
maven依賴
<!-- aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- google guava 依賴 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
<!-- lombok 簡化java代碼-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
首先定義一個自定義注解:
package com.limiting.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface AnRateLimiter {
//以固定數值往令牌桶添加令牌
double permitsPerSecond () ;
//獲取令牌最大等待時間
long timeout();
// 單位(例:分鍾/秒/毫秒) 默認:毫秒
TimeUnit timeunit() default TimeUnit.MILLISECONDS;
// 無法獲取令牌返回提示信息 默認值可以自行修改
String msg() default "系統繁忙,請稍后再試.";
}
然后使用aop的環繞通知來攔截注解,使用了一個ConcurrentMap來保存每個請求對應的令牌桶,key是沒有url請求,防止出現每個請求都會新建一個令牌桶這么會達不到限流效果.
package com.limiting.aspect;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.RateLimiter;
import com.limiting.annotation.AnRateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
/** * * 描述: * * @author 只寫BUG的攻城獅 * * @date 2018-09-12 12:07 */
@Slf4j
@Aspect
@Component
public class RateLimiterAspect {
/** * 使用url做為key,存放令牌桶 防止每次重新創建令牌桶 */
private Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();
@Pointcut("@annotation(com.limiting.annotation.AnRateLimiter)")
public void anRateLimiter() {
}
@Around("anRateLimiter()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 獲取request,response
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
// 或者url(存在map集合的key)
String url = request.getRequestURI();
// 獲取自定義注解
AnRateLimiter rateLimiter = getAnRateLimiter(joinPoint);
if (rateLimiter != null) {
RateLimiter limiter = null;
// 判斷map集合中是否有創建有創建好的令牌桶
if (!limitMap.containsKey(url)) {
// 創建令牌桶
limiter = RateLimiter.create(rateLimiter.permitsPerSecond());
limitMap.put(url, limiter);
log.info("<<================= 請求{},創建令牌桶,容量{} 成功!!!", url, rateLimiter.permitsPerSecond());
}
limiter = limitMap.get(url);
// 獲取令牌
boolean acquire = limiter.tryAcquire(rateLimiter.timeout(), rateLimiter.timeunit());
if (!acquire) {
responseResult(response, 500, rateLimiter.msg());
return null;
}
}
return joinPoint.proceed();
}
/** * 獲取注解對象 * @param joinPoint 對象 * @return ten LogAnnotation */
private AnRateLimiter getAnRateLimiter(final JoinPoint joinPoint) {
Method[] methods = joinPoint.getTarget().getClass().getDeclaredMethods();
String name = joinPoint.getSignature().getName();
if (!StringUtils.isEmpty(name)) {
for (Method method : methods) {
AnRateLimiter annotation = method.getAnnotation(AnRateLimiter.class);
if (!Objects.isNull(annotation) && name.equals(method.getName())) {
return annotation;
}
}
}
return null;
}
/** * 自定義響應結果 * * @param response 響應 * @param code 響應碼 * @param message 響應信息 */
private void responseResult(HttpServletResponse response, Integer code, String message) {
response.resetBuffer();
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.println("{\"code\":" + code + " ,\"message\" :\"" + message + "\"}");
response.flushBuffer();
} catch (IOException e) {
log.error(" 輸入響應出錯 e = {}", e.getMessage(), e);
} finally {
if (writer != null) {
writer.flush();
writer.close();
}
}
}
}
最后來試試自己定義的注解是否生效,能否達到限流效果.
@GetMapping("/index")
@AnRateLimiter(permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS,msg = "親,現在流量過大,請稍后再試.")
public String index() {
return System.currentTimeMillis() + "";
}
訪問請求(按F5狂刷新瀏覽器)效果如下圖:
總結
至此已基本上使用注解實現了接口限流,后期可以根據自己需求自行修改,這個只適於單個應用進行接口限流,如果是分布式項目或者微服務項目可以采用redis來實現,后期有時間來一個基於redis自定義注解來實現接口限流。
本人也是剛入Java開發行業沒多久的小菜鳥,在文章中可能存在一些說的不對,代碼不嚴謹的地方歡迎各位大神指出,本人表示由衷的感謝和耐心的學習,希望能在開發中給大家一些幫助。