當前位置:
首頁 >
前端技术
> javascript
>内容正文
javascript
SpringBoot - 优雅的实现【流控】
生活随笔
收集整理的這篇文章主要介紹了
SpringBoot - 优雅的实现【流控】
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
文章目錄
- 概述
- 限流算法
- 計數器限流
- 漏桶算法
- 令牌桶算法
- V1.0
- V2.0 自定義注解+AOP實現接口限流
- 搞依賴
- 搞自定義限流注解
- 搞AOP
- 用上驗證
- 源碼
概述
限流 簡言之就是當請求達到一定的并發數或速率,就對服務進行等待、排隊、降級、拒絕服務等操作。
限流算法
我們先簡單捋一捋限流算法
并發編程-25 高并發處理手段之消息隊列思路 + 應用拆分思路 + 應用限流思路
深入理解分布式技術 - 限流
計數器限流
漏桶算法
把水比作是請求,漏桶比作是系統處理能力極限,水先進入到漏桶里,漏桶里的水按一定速率流出,當流出的速率小于流入的速率時,由于漏桶容量有限,后續進入的水直接溢出(拒絕請求),以此實現限流
令牌桶算法
可以簡單地理解為醫去銀行辦理業務,只有拿到號以后才可以進行業務辦理。
系統會維護一個令牌(token)桶,以一個恒定的速度往桶里放入令牌(token),這時如果有請求進來想要被處理,則需要先從桶里獲取一個令牌(token),當桶里沒有令牌(token)可取時,則該請求將被拒絕服務。令牌桶算法通過控制桶的容量、發放令牌的速率,來達到對請求的限制。
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個請求*/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毫秒內,沒拿到令牌,就直接進入服務降級boolean tryAcquire = limiter.tryAcquire(500, TimeUnit.MILLISECONDS);if (!tryAcquire) {log.warn("BOOM 服務降級,時間{}", LocalDateTime.now().format(dtf));return "系統繁忙,請稍后再試!";}log.info("獲取令牌成功,時間{}", LocalDateTime.now().format(dtf));return "業務處理成功";}我們可以看到RateLimiter的2個核心方法:create()、tryAcquire()
- acquire() 獲取一個令牌, 改方法會阻塞直到獲取到這一個令牌, 返回值為獲取到這個令牌花費的時間
- acquire(int permits) 獲取指定數量的令牌, 該方法也會阻塞, 返回值為獲取到這 N 個令牌花費的時間
- tryAcquire() 判斷時候能獲取到令牌, 如果不能獲取立即返回 false
- tryAcquire(int permits) 獲取指定數量的令牌, 如果不能獲取立即返回 false
- tryAcquire(long timeout, TimeUnit unit) 判斷能否在指定時間內獲取到令牌, 如果不能獲取立即返回 false
- tryAcquire(int permits, long timeout, TimeUnit unit) 同上
測試一下
V2.0 自定義注解+AOP實現接口限流
1.0的功能實現了,但是業務代碼和限流代碼混在一起,非常的不美觀。
搞依賴
<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 "";/*** 最多的訪問限制次數*/double permitsPerSecond();/*** 獲取令牌最大等待時間*/long timeout();/*** 獲取令牌最大等待時間,單位(例:分鐘/秒/毫秒) 默認:毫秒*/TimeUnit timeunit() default TimeUnit.MILLISECONDS;/*** 得不到令牌的提示語*/String message() default "系統繁忙,請稍后再試."; }搞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;//驗證緩存是否有命中keyif (!limitMap.containsKey(key)) {// 創建令牌桶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);}}用上驗證
@GetMapping("/test2")@ArtisanLimit(key = "testLimit2", permitsPerSecond = 1, timeout = 500, timeunit = TimeUnit.MILLISECONDS, message = "test2 當前排隊人數較多,請稍后再試!")public String test2() {log.info("令牌桶test2獲取令牌成功");return "test2 ok";}源碼
https://github.com/yangshangwei/boot2
總結
以上是生活随笔為你收集整理的SpringBoot - 优雅的实现【流控】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SpringBoot - 优雅的实现【业
- 下一篇: SpringBoot - 优雅的实现【应