分布式|Spring Boot + Redis 实现分布式限流

一、常见限流算法 1、固定窗口限流算法 首先维护一个计数器,将单位时间段当做一个窗口,计数器记录这个窗口接收请求的次数。

  • 当次数少于限流阀值,就允许访问,并且计数器+1
  • 当次数大于限流阀值,就拒绝访问。
  • 当前的时间窗口过去之后,计数器清零。
2、滑动窗口限流算法 滑动窗口也是维护单位时间内的请求次数,其与固定窗口限流算法的区别是,滑动窗口的粒度更细,将一个大的时间窗口划分为若干个小的时间窗口,分别记录每个小周期内接口的访问次数,通过滑动时间删除小的时间窗口,以此来解决固定窗口临界值的问题
3、漏桶限流算法 原理很简单,可以认为就是注水漏水的过程。往漏桶中以任意速率流入水,以固定的速率流出水。当水超过桶的容量时,会被溢出,也就是被丢弃。因为桶容量是不变的,保证了整体的速率。
4、令牌桶算法 令牌桶算法每隔一段时间就将一定量的令牌放入桶中,获取到令牌的请求直接访问后段的服务,没有获取到令牌的请求会被拒绝。同时令牌桶有一定的容量,当桶中的令牌数达到最大值后,不再放入令牌。
二、常见分布式限流实现方案 限流又分为单机限流和分布式限流,常见的分布式限流方案如下
● 可以基于redis,做分布式限流
● 可以基于nginx做分布式限流
● 可以使用阿里开源的 sentinel 中间件
三、设计思路 1、希望单个接口限流的数量可以实时控制,引入Nacos用于配置接口限流数
2、根据自身项目需求,使用用户id与接口名作为key,使redisTemplate.opsForValue().increment方法作为vaule,从而实现次数的递增,然后设置缓存过期时间来实现固定时间窗口
3、希望对业务没有耦合,使用拦截器拦截固定请求,限流算法实现写在拦截器中
固定窗口限流算法实现:
在Redis中根据该用户id和接口名创建一个键,并设置这个键的过期时间,当用户请求到来的时候,先去redis中根据用户ip获取这个用户当前分钟请求了多少次,如果获取不到,则说明这个用户当前分钟第一次访问,就创建这个健,并+1,如果获取到了就判断当前有没有超过我们限制的次数,如果到了我们限制的次数则禁止访问。
四、代码实现 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(); } }

2、实现自定义拦截器
@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; }}

3、编写限流实现类
@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成功 ======"); }}

4、限流配置类实体
@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; }

5、Nacos配置类以及配置信息
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 = "https://www.it610.com/article/${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秒后重试。"}

【分布式|Spring Boot + Redis 实现分布式限流】

    推荐阅读