SpringMVC 簡單限流方案設計


一、概念

限流的目的是通過對並發訪問/請求進行限速,或者對一個時間窗口內的請求進行限速來保護系統,一旦達到限制速率則可以拒絕服務、排隊或等待、降級等處理。

常用的限流算法有兩種:漏桶算法令牌桶算法

漏桶算法的思路很簡單,水(請求)先進入到漏桶里,漏桶以一定的速度出水,當水流入速度過大會直接溢出,可以看出漏桶算法能強行限制數據的傳輸速率。

對於很多應用場景來說,除了要求能夠限制數據的平均傳輸速率外,還要求允許某種程度的突發傳輸。這時候漏桶算法可能就不合適了,令牌桶算法更為適合。

令牌桶算法的原理是系統會以一個恆定的速度往桶里放入令牌,而如果請求需要被處理,則需要先從桶里獲取一個令牌,當桶里沒有令牌可取時,則拒絕服務。

二、應用

Google 開源工具包 Guava 提供了限流工具類 RateLimiter,該類基於令牌桶算法來完成限流,非常易於使用。RateLimiter api 可以查看並發編程網 Guava RateLimiter 的介紹。

我們用 MVC 的攔截器 + Guava RateLimiter 實現我們的限流方案:

@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: 這邊自定義限流列表 urlProperties 的方案不太合理,可以考慮放在配置中心(Nacos、Spring Cloud Config 等)去動態的更新需要限流的 url。

參考博文:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM