【设计】分布式限流实现

863 阅读2分钟

这是我参与更文挑战的第20天,活动详情查看:更文挑战

一、前言

对应的限流算法可以看下这篇:链接

限流可以应对:

  • 热点业务带来的突发请求;
  • 调用方 bug 导致的突发请求;
  • 恶意攻击请求。

之前默认的限流方式都是单机下,例如使用 GuavaRateLimiter


如果部署了多个实例,则需要使用分布式限流。

例如,部署多个网关实例,实现分布式限流。

基于 Redislua 脚本实现限流算法:

  1. 计数模式(固定窗口模式)
  2. 令牌桶算法

Tips: 为减少多次请求(I/O),使用 lua 脚本,实现多次操作。



二、方案实现

以下方案均在网关gatewaySpring Cloud Alibaba) 基础上实现。

(1)计数算法

主要步骤:

  1. 创建 lua 脚本
  2. 在网关添加 filter
  3. 测试

创建 resource/limit.lua

local key = KEYS[1] --限流KEY(一秒一个)
local limit = tonumber(ARGV[1]) --限流大小
local current = tonumber(redis.call("get", key)) or 0
if current + 1 > limit then --如果超出限流大小
   return 0
else  --请求数+1,并设置n秒过期
   redis.call("INCRBY", key, "1")
   redis.call("expire", key, ARGV[2])
   return 1
end
    /**
     * 获取持续时间,1秒内限制请求,则duration设置2
     *
     * @return 返回缓存保存的值。
     */
    public int fetchDuration() {
        // 默认值为 1
        Integer durationSeconds = this.durationSeconds;
        if (durationSeconds == null || durationSeconds < 1) {
            durationSeconds = 1;
        }
        // 1秒内限制请求,则duration设置2
        return durationSeconds + 1;
    }
    public boolean acquire() {
        String key = "lmt:" + "123";
        int limitCount = 10;
        int duration = 2;
        Object result = redisTemplate.execute(
                new RedisScript<Long>() {
                    @Override
                    public String getSha1() {
                        return limitScriptSha1;
                    }

                    @Override
                    public Class<Long> getResultType() {
                        return Long.class;
                    }

                    @Override
                    public String getScriptAsString() {
                        return limitScript;
                    }
                },
                // KEYS[1] key
                Collections.singletonList(key),
                // ARGV[1] limit
                String.valueOf(limitCount),
                // ARGV[2] expire
                String.valueOf(duration)
        );
        return REDIS_SUCCESS.equals(result);
    }



(2)令牌桶算法

令牌桶算法需要在 Redis 中存储桶的大小、当前令牌数量,并且实现每隔一段时间添加新的令牌。最简单的办法当然是每隔一段时间请求一次 Redis,将存储的令牌数量递增。

但实际上我们可以通过对限流两次请求之间的时间和令牌添加速度来计算得出上次请求之后到本次请求时,令牌桶应添加的令牌数量。因此我们在 Redis 中只需要存储上次请求的时间和令牌桶中的令牌数量,而桶的大小和令牌的添加速度可以通过参数传入实现动态修改。

由于第一次运行脚本时默认令牌桶是满的,因此可以将数据的过期时间设置为令牌桶恢复到满所需的时间,及时释放资源。


这里使用 gateway 默认配置来测试下:

  1. 在配置文件中做以下的配置:
server:
  port: 8081
spring:
  cloud:
    gateway:
      routes:
      - id: limit_route
        uri: http://httpbin.org:80/get
        predicates:
        - After=2017-01-20T17:42:47.789-07:00[America/Denver]
        filters:
        - name: RequestRateLimiter
          args:
            key-resolver: '#{@hostAddrKeyResolver}'
            redis-rate-limiter.replenishRate: 1
            redis-rate-limiter.burstCapacity: 3
  application:
    name: gateway-limiter
  redis:
    host: localhost
    port: 6379
    database: 0



三、其他资料

www.infoq.cn/article/qg2… dockone.io/article/101…