五行代码实现分布式限流

161 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

写在前面

相信针对接口限流大家在实际的业务开发中都或多或少的遇到过,尤其现在微服务横行的情况下,单机限流方案绝大多数不能满足实际的微服务的架构,就使得像 GuavaRateLimiter 限流神器,在很多分布式场景下就无法大展身手。

今天彻彻底底当一次标题党,通过五行代码介绍一种简单实用的分布式限流方案,当你的接口限流场景对平滑性没有非常高要求的情况下,可以放心大胆的使用,简洁而高效。

话不多说上代码

我的限流场景是对一个业务接口的调用实施限流,限流的要求是一秒钟限制请求一次,同时我所请求的接口对一秒的限制并没有严格的连续性要求,如在 22:24:30.500 请求一次,又在22:24:31.100 请求一次,如果将请求精确到秒的维度来看,确实是22:24:30 请求了一次接口,再22:24:31 又请求了一次接口满足一秒一次调用的限制;但要是将时间放到毫秒的维度来看,在22:24:30.500 - 22:24:31:500 这一秒中来看,实际上请求了接口两次,不满足接口限流的要求,所以是否使用这种方案,要结合具体的业务场景具体分析。

    /**
     * 判断当前时间是否能够执行接口请求
     *
     * @param key      接口key
     * @param execTime 接口请求时间
     * @return 执行时间是否可以调度
     */
    private boolean checkLimitLock(String key, Long execTime) {
        //组装接口请求时间(秒)的接口限流key
        //hhmmss_key 如 222430_key
        String lockKey = wrapRateLimitLock(key, execTime);
        //利用 redis incr() 函数的原子性来锁定调度的那一秒是否能够执行
        Long routeCount = redisTemplate.opsForValue().increment(lockKey);
        //设置 key 过期时间,自动释放 redis 内存
        final long lockExpire = (execTime - System.currentTimeMillis()) / 1000 + 1;
        redisTemplate.boundValueOps(lockKey).expire(lockExpire, TimeUnit.SECONDS);
        return routeCount <= 1;
    }

说说限流模型

限流的原理是利用 redis 实现了一个分布式计数器进行限流,即是一个固定窗口算法限流模型,其流程为:

  • 按时间划分多个独立且固定大小的时间窗口
  • 请求落在时间窗口内计数器就 +1
  • 超过计数器限流阈值的请求将会被拒绝

固定窗口胜在实现简单,但由于时间窗口固定不够平滑,极端情况下会出现双倍突发请求的情况。若业务对限流有严格的要求,建议尝试换个模型:

  • 滑动窗口计数器:滑动时间窗口计数器是对固定时间窗口算法的一种改进,将固定窗口在进行区间划分同时对窗口进行滑动,丢弃老的一个小区间加入新的一个小区间,因为区间粒度更细,所以限流更加均匀。
  • 令牌桶算法:一个恒定的速率往桶里放入令牌,如果要请求接口时,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝请求。

在后面的文章中,会接口具体的场景介绍两种方法的实现。