SpringMvc 限流之 RateLimiter

概念

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

常用限流算法

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

漏桶算法思路很簡單,水(請求)先進入到漏桶裏,漏桶以一定的速度出水,當水流入速度過大會直接溢出,可以看出漏桶算法能強行限制數據的傳輸速率。
對於很多應用場景來說,除了要求能夠限制數據的平均傳輸速率外,還要求允許某種程度的突發傳輸。這時候漏桶算法可能就不合適了,令牌桶算法更爲適合。

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

控制併發數量

信號量Semaphore

Semaphore是一種基於計數的信號量。它可以設定一個閾值,基於此,多個線程競爭獲取許可信號,做完自己的申請後歸還,超過閾值後,線程申請許可信號將會被阻塞。

簡單的說:Semaphore(10)表示允許10個線程獲取許可證,也就是最大併發數是10。

控制訪問速率

限流工具類RateLimiter

Google開源工具包Guava提供了限流工具類RateLimiter,該類基於令牌桶算法來完成限流,非常易於使用。

RateLimiter源碼分析

調用create接口時,實際實例化的爲SmoothBursty類

  static final class SmoothBursty extends SmoothRateLimiter {

    /** The work (permits) of how many seconds can be saved up if this RateLimiter is unused? */
    final double maxBurstSeconds;


  /**
   * The currently stored permits.
   * 當前存儲令牌數
   */
  double storedPermits;

  /**
   * The maximum number of stored permits.
   * 最大存儲令牌數
   */
  double maxPermits;


   /**
   * The interval between two unit requests, at our stable rate. E.g., a stable rate of 5 permits
   * per second has a stable interval of 200ms.
   * 添加令牌時間間隔,可以理解成生成一個令牌需要的時間,這裏是微秒單位
   */
  double stableIntervalMicros;



  .......
  }



RateLimiter 創建

public static RateLimiter create(double permitsPerSecond) {
    return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
}

static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
    RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
    rateLimiter.setRate(permitsPerSecond);
    return rateLimiter;
}

acquire()方法


  public double acquire(int permits) {
    //計算獲取這些請求需要讓線程等待多長時間
    long microsToWait = reserve(permits);
    //讓線程阻塞microTowait微秒長的時間
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    //返回阻塞的時間
    return 1.0 * microsToWait / SECONDS.toMicros(1L);
  }


reserve()方法

  final long reserve(int permits) {
  //檢查permits是否合法
    checkPermits(permits);
  //保證線程安全
    synchronized (mutex()) {
      return reserveAndGetWaitLength(permits, stopwatch.readMicros());
    }
  }


 final long reserveAndGetWaitLength(int permits, long nowMicros) {
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
    return max(momentAvailable - nowMicros, 0);
  }

reserveEarliestAvailable()

storedPermitsToSpend爲桶中可以消費的令牌數,freshPermits爲還需要的(需要補充的)令牌數,根據該值計算需要等待的時間,追加並更新到nextFreeTicketMicros


//獲取requiredPermits個令牌,並返回需要等待到的時間點
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
    resync(nowMicros);
    long returnValue = nextFreeTicketMicros;
    double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
    double freshPermits = requiredPermits - storedPermitsToSpend;
    //當 requiredPermits>storedPermits纔會有實際意義,這段代碼允許我們提前獲取令牌,但是這種情況會造成下一次令牌生成的時間推遲。有種預支工資的意思
    long waitMicros =
        storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
            + (long) (freshPermits * stableIntervalMicros);

    this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
    //更新可消費的令牌
    this.storedPermits -= storedPermitsToSpend;
    return returnValue;
  }

 ```
 #### resync()
 若當前時間晚於nextFreeTicketMicros,則計算該段時間內可以生成多少令牌,將生成的令牌加入令牌桶中並更新數據

 ```

  void resync(long nowMicros) {
    // if nextFreeTicket is in the past, resync to now
    if (nowMicros > nextFreeTicketMicros) {
    //時間間隔內生成的新令牌
      double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
      //更新最大令牌數量
      storedPermits = min(maxPermits, storedPermits + newPermits);
      //下次可以獲取的時間
      nextFreeTicketMicros = nowMicros;
    }
  }

tryAcquire函數可以嘗試在timeout時間內獲取令牌,如果可以則掛起等待相應時間並返回true,否則立即返回false
canAcquire用於判斷timeout時間內是否可以獲取令牌。

應用

攔截器配置,可以統一配置所有請求的上限,也可以單獨對某個 url配置,該攔截器是基於 SpringMvc 的RequestMappingHandlerMapping獲取url 進行操作。

<bean id="requestLimitInterceptor" class="cn.fraudmetrix.creditcloud.app.intercepters.RequestLimitInterceptor">
        <property name="globalRateLimiter" value="100" />
        <property name="urlProperties">
            <props>
                <prop key="/creditcloud/test">100</prop>
            </props>
        </property>
    </bean>

    <!--攔截器配置-->
    <mvc:interceptors>
        <ref bean="requestLimitInterceptor" />
    </mvc:interceptors>

RequestLimitInterceptor 攔截器

public class RequestLimitInterceptor implements HandlerInterceptor ,BeanPostProcessor{

    private Logger logger = LoggerFactory.getLogger(RequestLimitInterceptor.class);


    private Integer globalRateLimiter = 100;

    private Map<PatternsRequestCondition, RateLimiter> urlRateMap;

    private Properties urlProperties;

    private UrlPathHelper urlPathHelper = new UrlPathHelper();


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (urlRateMap != null) {
            String lookupPath = urlPathHelper.getLookupPathForRequest(request);
            for (PatternsRequestCondition patternsRequestCondition : urlRateMap.keySet()) {
                //使用spring DispatcherServlet的匹配器PatternsRequestCondition進行匹配
                List<String> matches = patternsRequestCondition.getMatchingPatterns(lookupPath);
                if (!matches.isEmpty()) {
                    if (urlRateMap.get(patternsRequestCondition).tryAcquire(1000, TimeUnit.MILLISECONDS)) {
                        logger.info(" 請求'{}'匹配到mathes {} ,成功獲取令牌,進入請求。" ,lookupPath ,Joiner.on(",").join(patternsRequestCondition.getPatterns()) );
                    } else {
                        logger.info( " 請求'{}'匹配到mathes {},超過限流速率,獲取令牌失敗。" ,lookupPath ,Joiner.on(",").join(patternsRequestCondition.getPatterns()));
                        return false;
                    }

                }
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }

    /**
     * 限流的 URL與限流值的K/V 值
     *
     * @param urlProperties
     */
    public void setUrlProperties(Properties urlProperties) {
        this.urlProperties = urlProperties;
    }


    public void setGlobalRateLimiter(Integer globalRateLimiter) {
        this.globalRateLimiter = globalRateLimiter;
    }

    @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(urlRateMap==null){
                urlRateMap = new ConcurrentHashMap<>();
            }
            logger.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 rmi : handlerMethods.keySet()) {
                PatternsRequestCondition pc = rmi.getPatternsCondition();
                urlRateMap.put(pc,RateLimiter.create(globalRateLimiter));
            }
            if(urlProperties!=null){
                for(String urlPatterns :urlProperties.stringPropertyNames()){
                    String limit = urlProperties.getProperty(urlPatterns);
                    if(!limit.matches("^-?\\d+$"))
                        logger.error("the value {} for url patterns {} is not a number ,please check it ",limit,urlPatterns);
                    urlRateMap.put(new PatternsRequestCondition(urlPatterns), RateLimiter.create(Integer.parseInt(limit)));
                }
            }
        }
        return bean;
    }
}

總結

RateLimiter通常用於限制訪問某些物理或邏輯資源的速率。這與jdk併發包中的Semaphore相反,它限制併發訪問的數量而不是速率(注意,併發和速率是密切相關的)。

參考:https://segmentfault.com/a/1190000012875897

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章