@[toc]
## 為什麼要接口限流
- 在我們項目開發過程中,有些接口是暴露在使用者的常用中,包括一些高危接口,如 (支付,開發票,訂單),這些接口 都是高危接口,且被使用者經常使用,在高并發的情況下,io阻塞,不可避免的出現重複送出,或者點選頻繁的操作,是以我們就要加入限流,避免使用者多次點選,減少我們接口的壓力,把整資料不會重複,接口壓力減小
為什麼要做分布式
-
在我們做項目負載均衡的時候, 分布式,微服務架構的時候,不可避免的多個節點,這個時候我們就要考慮會被随機配置設定到各個節點,如果 我們使用 令牌桶 或者 漏鬥桶 算法到話,存到 本地,各個節點不會共享,是以
我們要考慮子產品,節點間的共享
實作方式
1. 算法實作(無分布式,單體架構,單節點)
- 自定義注解
package com.yxl.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* <p>
* 限流注解,
* </p>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
int NOT_LIMITED = 0;
/**
* qps
*/
@AliasFor("qps") double value() default NOT_LIMITED;
/**
* qps
*/
@AliasFor("value") double qps() default NOT_LIMITED;
/**
* 逾時時長
*/
int timeout() default 0;
/**
* 逾時時間機關
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
- AOP實作切面 + 令牌桶算法實作
package com.yxl.aspect;
import com.yxl.annotation.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.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* <p>
* 限流切面
* </p>
*
* @author yxl
* @date Created in 2019/9/12 14:27
*/
@Slf4j
@Aspect
@Component
public class RateLimiterAspect {
private static final ConcurrentMap<String, com.google.common.util.concurrent.RateLimiter> RATE_LIMITER_CACHE = new ConcurrentHashMap<>();
@Pointcut("@annotation(com.yxl.annotation.RateLimiter)")
public void rateLimit() {
}
@Around("rateLimit()")
public Object pointcut(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
// 通過 AnnotationUtils.findAnnotation 擷取 RateLimiter 注解
RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class);
if (rateLimiter != null && rateLimiter.qps() > RateLimiter.NOT_LIMITED) {
double qps = rateLimiter.qps();
if (RATE_LIMITER_CACHE.get(method.getName()) == null) {
// 初始化 QPS
RATE_LIMITER_CACHE.put(method.getName(), com.google.common.util.concurrent.RateLimiter.create(qps));
}
log.debug("【{}】的QPS設定為: {}", method.getName(), RATE_LIMITER_CACHE.get(method.getName()).getRate());
// 嘗試擷取令牌
if (RATE_LIMITER_CACHE.get(method.getName()) != null && !RATE_LIMITER_CACHE.get(method.getName()).tryAcquire(rateLimiter.timeout(), rateLimiter.timeUnit())) {
throw new RuntimeException("手速太快了,慢點兒吧~");
}
}
return point.proceed();
}
}
使用方式

檢視結果(這裡使用了自定義異常)
2. 分布式實作
package com.yxzapp.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* <p>
* 限流注解,
* </p>
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
int NOT_LIMITED = 0;
/**
* 類名
* @return
*/
String className() default "";
/**
* qps
*/
@AliasFor("qps") double value() default NOT_LIMITED;
/**
* qps
*/
@AliasFor("value") double qps() default NOT_LIMITED;
/**
* 限流時間
*/
int timeout() default 0;
/**
* 逾時時間機關
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
使用 AOP + redis 實作
package com.yxzapp.aspect;
import com.yxzapp.annotation.RateLimiter;
import com.yxzapp.commons.constant.MessageConstant;
import com.yxzapp.exception.BizException;
import com.yxzapp.modules.sys.entity.SysUser;
import com.yxzapp.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* <p>
* 限流切面
* </p>
*
* @author yxl
* @date 2020/6/19
*/
@Slf4j
@Aspect
@Component
public class RateLimiterAspect {
@Autowired
private RedisUtils redisUtils;
@Pointcut("@annotation(com.yxzapp.annotation.RateLimiter)")
public void rateLimit() {
}
@Around("rateLimit()")
public Object pointcut(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Class aClass = signature.getClass();
// 擷取方法上的@RateLimiter注解
RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class);
if (rateLimiter != null && rateLimiter.qps() > RateLimiter.NOT_LIMITED) {
//擷取qps
double qps = rateLimiter.qps();
String key = "RateLimiter:" rateLimiter.className() + +':'+ method.getName();
if(!redisUtils.hasKey(key)){
redisUtils.setMillisecond(key,rateLimiter.qps(),rateLimiter.timeout());
}else if(redisUtils.get(key) != null) {
throw new BizException(MessageConstant.MSG_STATUS,"手速太快了,慢點兒吧~");
}
log.debug("【{}】的QPS設定為: {}", key, redisUtils.get(key));
}
return point.proceed();
}
}
檢視結果 (這裡使用了自定義異常)