Redisson 分布式限流器 RRateLimiter 的使用及原理

04-19 9293阅读 0评论

文章目录

  • 一、基本使用
    • 1.1 创建限流器
    • 1.2 获取令牌
    • 1.3 使用示例
    • 二、实现原理

      一、基本使用

      1.1 创建限流器

      /**
       * Returns rate limiter instance by name
       * 
       * @param name of rate limiter
       * @return RateLimiter object
       */
      RRateLimiter getRateLimiter(String name);
      
      /**
       * Initializes RateLimiter's state and stores config to Redis server.
       * 
       * @param mode - rate mode
       * @param rate - rate
       * @param rateInterval - rate time interval
       * @param rateIntervalUnit - rate time interval unit
       * @return true if rate was set and false otherwise
       */
      boolean trySetRate(RateType mode, long rate, long rateInterval, RateIntervalUnit rateIntervalUnit);
      

      trySetRate 用于设置限流参数。其中 RateType 包含 OVERALL 和 PER_CLIENT 两个枚举常量,分别表示全局限流和单机限流。后面三个参数表明了令牌的生成速率,即每 rateInterval 生成 rate 个令牌,rateIntervalUnit 为 rateInterval 的时间单位。

      Redisson 分布式限流器 RRateLimiter 的使用及原理 第1张
      (图片来源网络,侵删)

      1.2 获取令牌

      /**
       * Acquires a specified permits from this RateLimiter, 
       * blocking until one is available.
       *
       * Acquires the given number of permits, if they are available 
       * and returns immediately, reducing the number of available permits 
       * by the given amount.
       * 
       * @param permits the number of permits to acquire
       */
      void acquire(long permits);
      /**
       * Acquires the given number of permits only if all are available
       * within the given waiting time.
       *
       * Acquires the given number of permits, if all are available and returns immediately,
       * with the value true, reducing the number of available permits by one.
       *
       * If no permit is available then the current thread becomes
       * disabled for thread scheduling purposes and lies dormant until
       * the specified waiting time elapses.
       *
       * If a permits is acquired then the value true is returned.
       *
       * If the specified waiting time elapses then the value false
       * is returned.  If the time is less than or equal to zero, the method
       * will not wait at all.
       *
       * @param permits amount
       * @param timeout the maximum time to wait for a permit
       * @param unit the time unit of the timeout argument
       * @return true if a permit was acquired and false
       *         if the waiting time elapsed before a permit was acquired
       */
      boolean tryAcquire(long permits, long timeout, TimeUnit unit);
      

      acquire 和 tryAcquire 均可用于获取指定数量的令牌,不过 acquire 会阻塞等待,而 tryAcquire 会等待 timeout 时间,如果仍然没有获得指定数量的令牌直接返回 false。

      1.3 使用示例

      @Slf4j
      @SpringBootTest
      class RateLimiterTest {
          
          @Autowired
          private RedissonClient redissonClient;
          
          private static final int threadCount = 10;
          @Test
          void test() throws InterruptedException {
              RRateLimiter rateLimiter = redissonClient.getRateLimiter("my_limiter");
              rateLimiter.trySetRate(RateType.OVERALL, 10, 1, RateIntervalUnit.SECONDS);
              CountDownLatch latch = new CountDownLatch(threadCount);
              for (int i = 0; i  {
                      rateLimiter.tryAcquire(5, 3, TimeUnit.SECONDS);
                      latch.countDown();
                      log.info("latch count {}", latch.getCount());
                  }).start();
              }
              
              latch.await();
          }
      }
      
      2024-01-16 20:14:27 INFO  [Thread-2] atreus.ink.rate.RateLimiterTest : latch count 9
      2024-01-16 20:14:27 INFO  [Thread-3] atreus.ink.rate.RateLimiterTest : latch count 8
      2024-01-16 20:14:28 INFO  [Thread-1] atreus.ink.rate.RateLimiterTest : latch count 7
      2024-01-16 20:14:29 INFO  [Thread-10] atreus.ink.rate.RateLimiterTest : latch count 6
      2024-01-16 20:14:29 INFO  [Thread-8] atreus.ink.rate.RateLimiterTest : latch count 5
      2024-01-16 20:14:30 INFO  [Thread-5] atreus.ink.rate.RateLimiterTest : latch count 4
      2024-01-16 20:14:30 INFO  [Thread-4] atreus.ink.rate.RateLimiterTest : latch count 3
      2024-01-16 20:14:30 INFO  [Thread-6] atreus.ink.rate.RateLimiterTest : latch count 2
      2024-01-16 20:14:30 INFO  [Thread-7] atreus.ink.rate.RateLimiterTest : latch count 1
      2024-01-16 20:14:30 INFO  [Thread-9] atreus.ink.rate.RateLimiterTest : latch count 0
      

      二、实现原理

      Redisson 的 RRateLimiter 基于令牌桶实现,令牌桶的主要特点如下:

      • 令牌以固定速率生成。
      • 生成的令牌放入令牌桶中存放,如果令牌桶满了则多余的令牌会直接丢弃,当请求到达时,会尝试从令牌桶中取令牌,取到了令牌的请求可以执行。
      • 如果桶空了,那么尝试取令牌的请求会被直接丢弃。

        RRateLimiter 在创建限流器时通过下面 Lua 脚本设置限流器的相关参数:

        redis.call('hsetnx', KEYS[1], 'rate', ARGV[1]);
        redis.call('hsetnx', KEYS[1], 'interval', ARGV[2]);
        return redis.call('hsetnx', KEYS[1], 'type', ARGV[3]);
        

        而获取令牌则是通过以下的 Lua 脚本实现:

        -- 请求参数示例
        -- KEYS[1] my_limiter
        -- KEYS[2] {my_limiter}:value
        -- KEYS[4] {my_limiter}:permits
        -- ARGV[1] 3 本次请求的令牌数
        -- ARGV[2] 1705396021850 System.currentTimeMillis()
        -- ARGV[3] 6966135962453115904 ThreadLocalRandom.current().nextLong()
        -- 读取 RRateLimiter.trySetRate 中配置的限流器信息
        local rate = redis.call('hget', KEYS[1], 'rate');  -- 10 一个时间窗口内产生的令牌数
        local interval = redis.call('hget', KEYS[1], 'interval');  -- 1000 一个时间窗口对应的毫秒数
        local type = redis.call('hget', KEYS[1], 'type');  -- 0 全局限流
        assert(rate ~= false and interval ~= false and type ~= false, 'RateLimiter is not initialized')
        local valueName = KEYS[2];  -- {my_limiter}:value 当前可用令牌数字符串的 key
        local permitsName = KEYS[4];  -- {my_limiter}:permits 授权记录有序集合的 key
        -- 单机限流配置 无需考虑
        if type == '1' then
            valueName = KEYS[3];
            permitsName = KEYS[5];
        end;
        -- 查询当前可用的令牌数 查询失败表明是首次请求令牌
        local currentValue = redis.call('get', valueName);
        if currentValue == false then -- 首次请求令牌
            -- 单次请求的令牌数不能超过一个时间窗口内产生的令牌数
            assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount could not exceed defined rate');
            
            -- 更新当前可用令牌数以及令牌授权记录 {my_limiter}:permits
            -- set {my_limiter}:permits 10
            redis.call('set', valueName, rate);
            -- zadd {my_limiter}:permits 1705396021850 6966135962453115904_1
            redis.call('zadd', permitsName, ARGV[2], struct.pack('fI', ARGV[3], ARGV[1]));
            -- decrby {my_limiter}:permits 3
            redis.call('decrby', valueName, ARGV[1]);
            return nil;
        else -- 再次请求令牌
            -- 查询可以回收的令牌对应的授权记录 即一个时间窗口前的所有授权记录且包括一个时间窗口前这一时刻
            -- 旧令牌回收的本质是新令牌的加入 如果一个令牌是在一个时间窗口前被分配的 那经过一个时间窗口后这个空出的位置应该已经由新令牌填充
            -- zrangebyscore {my_limiter}:permits 0 1705396020850
            local expiredValues = redis.call('zrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval); -- [1936135962853113704_2, 536135765023123704_5]
            
            -- 统计可以回收的令牌数
            local released = 0;
            for i, v in ipairs(expiredValues) do
                local random, permits = struct.unpack('fI', v);
                -- released = released + 2
                -- released = released + 5
                released = released + permits;
            end;
            -- 删除授权记录并回收令牌
            if released > 0 then
                -- zrem {my_limiter}:permits 1936135962853113704_2 536135765023123704_5
                redis.call('zrem', permitsName, unpack(expiredValues));
                currentValue = tonumber(currentValue) + released;
                -- incrby {my_limiter}:value 7
                redis.call('set', valueName, currentValue);
            end;
            if tonumber(currentValue)  
        

        参考:

        Redisson 分布式限流器 RRateLimiter 的使用及原理 第2张
        (图片来源网络,侵删)

        https://github.com/oneone1995/blog/issues/13

        https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673


免责声明
1、本网站属于个人的非赢利性网站,转载的文章遵循原作者的版权声明。
2、本网站转载文章仅为传播更多信息之目的,凡在本网站出现的信息,均仅供参考。本网站将尽力确保所
提供信息的准确性及可靠性,但不保证信息的正确性和完整性,且不对因信息的不正确或遗漏导致的任何
损失或损害承担责任。
3、任何透过本网站网页而链接及得到的资讯、产品及服务,本网站概不负责,亦不负任何法律责任。
4、本网站所刊发、转载的文章,其版权均归原作者所有,如其他媒体、网站或个人从本网下载使用,请在
转载有关文章时务必尊重该文章的著作权,保留本网注明的“稿件来源”,并白负版权等法律责任。

手机扫描二维码访问

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
评论列表 (暂无评论,9293人围观)

还没有评论,来说两句吧...

目录[+]