• redisson分布式限流[RRateLimiter]源码分析


    分布式限流-单位时间多实例多线程访问次数限制

    接前面聊一聊redisson及优雅实现说一说spring boot优雅集成redisson,简单以源码的方式给大家介绍了redisson的:可重入性、阻塞、续约、红锁、联锁、加锁解锁流程和集成spring boot注意点和优雅实现方式。

    接下来在讲一讲平时用的比较多的限流模块--RRateLimiter

    1.简单使用

    1. public static void main(String[] args) throws InterruptedException {
    2. RRateLimiter rateLimiter = createLimiter();
    3. int allThreadNum = 20;
    4. CountDownLatch latch = new CountDownLatch(allThreadNum);
    5. long startTime = System.currentTimeMillis();
    6. for (int i = 0; i < allThreadNum; i++) {
    7. // new Thread(() -> {
    8. if(i % 3 == 0) Thread.sleep(1000);
    9. boolean pass = rateLimiter.tryAcquire();
    10. if(pass) {
    11. log.info("get ");
    12. } else {
    13. log.info("no");
    14. }
    15. // latch.countDown();
    16. // }).start();
    17. }
    18. // latch.await();
    19. System.out.println("Elapsed " + (System.currentTimeMillis() - startTime));
    20. }
    21. public static RRateLimiter createLimiter() {
    22. Config config = new Config();
    23. config.useSingleServer()
    24. .setTimeout(1000000)
    25. .setPassword("123456")
    26. .setAddress("redis://xxxx:6379");
    27. RedissonClient redisson = Redisson.create(config);
    28. RRateLimiter rateLimiter = redisson.getRateLimiter("myRateLimiter3");
    29. // 初始化:PER_CLIENT 单实例执行,OVERALL 全实例执行
    30. // 最大流速 =10秒钟产生3个令牌
    31. rateLimiter.trySetRate(RateType.OVERALL, 3, 10, RateIntervalUnit.SECONDS);
    32. return rateLimiter;
    33. }
    34. 复制代码

    实际结果:

    1. [2022-10-29 14:32:46.261][INFO ][main][][] RedisTest - get
    2. [2022-10-29 14:32:46.312][INFO ][main][][] RedisTest - get
    3. [2022-10-29 14:32:46.358][INFO ][main][][] RedisTest - get
    4. [2022-10-29 14:32:47.416][INFO ][main][][] RedisTest - no
    5. [2022-10-29 14:32:47.469][INFO ][main][][] RedisTest - no
    6. [2022-10-29 14:32:47.517][INFO ][main][][] RedisTest - no
    7. [2022-10-29 14:32:48.577][INFO ][main][][] RedisTest - no
    8. [2022-10-29 14:32:48.623][INFO ][main][][] RedisTest - no
    9. 复制代码

    2. 实现限流redisson使用了哪些redis数据结构

    1. Hash结构 -- 限流器结构:

      参数rate代表速率

      参数interval代表多少时间内产生的令牌

      参数type代表单机还是集群

    2. ZSET结构 -- 记录获取令牌的时间戳,用于时间对比。

      1667025166312 --> 2022-10-29 14:32:46

      1667025166262 --> 2022-10-29 14:32:46

      1667025166215 --> 2022-10-29 14:32:46

    3. String结构 --记录的是当前令牌桶中的令牌数【很明显被我用完了现在是0】

    3. 超过10s,我再次获取一个令牌,数据结构发生的变化

    1. ZSET结构。-- 新生成一个ZSET结构,存放获取令牌的时间戳

    1. String 结构 --当前令牌桶还有2个令牌

    4. 源码浅析

    1. RRateLimiter rateLimiter = redisson.getRateLimiter("myRateLimiter3");
    2. // 初始化
    3. // 最大流速 =10秒钟产生3个令牌
    4. rateLimiter.trySetRate(RateType.PER_CLIENT, 3, 10, RateIntervalUnit.SECONDS);
    5. 复制代码

    初始化定义没有什么好讲的,就是创建HASH结构

    主要还是讲讲: rateLimiter.tryAcquire()

    1. private RFuture tryAcquireAsync(RedisCommand command, Long value) {
    2. return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "local rate = redis.call('hget', KEYS[1], 'rate');local interval = redis.call('hget', KEYS[1], 'interval');local type = redis.call('hget', KEYS[1], 'type');assert(rate ~= false and interval ~= false and type ~= false, 'RateLimiter is not initialized')local valueName = KEYS[2];local permitsName = KEYS[4];if type == '1' then valueName = KEYS[3];permitsName = KEYS[5];end;local currentValue = redis.call('get', valueName); if currentValue ~= false then local expiredValues = redis.call('zrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval); local released = 0; for i, v in ipairs(expiredValues) do local random, permits = struct.unpack('fI', v);released = released + permits;end; if released > 0 then redis.call('zrem', permitsName, unpack(expiredValues)); currentValue = tonumber(currentValue) + released; redis.call('set', valueName, currentValue);end;if tonumber(currentValue) < tonumber(ARGV[1]) then local nearest = redis.call('zrangebyscore', permitsName, '(' .. (tonumber(ARGV[2]) - interval), tonumber(ARGV[2]), 'withscores', 'limit', 0, 1); local random, permits = struct.unpack('fI', nearest[1]);return tonumber(nearest[2]) - (tonumber(ARGV[2]) - interval);else redis.call('zadd', permitsName, ARGV[2], struct.pack('fI', ARGV[3], ARGV[1])); redis.call('decrby', valueName, ARGV[1]); return nil; end; else assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount could not exceed defined rate'); redis.call('set', valueName, rate); redis.call('zadd', permitsName, ARGV[2], struct.pack('fI', ARGV[3], ARGV[1])); redis.call('decrby', valueName, ARGV[1]); return nil; end;", Arrays.asList(this.getName(), this.getValueName(), this.getClientValueName(), this.getPermitsName(), this.getClientPermitsName()), new Object[]{value, System.currentTimeMillis(), ThreadLocalRandom.current().nextLong()});
    3. }
    4. 复制代码

    主要就是这段lua代码,下面我详细过一下

    作者目前用的3.16.3版本,刚好遇见redisson的bug,见3197,请大家用最新版本,以下为修复后解析。

    1. -- 获取hash结构的速率
    2. local rate = redis.call("hget", KEYS[1], "rate")
    3. -- 获取hash结构的时间区间(ms)
    4. local interval = redis.call("hget", KEYS[1], "interval")
    5. -- 获取hash结构的时间类型
    6. local type = redis.call("hget", KEYS[1], "type")
    7. -- 判断是否初始化限流结构
    8. assert(rate ~= false and interval ~= false and type ~= false, "RateLimiter is not initialized")
    9. -- {name}:value string结构,这个key记录的是当前令牌桶中的令牌数
    10. local valueName = KEYS[2]
    11. -- {name}:permits zset结构,记录了请求的令牌数,score则为请求的时间戳
    12. local permitsName = KEYS[4]
    13. -- 单机限流才会用到,集群模式不用关注
    14. if type == "1" then
    15. valueName = KEYS[3]
    16. permitsName = KEYS[5]
    17. end
    18. -- 生产速率rate必须比请求的令牌数大
    19. assert(tonumber(rate) >= tonumber(ARGV[1]), "Requested permits amount could not exceed defined rate")
    20. -- 初始化RateLimiter并不会初始化stirng结构,因此第一次获取这里currentValue是null
    21. local currentValue = redis.call("get", valueName)
    22. if currentValue ~= false then
    23. -- 第二次获取令牌执行
    24. -------------------------- 获取zset结构:统计之前的请求令牌数
    25. -- 范围是0 ~ (第二次请求时间戳 - 令牌生产的时间)
    26. local expiredValues = redis.call("zrangebyscore", permitsName, 0, tonumber(ARGV[2]) - interval)
    27. local released = 0
    28. -- lua迭代器,遍历expiredValues,如果有值,那么released等于之前所有请求的令牌数之和,表示应该释放多少令牌
    29. for i, v in ipairs(expiredValues) do
    30. -- 获取请求数permits
    31. local random, permits = struct.unpack("fI", v)
    32. released = released + permits
    33. end
    34. -- 之前的请求令牌数 > 0, 例如10s产生3个令牌,现在超过10s了,重置周期并计算剩余令牌数
    35. if released > 0 then
    36. -- 移除zset中所有元素【要求是同一个限流器permitsName,不然就移除不了,尴尬】
    37. redis.call("zrem", permitsName, unpack(expiredValues))
    38. currentValue = tonumber(currentValue) + released
    39. ------------------------- 更新string结构:=剩下令牌数+释放令牌数
    40. redis.call("set", valueName, currentValue)
    41. end
    42. -- 如果当前令牌数 < 请求的令牌数
    43. if tonumber(currentValue) < tonumber(ARGV[1]) then
    44. -- 从zset中找到距离当前时间最近的那个请求,也就是上一次放进去的请求信息
    45. local nearest = redis.call('zrangebyscore', permitsName, '(' .. (tonumber(ARGV[2]) - interval), tonumber(ARGV[2]), 'withscores', 'limit', 0, 1);
    46. local random, permits = struct.unpack("fI", nearest[1])
    47. -- 返回 上一次请求的时间戳 - (当前时间戳 - 令牌生成的时间间隔) 这个值表示还需要多久才能生产出足够的令牌
    48. return tonumber(nearest[2]) - (tonumber(ARGV[2]) - interval)
    49. else
    50. -- 如果当前令牌数 ≥ 请求的令牌数,表示令牌够多,更新zset
    51. ------------------------- 更新zset结构
    52. redis.call("zadd", permitsName, ARGV[2], struct.pack("fI", ARGV[3], ARGV[1]))
    53. ------------------------- 更新Stringt结构,减少一个剩下的令牌数
    54. redis.call("decrby", valueName, ARGV[1])
    55. return nil
    56. end
    57. else
    58. --------汀雨笔记----------------- 初始化Stringt结构,当前限流器的令牌数
    59. redis.call("set", valueName, rate)
    60. --------汀雨笔记----------------- 初始化zset结构
    61. redis.call("zadd", permitsName, ARGV[2], struct.pack("fI", ARGV[3], ARGV[1]))
    62. -- struct.pack第一个参数表示格式字符串,f是浮点数、I是长整数。所以这个格式字符串表示的是把一个浮点数和长整数拼起来的结构体,
    63. -- ARGV[2]就是请求时间戳,ARGV[1]是请求的令牌数,统计会用到,ARGV[3]是当前时间戳为种子的随机数,具体用处还不知道,知道的网友可以留言
    64. ------------------------- 更新Stringt结构,因为这是获取令牌操作,减掉一个令牌
    65. -------------------------【本文作者认为,这里可以直接初始化string结构,值为rate - 1
    66. redis.call("decrby", valueName, ARGV[1])
    67. return nil
    68. end
    69. 复制代码

    这段lua代码也并不复杂,令牌桶的数量主要是通过时间窗口来控制,判断上一个请求是否超过了令牌生产周期。

    留下一个疑问?

    1. -- 移除zset中所有元素【要求是同一个限流器permitsName,不然就移除不了,尴尬】
    2. redis.call("zrem", permitsName, unpack(expiredValues))
    3. 复制代码

    我自己在本地测试,只要超过10s,permitsName就不一样,这就导致了这部分数据是不能移除的,就产生了冗余数据,从前面的截图也可以看出,是新生成了一个zset数据结构。

    相当于直接走到了这一步:

    1. ------------------------- 更新zset结构
    2. redis.call("zadd", permitsName, ARGV[2], struct.pack("fI", ARGV[3], ARGV[1]))
    3. 复制代码

    至于为什么会产生这样的结果,会的小伙伴可以留言,或者过段时间我提个issue。

    及时当勉励 岁月不待人

    能看到这里的人呀,都是菁英。❤❤️❤️❤️❤

    非常感谢

    菁英们能看到这里,如果这个文章写得还不错,觉得有点东西的话 求点赞👍 求关注❤️ 求分享👥 对需要持续更新的我来说真的 非常有用!!

    如果本篇博客有任何错误,请批评指教,不胜感激 !

  • 相关阅读:
    GitOps 工具 Argo CD 实战教程
    六轴传感器 SH3001
    六、【React拓展】Component的2个效率问题
    一文总结MySQL的指令是如何工作的
    腾讯云什么服务器值得买?
    js--js实现基础排序算法
    599. 两个列表的最小索引总和
    吴恩达对话刘慈欣:让科幻更有勇气,让人工智能更有想象力
    leetcode:914. 卡牌分组(python3解法)
    Bus 消息总线
  • 原文地址:https://blog.csdn.net/BASK2311/article/details/127595454