RateLimiter是guava提供的基於令牌桶算法的實現類,可以非常簡單的完成限流特技,並且根據系統的實際情況來調整生成token的速率。
通常可應用於搶購限流防止沖垮系統;限制某接口、服務單位時間內的訪問量,譬如一些第三方服務會對用戶訪問量進行限制;限制網速,單位時間內只允許上傳下載多少字節等。
guava的maven依賴
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>25.1-jre</version>
</dependency>
令牌桶的原理,有一個獨立線程一直以一個固定的速率往桶中存放令牌,客戶端去桶中獲取令牌,獲取到令牌,就可以訪問,獲取不到,說明請求過多,需要服務降級。
示例代碼:
(1)請求限流注解
/** * @創建人: hadoop * @創建時間: 2020/2/12 * @描述: */ @Target(value = {ElementType.METHOD,ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface LimitingAnnotation { /** * 獲取令牌超時時間 */ long timeOut() default 0L; /** * 限流速率,每秒最多產生令牌數 */ long produceRate() default 1000L; }
(2)請求限流切面
/** * @創建人: hadoop * @創建時間: 2020/2/12 * @描述: 服務限流降級切面,防止每秒請求數過多 */ @Component @Aspect @Slf4j public class LimitingAop { @Value("${request.limit}") private Integer limitValue ; @Autowired private HttpServletResponse response; // 令牌桶 // limitValue : 表示每秒中生成limitValue個令牌存放在桶中 @SuppressWarnings("UnstableApiUsage") private RateLimiter rateLimiter = RateLimiter.create(limitValue); /** * 限流切點 */ @Pointcut("@annotation(com.service.bussiness.aop.LimitingAnnotation)") public void limitingPointCut() { } @SuppressWarnings({"rawtypes", "UnstableApiUsage"}) @Around("limitingPointCut()") public Object controllerAround( ProceedingJoinPoint point ) { String description = CommonConst.EMPTYSTRING; Method method = null; String methodName = point.getSignature().getName(); Class[] paramTypes = ((MethodSignature) point.getSignature()).getParameterTypes(); try { method = point.getTarget().getClass().getMethod(methodName, paramTypes); if ( !method.isAnnotationPresent(LimitingAnnotation.class) ) { return null; } } catch ( NoSuchMethodException e ) { log.error("限流切面出現異常,異常原因是: " + e.getMessage()); throw new CustomException( Integer.parseInt(CustomExceptionType.SYSTEM_ERROR.getCode()) , e.getMessage(), "限流切面出現異常", "請求的目標方法不存在" ); } LimitingAnnotation limitingAnnotation = method.getAnnotation(LimitingAnnotation.class); if ( null == limitingAnnotation ){ return null; } long timeOut = limitingAnnotation.timeOut(); long produceRate = limitingAnnotation.produceRate(); // 設置限流速率,每秒產生多少令牌 rateLimiter.setRate(produceRate); // 每次發送請求,在設定ms內沒有獲取到令牌,則對服務進行降級處理 boolean acquire = rateLimiter.tryAcquire(timeOut, TimeUnit.MILLISECONDS); if ( !acquire ){ getErrorMsg(); return null; } try { return point.proceed(); } catch (Throwable throwable) { log.error(methodName+"請求出現異常,異常原因是: " + throwable.getMessage()); throwable.printStackTrace(); } return null; } /** * 向客戶端輸出服務降級信息 * */ public void getErrorMsg(){ response.setHeader("Content-Type","application/json;charset=UTF-8"); PrintWriter printWriter = null; try { printWriter = response.getWriter(); Map<String,Object> resultMap = new HashMap<>(); resultMap.put("msg","前方任務過多,請稍后再試"); printWriter.write(JSON.toJSONString(resultMap)); printWriter.flush(); } catch (IOException e) { e.printStackTrace(); } } }
(3) 請求服務
@RequestMapping(value = "/search", method = RequestMethod.POST, consumes = "application/json") @ResponseBody @LimitingAnnotation( timeOut = 500, produceRate = 1000 ) public ResponseEntity<String> search( @RequestBody String param )
{return this.searchService.searchBillInfo( xEncryption , params ); }
參考: