SpringMvc 限流之 RateLimiter

本文介绍了SpringMvc中使用RateLimiter进行限流的原理和应用。通过分析RateLimiter的源码,包括SmoothBursty类、create接口、acquire、reserve和reserveEarliestAvailable方法,阐述了如何控制并发数量和访问速率。同时,提到了Semaphore作为另一种控制并发的手段。最后,展示了RateLimiter在拦截器中的配置和应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概念

限流 限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理

常用限流算法

常用的限流算法有两种:漏桶算法令牌桶算法

漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。

令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

控制并发数量

信号量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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值