首先维护一个计数器,将单位时间段当做一个窗口,计数器记录这个窗口接收请求的次数。
滑动窗口也是维护单位时间内的请求次数,其与固定窗口限流算法的区别是,滑动窗口的粒度更细,将一个大的时间窗口划分为若干个小的时间窗口,分别记录每个小周期内接口的访问次数,通过滑动时间删除小的时间窗口,以此来解决固定窗口临界值的问题
原理很简单,可以认为就是注水漏水的过程。往漏桶中以任意速率流入水,以固定的速率流出水。当水超过桶的容量时,会被溢出,也就是被丢弃。因为桶容量是不变的,保证了整体的速率。
令牌桶算法每隔一段时间就将一定量的令牌放入桶中,获取到令牌的请求直接访问后段的服务,没有获取到令牌的请求会被拒绝。同时令牌桶有一定的容量,当桶中的令牌数达到最大值后,不再放入令牌。
限流又分为单机限流和分布式限流,常见的分布式限流方案如下
● 可以基于redis,做分布式限流
● 可以基于nginx做分布式限流
● 可以使用阿里开源的 sentinel 中间件
1、希望单个接口限流的数量可以实时控制,引入Nacos用于配置接口限流数
2、根据自身项目需求,使用用户id与接口名作为key,使redisTemplate.opsForValue().increment方法作为vaule,从而实现次数的递增,然后设置缓存过期时间来实现固定时间窗口
3、希望对业务没有耦合,使用拦截器拦截固定请求,限流算法实现写在拦截器中
固定窗口限流算法实现:
在Redis中根据该用户id和接口名创建一个键,并设置这个键的过期时间,当用户请求到来的时候,先去redis中根据用户ip获取这个用户当前分钟请求了多少次,如果获取不到,则说明这个用户当前分钟第一次访问,就创建这个健,并+1,如果获取到了就判断当前有没有超过我们限制的次数,如果到了我们限制的次数则禁止访问。
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Autowired
private LimitInterceptor limitInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// addPathPatterns 用于添加拦截规则,/**表示拦截所有请求
// excludePathPatterns 用户排除拦截
registry.addInterceptor(limitInterceptor).addPathPatterns("/**");
}
@Bean
public LimitInterceptor initLimitInterceptorBean() {
return new LimitInterceptor();
}
}
@Slf4j
public class LimitInterceptor implements HandlerInterceptor {
@Autowired(required = false)
private LimitService limitService;
@Autowired
private ApplicationContext applicationContext;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (limitService == null) {
log.error("===>limitService Bean不存在");
return true;
}
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequestMapping requestMapping = handlerMethod.getMethodAnnotation(RequestMapping.class);
if (requestMapping != null && requestMapping.value() != null) {
Object userIdObj = request.getParameter("userId");
Long userId;
if (userIdObj == null) {
//对象传值
userIdObj = request.getAttribute("userId");
}
if (userIdObj == null) {
return true;
}
try {
userId = Long.valueOf(userIdObj.toString());
} catch (NumberFormatException e) {
return true;
}
String urlPath = getHandlerMethodMapperingInfo(handlerMethod);
if (StringUtils.isBlank(urlPath)) {
return true;
}
urlPath = trimUrlPathDupliLine(urlPath);
limitService.limitCheck(userId, urlPath);
}
}
return true;
}
public String getHandlerMethodMapperingInfo(HandlerMethod handlerMethodParam) {
Map methodNameUrlPathMap = new HashMap<>();
RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
Map handlerMethods = mapping.getHandlerMethods();
for (RequestMappingInfo requestMappingInfo : handlerMethods.keySet()) {
Set patternsSet = requestMappingInfo.getPatternsCondition().getPatterns();
HandlerMethod handlerMethod = handlerMethods.get(requestMappingInfo);
for (String urlPath : patternsSet) {
methodNameUrlPathMap.put(initDescription(handlerMethod.getBeanType(), handlerMethod.getMethod()), urlPath);
}
}
return methodNameUrlPathMap.get(initDescription(handlerMethodParam.getBeanType(), handlerMethodParam.getMethod()));
}
//class+method()
private static String initDescription(Class> beanType, Method method) {
StringJoiner joiner = new StringJoiner(", ", "(", ")");
Class[] var3 = method.getParameterTypes();
int var4 = var3.length;
for (int var5 = 0; var5 < var4; ++var5) {
Class> paramType = var3[var5];
joiner.add(paramType.getSimpleName());
}
return beanType.getName() + "#" + method.getName() + joiner.toString();
}
/**
* 多个/正则替换成/
*
* @param urlPath
* @return
*/
private static String trimUrlPathDupliLine(String urlPath) {
if (StringUtils.isBlank(urlPath)) {
return urlPath;
}
urlPath = urlPath.replaceAll("[/]+", "/");
if (urlPath.startsWith("/")) {
urlPath = urlPath.replaceFirst("/", "");
}
return urlPath;
}
}
@Service
@ConditionalOnBean(LimitInterceptor.class)
public class LimitService implements InitializingBean {
private static final Logger logger = Logger.getLogger(LimitService.class);
@Autowired(required = false)
private RedisTemplate redisTemplate;
public void limitCheck(Long userId, String methodName) {
try {
LimitInfo limitInfo = NacosLimitSwitch.limitInfo;
if (limitInfo == null || !limitInfo.isOpen() || StringUtils.isBlank(methodName)) {
return;
}
if (redisTemplate == null) {
logger.error("警告:请先初始化RedisTemplate模版!!!");
return;
}
Map interfaceInfoMap = limitInfo.getInterfaceInfoMap();
InterfaceInfo interfaceInfo = interfaceInfoMap == null ? null : interfaceInfoMap.get(methodName);
if (interfaceInfo != null && interfaceInfo.isOpen() && interfaceInfo.getSeconds() != null && interfaceInfo.getTimes() != null) {
Long times = redisTemplate.opsForValue().increment(userId + methodName, 1);
if (times == 1) {
redisTemplate.expire( userId + methodName, interfaceInfo.getSeconds(), TimeUnit.SECONDS);
}
if (times > interfaceInfo.getTimes()) {
String desc = StringUtils.isBlank(interfaceInfo.getDesc()) ? (StringUtils.isBlank(limitInfo.getDesc()) ? "接口[" + methodName + "]限流," + interfaceInfo.getSeconds() + "秒请求不能超过" + interfaceInfo.getTimes() + "次" : limitInfo.getDesc()) : interfaceInfo.getDesc();
throw new Exception(desc);
}
}
if (limitInfo.isOpen() && limitInfo.getSeconds() != null && limitInfo.getTimes() != null && interfaceInfo == null) {
Long times = redisTemplate.opsForValue().increment( userId + methodName, 1);
if (times == 1) {
redisTemplate.expire(userId + methodName, limitInfo.getSeconds(), TimeUnit.SECONDS);
}
if (times > limitInfo.getTimes()) {
String desc = StringUtils.isBlank(limitInfo.getDesc()) ? "接口[" + methodName + "]限流," + limitInfo.getSeconds() + "秒请求不能超过" + limitInfo.getTimes() + "次" : limitInfo.getDesc();
throw new Exception(desc);
}
}
} catch (Exception e1) {
logger.error("limitCheck error:", e1);
}
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("====== LimitService限流器 init成功 ======");
}
}
@Data
public class InterfaceInfo {
/**
* true:开启限流,false:不开启限流
*/
private boolean isOpen;
/**
* 报错提示内容
*/
private String desc;
/**
* 时间,秒
*/
private Integer seconds;
/**
* 访问次数
*/
private Integer times;
}
@Data
public class LimitInfo {
/**
* true:开启限流,false:不开启限流
*/
private boolean isOpen;
/**
* 报错提示内容
*/
private String desc;
/**
* 时间,秒
*/
private Integer seconds;
/**
* 访问次数
*/
private Integer times;
private Map interfaceInfoMap;
}
public class NacosLimitSwitch {
/**
* 接口限流配置信息
*/
public static LimitInfo limitInfo;
}
@Configuration
@ConditionalOnBean(LimitInterceptor.class)
public class NacosLimitProperties {
private static final Logger logger = Logger.getLogger(NacosLimitProperties.class);
/**
* 接口限流配置
*/
@NacosValue(value = "${limitConfig:}", autoRefreshed = true)
public void setLimitConfig(String limitConfig) {
System.out.println("接口限流配置:" + limitConfig);
if (StringUtils.isBlank(limitConfig)) {
NacosLimitSwitch.limitInfo = null;
} else {
try {
NacosLimitSwitch.limitInfo = JSONObject.parseObject(limitConfig, LimitInfo.class);
} catch (Exception e) {
logger.error("限流配置反序列化出错:limitConfig=" + limitConfig, e);
}
}
}
}
limitConfig={"interfaceInfoMap":{"test/get":{"open":true,"seconds":5,"times":8,"desc":"当前操作过于频繁"},"test2/get":{"open":true,"seconds":5,"times":8,"desc":"当前操作过于频繁"}},"open":true,"seconds":20,"times":20,"desc":"当前操作过于频繁,请10秒后重试。"}