圖片來自網上java
令牌桶會以一個恆定的速率向固定容量大小桶中放入令牌,當有瀏覽來時取走一個或者多個令牌,當發生高併發狀況下拿到令牌的執行業務邏輯,沒有獲取到令牌的就會丟棄獲取服務降級處理,提示一個友好的錯誤信息給用戶。web
2. RateLimiter簡單實現
maven依賴redis
<!-- guava --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> </dependency>
本人使用的是SpringBoot 2.0.4.RELEASE,Jdk1.8環境下編寫,部分代碼貼出:算法
/** * 以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"; }
調用結果以下:spring
使用RateLimiter注意的地方:json
容許先消費,後付款,意思就是它能夠來一個請求的時候一次性取走幾個或者是剩下全部的令牌甚至多取,可是後面的請求就得爲上一次請求買單,它須要等待桶中的令牌補齊以後才能繼續獲取令牌。瀏覽器
3.自定義註解實現基於接口限流併發
仔細看會發現上面的簡單實現會形成我每一個接口都要寫一次限流方法代碼很冗餘,因此採用aop來使用自定義註解來實現。app
maven依賴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開發行業沒多久的小菜鳥,在文章中可能存在一些說的不對,代碼不嚴謹的地方歡迎各位大神指出,本人表示由衷的感謝和耐心的學習,但願能在開發中給你們一些幫助。