系統在設計的時候,我們會有一個系統的預估容量,長時間超過系統能承受的TPS/QPS閾值,系統有可能會被壓垮,最終導致整個服務不可用。為了避免這種情況,我們需要對介面請求進行限流。
所以,我們可以透過對並發存取請求進行限速或一個時間窗口內的請求數量進行限速來保護系統或避免不必要的資源浪費,一旦達到限制速率則可以拒絕服務、排隊或等待。
系統有一個獲取手機簡訊驗證碼的接口,因為是開放接口,所以為了避免用戶不斷的發送請求取得驗證碼,防止惡意刷介面的情況發生,於是用最簡單的計數器方式做了限流,限制每個IP每分鐘只能請求一次,然後其他每個手機號碼的時間窗口限制則是透過業務邏輯進行判斷。一般一些介面訪問量比較大的,可能會壓垮系統的,則需要加入流量限制!如:秒殺等...
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RateLimiter { /** * 限流key */ String key() default Constants.RATE_LIMIT_KEY; /** * 限流时间,单位秒 */ int time() default 60; /** * 限流次数 */ int count() default 100; /** * 限流类型 */ LimitType limitType() default LimitType.DEFAULT; /** * 限流后返回的文字 */ String limitMsg() default "访问过于频繁,请稍候再试"; }
@Aspect @Component public class RateLimiterAspect { private final static Logger log = LoggerFactory.getLogger(RateLimiterAspect.class); @Autowired private RedisUtils redisUtils; @Before("@annotation(rateLimiter)") public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable { int time = rateLimiter.time(); int count = rateLimiter.count(); long total = 1L; String combineKey = getCombineKey(rateLimiter, point); try { if(redisUtils.hasKey(combineKey)){ total = redisUtils.incr(combineKey,1); //请求进来,对应的key加1 if(total > count) throw new ServiceRuntimeException(rateLimiter.limitMsg()); }else{ redisUtils.set(combineKey,1,time); //初始化key } } catch (ServiceRuntimeException e) { throw e; } catch (Exception e) { throw new ServiceRuntimeException("网络繁忙,请稍候再试"); } } /** * 获取限流key * @param rateLimiter * @param point * @return */ public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) { StringBuffer stringBuffer = new StringBuffer(rateLimiter.key()); if (rateLimiter.limitType() == LimitType.IP) { stringBuffer.append(IpUtils.getIpAddr(ServletUtils.getRequest())).append("-"); } MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); Class<?> targetClass = method.getDeclaringClass(); stringBuffer.append(targetClass.getName()).append("-").append(method.getName()); return stringBuffer.toString(); } }
@RestController public class TestController { @RateLimiter(time = 60, count = 1, limitType = LimitType.IP, limitMsg = "一分钟内只能请求一次,请稍后重试") @GetMapping("/hello") public ResultMsg hello() { return ResultMsg.success("Hello World!"); } }
@RestControllerAdvice public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); /** * 业务异常 */ @ExceptionHandler(ServiceRuntimeException.class) public ResultMsg handleServiceException(ServiceRuntimeException e, HttpServletRequest request) { return ResultMsg.error(e.getMessage()); } /** * 系统异常 */ @ExceptionHandler(Exception.class) public ResultMsg handleException(Exception e, HttpServletRequest request) { return ResultMsg.error("系统异常"); } }
1)第一次發送,正常返回結果
2)一分鐘內第二次發送,返回錯誤,限流提示
以上,就是AOP Redis
的方案實作介面限流,你學廢了嗎?
還有其他的限流方式,如滑動視窗限流方式(比計數器更嚴謹)、令牌桶等...,有興趣的小夥伴可以學習一下。
以上是接口限流,騷操作全在這裡了!的詳細內容。更多資訊請關注PHP中文網其他相關文章!