Spring MVC simple current limiting Design

First, the concept

Current limiting purposes is protected by concurrent access / limit the rate request, or a request within a time window system speed, the rate limit is reached once the service may be denied, or waiting queue, demotion process.

Limiting algorithm commonly used in two ways: leaky bucket and token bucket algorithm :

Leaky bucket algorithm is very simple idea, water (request) to the drain into the first bucket, leaky bucket of water at a constant speed, when the speed of the water flows directly through the overflow Assembly can be seen that the token bucket algorithm can impose restrictions on the data transmission rate.

For many scenarios, it is possible to limit the claims in addition to the average data transfer rate, but also requires a certain degree of allowed burst transmission. This time may be inappropriate leaky bucket algorithm, the token bucket algorithm is more suitable.

Token bucket algorithm principle is that the system will be at a constant speed into a token in the bucket, but if the request to be processed, starting with the need to obtain a token bucket, when the token bucket is not desirable, then denial of service.

Second, the application

Google open source toolkit Guava provides current limiting tools RateLimiter, class token bucket algorithm to complete current limiting, very easy to use. RateLimiter api can see concurrent programming network Guava RateLimiter introduced.

We use MVC interceptors + Guava RateLimiter achieve our limiting scheme:

@Slf4j
public class RequestLimitInterceptor extends HandlerInterceptorAdapter implements BeanPostProcessor {

    private static final Integer GLOBAL_RATE_LIMITER = 10;

    private static Map<PatternsRequestCondition, RateLimiter> URL_RATE_MAP;

    private Properties urlProperties;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (URL_RATE_MAP != null) {
            String lookupPath = new UrlPathHelper().getLookupPathForRequest(request);
            for (PatternsRequestCondition patternsRequestCondition : URL_RATE_MAP.keySet()) {
                //使用spring DispatcherServlet的匹配器PatternsRequestCondition进行匹配
                //spring 3.x 版本
                //Set<String> matches = patternsRequestCondition.getMatchingCondition(request).getPatterns();
                //spring 4.x 版本
                List<String> matches = patternsRequestCondition.getMatchingPatterns(lookupPath);
                if (CollectionUtils.isEmpty(matches)){
                    continue;
                }
                //尝试获取令牌
                if (!URL_RATE_MAP.get(patternsRequestCondition).tryAcquire(1000, TimeUnit.MILLISECONDS)) {
                    log.info(" 请求'{}'匹配到 mathes {},超过限流速率,获取令牌失败。", lookupPath, Joiner.on(",").join(patternsRequestCondition.getPatterns()));
                    return false;
                }
                log.info(" 请求'{}'匹配到 mathes {} ,成功获取令牌,进入请求。", lookupPath, Joiner.on(",").join(patternsRequestCondition.getPatterns()));
            }
        }
        return super.preHandle(request, response, handler);
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (RequestMappingHandlerMapping.class.isAssignableFrom(bean.getClass())) {
            if (URL_RATE_MAP == null) {
                URL_RATE_MAP = new ConcurrentHashMap<>(16);
            }
            log.info("we get all the controllers's methods and assign it to urlRateMap");
            RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) bean;
            Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();
            for (RequestMappingInfo mappingInfo : handlerMethods.keySet()) {
                PatternsRequestCondition requestCondition = mappingInfo.getPatternsCondition();
                // 默认的 url 限流方案设定
                URL_RATE_MAP.put(requestCondition, RateLimiter.create(GLOBAL_RATE_LIMITER));
            }
            // 自定义的限流方案设定
            if (urlProperties != null) {
                for (String urlPatterns : urlProperties.stringPropertyNames()) {
                    String limit = urlProperties.getProperty(urlPatterns);
                    if (!limit.matches("^-?\\d+$")){
                        log.error("the value {} for url patterns {} is not a number ,please check it ", limit, urlPatterns);
                    }
                    URL_RATE_MAP.put(new PatternsRequestCondition(urlPatterns), RateLimiter.create(Integer.parseInt(limit)));
                }
            }
        }
        return bean;
    }

    /**
     * 限流的 URL与限流值的 K/V 值
     *
     * @param urlProperties
     */
    public void setUrlProperties(Properties urlProperties) {
        this.urlProperties = urlProperties;
    }
}
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Bean
    public RequestLimitInterceptor requestLimitInterceptor(){
        RequestLimitInterceptor limitInterceptor = new RequestLimitInterceptor();
        // 设置自定义的 url 限流方案
        Properties properties = new Properties();
        properties.setProperty("/admin/**", "10");
        limitInterceptor.setUrlProperties(properties);
        return limitInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 限流方案
        registry.addInterceptor(requestLimitInterceptor());
    }
}

tips: here a custom program limiting list urlProperties not reasonable, can be considered in a configuration center (Nacos, Spring Cloud Config, etc.) to dynamically update url limiting of need.

Reference Hirofumi:

  1. https://blog.csdn.net/Lili429/article/details/79236819
  2. https://blog.csdn.net/valleychen1111/article/details/78038366

Guess you like

Origin www.cnblogs.com/jmcui/p/11281788.html