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<String, String> methodNameUrlPathMap = new HashMap<>();
        RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();
        for (RequestMappingInfo requestMappingInfo : handlerMethods.keySet()) {
            Set<String> 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<String, InterfaceInfo> 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<String, InterfaceInfo> 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 = "${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秒后重试。"}

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值