本文從限流背景開始,介紹了限流的常用方法、代碼實現和限流組件源碼分析。本文是該系列的第一篇,介紹限流背景,限流算法和RateLimiter限流實現。第二篇會介紹RateLimiter的源碼實現。
一、限流背景
限流是保護系統的重要利器,通過對並發訪問或請求數進行限制或者對一個時間窗口內的請求數進行限速,用於防止大流量或突發流量導致服務崩潰。一旦達到限制速率則可以拒絕服務或進行流量整形。
在實踐中會在網絡層,接入層(Nginx),應用層進行限流。本文介紹的是應用層的限流方式,對於其它層原理類似,分層或使用的技術手段不同而已。
一般的大流量場景如秒殺,搶購系統,大並發電商系統等,實現技術可以限制線程池,數據庫連接池,瞬間並發數,接口調用速率、限制MQ消費速率。根據網絡連接數、網絡流量、CPU或內存負載等來限流等。
二、限流算法
限流常用的基本算法有兩種,漏桶算法和令牌桶算法。

如上圖就像一個漏斗一樣,進來的水量就像訪問流量一樣,出去的水量像系統處理請求一樣。當訪問流量過大時,漏斗中就會積水,如果水太多了就會溢出。
2.1 漏桶算法
漏桶算法一般有隊列(Queue)和處理器兩個組件構成,隊列用於存放請求,處理器復雜處理請求。
(1)請求達到如果未滿,則放入隊列,之后有處理器按照固定速率取出請求進行處理。
(2)如果請求量過大超出了最大限制,則新來的請求會被拋棄。

漏桶算法示意圖
2.2 令牌桶算法
令牌桶算法與漏桶算法組件構成結構類似,區別在於令牌桶會先發放令牌,如果有令牌,則繼續處理,無則拋棄請求。
(1)往桶內按照固定速率,添加固定容量的令牌。令牌數量達到最大限制,則會丟棄或拒絕。
(2)當請求到達時,如果獲取到令牌則直接處理,如果獲取不到,則會丟棄或放到緩存區。

令牌桶算法示意圖
2.3令牌桶算法和漏桶算法對比
(1)令牌桶算法是按照固定速率往桶中添加令牌,請求是否被處理需要看桶中令牌是否足夠,當令牌數減為零時則拒絕新的請求;漏桶則是按照常量固定速率流出請求,流入請求速率任意,當流入的請求數累積到漏桶容量時,則新流入的請求被拒絕;
(2)令牌桶算法限制的是平均流入速率,允許突發請求,只要有令牌就可以處理,支持一次拿多個令牌,比如10個或15個;漏桶算法限制的是常量流出速率,即流出速率是一個固定常量,比如速率流出1,則一直都是1,而不能一次是1,下次是2,從而可以平滑突發流入速率;
(3)令牌桶算法允許一定程度的突發流量,而漏桶算法可以平滑流出速率;
三、限流實現
Guava的RateLimiter提供了令牌桶算法實現:平滑突發限流(SmoothBursty)和平滑預熱限流(SmoothWarmingUp)實現。我們將通過案例介紹RateLimiter的使用。
3.1 maven引用
1 <dependency> 2 <groupId>com.google.guava</groupId> 3 <artifactId>guava</artifactId> 4 <version>28.1-jre</version> 5 </dependency>
3.2 定義注解
1 @Target({ElementType.PARAMETER, ElementType.METHOD}) 2 @Retention(RetentionPolicy.RUNTIME) 3 @Documented 4 public @interface RequestRateLimitAnnotation { 5 /** 6 * 固定令牌個數 7 * @return 8 */ 9 double limitNum(); 10 /** 11 * 獲取令牌超時時間 12 * @return 13 */ 14 long timeout(); 15 /** 16 * 單位-默認毫秒 17 * @return 18 */ 19 TimeUnit timeUnit() default TimeUnit.MILLISECONDS; 20 /** 21 * 無法獲取令牌時的錯誤信息 22 * @return 23 */ 24 String errMsg() default "請求太頻繁!"; 25 }
3.3 AOP攔截
1 @Slf4j 2 @Aspect 3 @Component 4 public class RequestRateLimitAspect { 8 9 /** 10 * 使用url做為key,存放令牌桶 防止每次重新創建令牌桶 11 */ 12 private Map<String, RateLimiter> limitMap = Maps.newConcurrentMap(); 13 14 @Pointcut("@annotation(com.itfly8.test.annotation.RequestRateLimitAnnotation)") 15 public void execMethod() { 16 17 } 18 19 @Around("execMethod()") 20 public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { 21 //獲取請求uri 22 HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); 23 String reqUrl=request.getRequestURI(); 24 25 // 獲取令牌 26 RequestRateLimitAnnotation rateLimiter = this.getRequestRateLimiter(joinPoint); 27 if (Objects.nonNull(rateLimiter)) { 28 RateLimiter limiter = getRateLimiter(reqUrl, rateLimiter); 29 boolean acquire = limiter.tryAcquire(rateLimiter.timeout(), rateLimiter.timeUnit()); 30 if (!acquire) { 31 return new Response<>("200", reqUrl.concat(rateLimiter.errMsg())); 32 } 33 } 34 35 //獲得令牌,繼續執行 36 return joinPoint.proceed(); 37 } 38 39 40 /** 41 * 獲取RateLimiter 42 * 43 * @return 44 */ 45 private RateLimiter getRateLimiter(String reqUrl, RequestRateLimitAnnotation rateLimiter) { 46 RateLimiter limiter = limitMap.get(reqUrl); 47 if (Objects.isNull(limiter)) { 48 synchronized (this) { 49 limiter = limitMap.get(reqUrl); 50 if (Objects.isNull(limiter)) { 51 limiter = RateLimiter.create(rateLimiter.limitNum()); 52 limitMap.put(reqUrl, limiter); 53 log.info("RequestRateLimitAspect請求{},創建令牌桶,容量{} 成功!!!", reqUrl, rateLimiter.limitNum()); 54 } 55 } 56 } 57 58 return limiter; 59 } 60 61 /** 62 * 獲取注解對象 63 * @param joinPoint 對象 64 * @return ten LogAnnotation 65 */ 66 private RequestRateLimitAnnotation getRequestRateLimiter(final JoinPoint joinPoint) { 67 Method[] methods = joinPoint.getTarget().getClass().getDeclaredMethods(); 68 String name = joinPoint.getSignature().getName(); 69 if (!StringUtils.isEmpty(name)) { 70 for (Method method : methods) { 71 RequestRateLimitAnnotation annotation = method.getAnnotation(RequestRateLimitAnnotation.class); 72 if (!Objects.isNull(annotation) && name.equals(method.getName())) { 73 return annotation; 74 } 75 } 76 } 77 return null; 78 } 79 }
3.4 Controller應用
1 @RestController 2 @RequestMapping("/test/") 3 public class ExampleController { 4 @RequestRateLimitAnnotation(limitNum = 2,timeout = 10) 5 @PostMapping("/ratelimit") 6 public String testRateLimit() { 7 /** 8 * 測試代碼 9 */ 10 return "success"; 11 }
3.5 執行效果
如果超過最大限流,則打印日志如下:
1 com.itlfy8.test.controller.TestController result:{"code":"200","msg":"/test/ratelimit請求太頻繁!","data":{}}
四、總結
本文從限流場景、限流算法和RateLimit的使用三個方面,介紹了高並發限流的方法,代碼提供了一種較為通用的實現方式,在此基礎上擴展可以實現比較通用的限流機制。
