guava rate Limite原理参考:https://www.cnblogs.com/fnlingnzb-learner/p/13086185.html
guava的限流器,可以理解为并发包中的信号量。通过tryAcquire方式获取有限的令牌(即你要限制的qps),获取到就可以进入Controller中执行。但是只适用于单机版本的限流器,对于集群限流器只能使用redis去实现。
上述是在进入方法前做限流拦截,因此结合拦截器去做限流是最合适的。
关于限流配置:
将预先设置的限流策略配置在DynamicConfig(放置在配置文件中,服务启动时会解析配置文件获取到限流策略,做的更好的是一种动态配置。提供refresh接口,服务器上放一个root权限才能写的配置文件,root权限是为了业务安全。root用户修改后调用refresh接口将新策略刷入该动态配置)
@Slf4j
public class RateLimitInterceptor extends HandlerInterceptorAdapter {
// 为了避免反复生成限流器对象,https://juejin.cn/post/6844904057128091655 并发包中的map防止并发更新失败
private Map<String, RateLimiter> limiterMap = new ConcurrentHashMap<>();
private static final int ACQUIRE_WAIT_TIME = 100;
@Autowired
private ResponseUtil responseUtil;
@Resource
private RedisTemplate<String, String> redisClient;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//每条限流规则是一个item。
for (RateLimitRule item : DynamicConfig.limitRule) {
//获取单条限流规则中所有限流维度
Set<String> limitDimension = item.getLimitDimension();
//提取这次请求中限流纬度特征,拼成本次请求的特征key
StringBuffer sb = new StringBuffer();
if (limitDimension.contains(“sysetemID”)) {
sb.append("systemId=");
sb.append(ThreadInfoCache.getSystemId());
}
if (limitDimension.contains(entityNameKey)) {
sb.append("entityName=");
String entityName=StringUtils.isEmpty(request.getParameter("entityName"))
?"unknow":request.getParameter("entityName");
sb.append(entityName);
}
if (limitDimension.contains("method")) {
sb.append("method=");
sb.append(ThreadLocalCache.get(OpenApiContextConstant.Method));
}
if(StringUtils.isEmpty(sb.toString())){
continue;
}
if(!tryLimit(sb.toString(),item.getLimitQps(),item.getRuleType(),item.getCustomKey())){
responseUtil.noAuthResponse(response, ResponeCode.OVER_LIMIT_ERROR,ResponeCode.OVER_LIMIT_ERROR);
return false;
}
}
return true;
}
private boolean tryLimit(String requestLimitKey,int confLimitQps,int confLimitType,String confCustomRuleLimitKey){
if(LimitTypeEnum.LOCAL_LIMIT.code==confLimitType){
return tryLocalLimit(requestLimitKey+confLimitQps, confLimitQps);
}
return tryAllLimit(requestLimitKey, confLimitQps);
}
//本地限流根据特征key和对应qps缓存一个guavaLimit在map中,map中的key是特征key加qps,这样缓存规则变了之后,就会产生一个新的guavaLimit。
private boolean tryLocalLimit(String key,int qps){
// guava的限流器限制
RateLimiter limitRate = getLimitRate(key, qps);
return limitRate.tryAcquire(ACQUIRE_WAIT_TIME, TimeUnit.MILLISECONDS);
}
//集群限流使用redis
private boolean tryAllLimit(String key,int qps){
String limitKey = key + System.currentTimeMillis() / 1000;
//limitKey=key;
Long current = redisClient.opsForValue().increment(limitKey,1);
//为了避免单次设置超时时间可能出现的失败,造成死key,同时也为了避免频繁更新redis,默认设置三次重试确保成功
if(current<4){
// https://www.runoob.com/redis/keys-expire.html
// expire刷新过期时间的命令
redisClient.expire(limitKey,120,TimeUnit.SECONDS);
}
return current<qps;
}
private RateLimiter getLimitRate(String key,int qps) {
if (limiterMap.containsKey(key)) {
return limiterMap.get(key);
}
RateLimiter rateLimiter = RateLimiter.create(qps);
limiterMap.putIfAbsent(key, rateLimiter);
return limiterMap.get(key);
}
@Override
public void afterCompletion(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
}
}
限流器配置
[{
"limitDimension": ["systemId", "method", "entityName", "tenant"],
"limitQps": 30,
"ruleType": 2
}, {
"customKey": "systemId=aIDmethod=search",
"limitDimension": ["systemId", "method"],
"limitQps": 2,
"ruleType": 2
}]
第一条规则代表:全局限流,对于每个systemId的每个method的每个entityName的每个tenant限制qps为30。
第二条规则代表:全局限流,对于systemId为A的租户且请求方法为search的所有请求限制qps为30。