设计一个基于用户的API限流策略 Rate Limit

设计一个基于用户的API限流策略 Rate Limit

应用场景

API接口的流量控制策略:缓存、降级、限流。限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。限流策略虽然降低了服务接口的访问频率和并发量,却换取服务接口和业务应用系统的高可用。

常用的限流策略:

  • Nginx 限流

    按照一定的规则如帐号、IP、系统间调用逻辑等在 Nginx 层面做限流

  • 业务系统限流

    • 客户端限流

    • 服务端限流

  • 数据库限流

常用限流算法

  • 计数器

    计数器是最简单粗暴的算法,通过直接统计每个时间的请求数目来判断是否需要拒绝。

    比如某个服务最多只能每秒钟处理 100 个请求。我们可以设置一个 1 秒钟的滑动窗口,窗口中有 10 个格子,每个格子 100 毫秒,每 100 毫秒移动一次,每次移动都需要记录当前服务请求的次数。内存中需要保存 10 次的次数。可以用数据结构 LinkedList 来实现。格子每次移动的时候判断一次,当前访问次数和 LinkedList 中最后一个相差是否超过 100 ,如果超过就需要限流了。

    设计一个基于用户的API限流策略 Rate Limit_第1张图片

    很明显,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。

    计数器的实现简单,但是是平均分配1秒钟的请求,然而实际情况中的请求往往是动态的,流量不平滑的。

  • 漏桶

    漏桶( Leaky Bucket )算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。

    设计一个基于用户的API限流策略 Rate Limit_第2张图片

    可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水( burst ),另一个是水桶漏洞的大小( rate )。

    因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发( burst )到端口速率.因此,漏桶算法对于存在突发特性的流量来说缺乏效率.

  • 令牌桶

    令牌桶算法( Token Bucket )和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100 ,则间隔是 10ms )往桶里加入 Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个 Token ,如果没有 Token 可拿了就阻塞或者拒绝服务.

    设计一个基于用户的API限流策略 Rate Limit_第3张图片

    令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量.

基于Redis的令牌桶算法限流策略实现

  • 策略

    因为出现了某些客户突然加大流量的情况,为了避免抢占其他用户的资源,所以设计了基于用户(限制单个用户的最大请求数)的令牌桶策略。

  • JAVA 实现

    /**
     * 获取令牌
     *
     * @param key            令牌类别标识(每个用户不同)
     * @param permits        请求的令牌数量
     * @param currMillSecond 当前毫秒数
     * @return 是否能请求到令牌
     */
    public boolean acquire(String key, Integer permits, long currMillSecond) {
        try (Jedis jedis = JedisPoolUtil.getJedisPool().getResource()) {
            //针对新用户创建令牌桶
            if (!jedis.exists(key)) {
                jedis.hset(key, "last_mill_second", String.valueOf(currMillSecond));
                jedis.hset(key, "curr_permits", "0");
                jedis.hset(key, "max_permits", "500");
                jedis.hset(key, "rate", "400");
                return true;
            }
            //获取令牌桶信息,上一个令牌时间,当前可用令牌数,最大令牌数,令牌消耗速率
            List limitInfo = jedis.hmget(key, "last_mill_second", "curr_permits", "max_permits", "rate");
            long lastMillSecond = Long.parseLong(limitInfo.get(0));
            Integer currPermits = Integer.valueOf(limitInfo.get(1));
            Integer maxPermits = Integer.valueOf(limitInfo.get(2));
            Double rate = Double.valueOf(limitInfo.get(3));
            //向桶里面添加令牌
            Double reversePermitsDouble = ((currMillSecond - lastMillSecond) / 1000) * rate;
            Integer reversePermits = reversePermitsDouble.intValue();
            Integer expectCurrPermits = reversePermits + currPermits;
            Integer localCurrPermits = Math.min(expectCurrPermits, maxPermits);
            //添加令牌之后更新时间
            if (reversePermits > 0) {
                jedis.hset(key, "last_mill_second", String.valueOf(currMillSecond));
            }
            //判断桶里面剩余的令牌数目
            if (localCurrPermits - permits >= 0) {
                jedis.hset(key, "curr_permits", String.valueOf(localCurrPermits - permits));
                return true;
            } else {
                jedis.hset(key, "curr_permits", String.valueOf(localCurrPermits));
                return false;
            }
        } catch (Exception e) {
            return false;
        }
    }
    

    使用方式:

    if (!rateLimiterService.acquire("limiter:" + uid, 1, System.currentTimeMillis())) {
            throw new BusinessException(ExceptionType.TOO_BUSY);
    }
    

你可能感兴趣的:(Java)