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

打印 上一主题 下一主题

主题 679|帖子 679|积分 2037


一、根本使用

1.1 创建限流器

  1. /**
  2. * Returns rate limiter instance by name
  3. *
  4. * @param name of rate limiter
  5. * @return RateLimiter object
  6. */
  7. RRateLimiter getRateLimiter(String name);
复制代码
  1. /**
  2. * Initializes RateLimiter's state and stores config to Redis server.
  3. *
  4. * @param mode - rate mode
  5. * @param rate - rate
  6. * @param rateInterval - rate time interval
  7. * @param rateIntervalUnit - rate time interval unit
  8. * @return true if rate was set and false otherwise
  9. */
  10. boolean trySetRate(RateType mode, long rate, long rateInterval, RateIntervalUnit rateIntervalUnit);
复制代码
trySetRate 用于设置限流参数。此中 RateType 包罗 OVERALL 和 PER_CLIENT 两个罗列常量,分别表示全局限流和单机限流。后面三个参数表明白令牌的生成速率,即每 rateInterval 生成 rate 个令牌,rateIntervalUnit 为 rateInterval 的时间单位。
1.2 获取令牌

  1. /**
  2. * Acquires a specified permits from this RateLimiter,
  3. * blocking until one is available.
  4. *
  5. * Acquires the given number of permits, if they are available
  6. * and returns immediately, reducing the number of available permits
  7. * by the given amount.
  8. *
  9. * @param permits the number of permits to acquire
  10. */
  11. void acquire(long permits);
  12. /**
  13. * Acquires the given number of permits only if all are available
  14. * within the given waiting time.
  15. *
  16. * Acquires the given number of permits, if all are available and returns immediately,
  17. * with the value true, reducing the number of available permits by one.
  18. *
  19. * If no permit is available then the current thread becomes
  20. * disabled for thread scheduling purposes and lies dormant until
  21. * the specified waiting time elapses.
  22. *
  23. * If a permits is acquired then the value true is returned.
  24. *
  25. * If the specified waiting time elapses then the value false
  26. * is returned.  If the time is less than or equal to zero, the method
  27. * will not wait at all.
  28. *
  29. * @param permits amount
  30. * @param timeout the maximum time to wait for a permit
  31. * @param unit the time unit of the timeout argument
  32. * @return true if a permit was acquired and false
  33. *         if the waiting time elapsed before a permit was acquired
  34. */
  35. boolean tryAcquire(long permits, long timeout, TimeUnit unit);
复制代码
acquire 和 tryAcquire 均可用于获取指定数量的令牌,不外 acquire 会阻塞等候,而 tryAcquire 会等候 timeout 时间,如果仍然没有获得指定数量的令牌直接返回 false。
1.3 使用示例

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

二、实现原理

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


  • 令牌以固定速率生成。
  • 生成的令牌放入令牌桶中存放,如果令牌桶满了则多余的令牌会直接丢弃,当哀求到达时,会尝试从令牌桶中取令牌,取到了令牌的哀求可以实行。
  • 如果桶空了,那么尝试取令牌的哀求会被直接丢弃。
RRateLimiter 在创建限流器时通过下面 Lua 脚本设置限流器的相干参数:
  1. redis.call('hsetnx', KEYS[1], 'rate', ARGV[1]);
  2. redis.call('hsetnx', KEYS[1], 'interval', ARGV[2]);
  3. return redis.call('hsetnx', KEYS[1], 'type', ARGV[3]);
复制代码
而获取令牌则是通过以下的 Lua 脚本实现:
  1. -- 请求参数示例
  2. -- KEYS[1] my_limiter
  3. -- KEYS[2] {my_limiter}:value
  4. -- KEYS[4] {my_limiter}:permits
  5. -- ARGV[1] 3 本次请求的令牌数
  6. -- ARGV[2] 1705396021850 System.currentTimeMillis()
  7. -- ARGV[3] 6966135962453115904 ThreadLocalRandom.current().nextLong()
  8. -- 读取 RRateLimiter.trySetRate 中配置的限流器信息
  9. local rate = redis.call('hget', KEYS[1], 'rate');  -- 10 一个时间窗口内产生的令牌数
  10. local interval = redis.call('hget', KEYS[1], 'interval');  -- 1000 一个时间窗口对应的毫秒数
  11. local type = redis.call('hget', KEYS[1], 'type');  -- 0 全局限流
  12. assert(rate ~= false and interval ~= false and type ~= false, 'RateLimiter is not initialized')
  13. local valueName = KEYS[2];  -- {my_limiter}:value 当前可用令牌数字符串的 key
  14. local permitsName = KEYS[4];  -- {my_limiter}:permits 授权记录有序集合的 key
  15. -- 单机限流配置 无需考虑
  16. if type == '1' then
  17.     valueName = KEYS[3];
  18.     permitsName = KEYS[5];
  19. end;
  20. -- 查询当前可用的令牌数 查询失败表明是首次请求令牌
  21. local currentValue = redis.call('get', valueName);
  22. if currentValue == false then -- 首次请求令牌
  23.     -- 单次请求的令牌数不能超过一个时间窗口内产生的令牌数
  24.     assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount could not exceed defined rate');
  25.    
  26.     -- 更新当前可用令牌数以及令牌授权记录 {my_limiter}:permits
  27.     -- set {my_limiter}:permits 10
  28.     redis.call('set', valueName, rate);
  29.     -- zadd {my_limiter}:permits 1705396021850 6966135962453115904_1
  30.     redis.call('zadd', permitsName, ARGV[2], struct.pack('fI', ARGV[3], ARGV[1]));
  31.     -- decrby {my_limiter}:permits 3
  32.     redis.call('decrby', valueName, ARGV[1]);
  33.     return nil;
  34. else -- 再次请求令牌
  35.     -- 查询可以回收的令牌对应的授权记录 即一个时间窗口前的所有授权记录且包括一个时间窗口前这一时刻
  36.     -- 旧令牌回收的本质是新令牌的加入 如果一个令牌是在一个时间窗口前被分配的 那经过一个时间窗口后这个空出的位置应该已经由新令牌填充
  37.     -- zrangebyscore {my_limiter}:permits 0 1705396020850
  38.     local expiredValues = redis.call('zrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval); -- [1936135962853113704_2, 536135765023123704_5]
  39.    
  40.     -- 统计可以回收的令牌数
  41.     local released = 0;
  42.     for i, v in ipairs(expiredValues) do
  43.         local random, permits = struct.unpack('fI', v);
  44.         -- released = released + 2
  45.         -- released = released + 5
  46.         released = released + permits;
  47.     end;
  48.     -- 删除授权记录并回收令牌
  49.     if released > 0 then
  50.         -- zrem {my_limiter}:permits 1936135962853113704_2 536135765023123704_5
  51.         redis.call('zrem', permitsName, unpack(expiredValues));
  52.         currentValue = tonumber(currentValue) + released;
  53.         -- incrby {my_limiter}:value 7
  54.         redis.call('set', valueName, currentValue);
  55.     end;
  56.     if tonumber(currentValue) < tonumber(ARGV[1]) then
  57.         -- 如果回收后可用令牌数仍然不足 返回需要等待的时间
  58.         -- zrangebyscore {my_limiter}:permits (1705396020850 1705396021850 withscores limit 0 1
  59.         local nearest = redis.call('zrangebyscore', permitsName, '(' .. (tonumber(ARGV[2]) - interval), tonumber(ARGV[2]), 'withscores', 'limit', 0, 1);
  60.         local random, permits = struct.unpack('fI', nearest[1]);
  61.         -- 1705396021650 - 1705396021850 + 1000 = 800
  62.         return tonumber(nearest[2]) - (tonumber(ARGV[2]) - interval);
  63.     else
  64.         
  65.         redis.call('zadd', permitsName, ARGV[2], struct.pack('fI', ARGV[3], ARGV[1]));
  66.         redis.call('decrby', valueName, ARGV[1]);
  67.         return nil;
  68.     end;
  69. end;
复制代码

参考:
https://github.com/oneone1995/blog/issues/13
https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

圆咕噜咕噜

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表