redis:redission令牌桶限流算法解析

redission令牌桶限流算法解析和实际模拟

以下有些内容是个人理解,如果有问题欢迎指正!!!

参考的原文链接:分布式服务限流实战,Redisson分布式限流器的使用及原理详解

使用:

RRateLimiter rateLimiter = redissonClient.getRateLimiter("token");

rateLimiter.trySetRate(RateType.OVERALL, 10, 30, RateIntervalUnit.SECONDS);

rateLimiter.tryAcquire(3);

trySetRate初始化内部实现:

    @Override
    public RFuture<Boolean> trySetRateAsync(RateType type, long rate, long rateInterval, RateIntervalUnit unit) {
        return commandExecutor.evalWriteNoRetryAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "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]);",
                Collections.singletonList(getRawName()), rate, unit.toMillis(rateInterval), type.ordinal());
    }

redis命令:
在这里插入图片描述
tryAcquire内部实现:

private <T> RFuture<T> tryAcquireAsync(RedisCommand<T> command, Long value) {
        byte[] random = new byte[8];
        ThreadLocalRandom.current().nextBytes(random);

        return commandExecutor.evalWriteAsync(getRawName(), 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;"

              + "assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount could not exceed defined rate'); "

              + "local currentValue = redis.call('get', valueName); "
              + "local res;"
              + "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('Bc0I', v);"
                          + "released = released + permits;"
                     + "end; "

                     + "if released > 0 then "
                          + "redis.call('zremrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval); "
                          + "if tonumber(currentValue) + released > tonumber(rate) then "
                               + "currentValue = tonumber(rate) - redis.call('zcard', permitsName); "
                          + "else "
                               + "currentValue = tonumber(currentValue) + released; "
                          + "end; "
                          + "redis.call('set', valueName, currentValue);"
                     + "end;"

                     + "if tonumber(currentValue) < tonumber(ARGV[1]) then "
                         + "local firstValue = redis.call('zrange', permitsName, 0, 0, 'withscores'); "
                         + "res = 3 + interval - (tonumber(ARGV[2]) - tonumber(firstValue[2]));"
                     + "else "
                         + "redis.call('zadd', permitsName, ARGV[2], struct.pack('Bc0I', string.len(ARGV[3]), ARGV[3], ARGV[1])); "
                         + "redis.call('decrby', valueName, ARGV[1]); "
                         + "res = nil; "
                     + "end; "
              + "else "
                     + "redis.call('set', valueName, rate); "
                     + "redis.call('zadd', permitsName, ARGV[2], struct.pack('Bc0I', string.len(ARGV[3]), ARGV[3], ARGV[1])); "
                     + "redis.call('decrby', valueName, ARGV[1]); "
                     + "res = nil; "
              + "end;"

              + "local ttl = redis.call('pttl', KEYS[1]); "
              + "if ttl > 0 then "
                  + "redis.call('pexpire', valueName, ttl); "
                  + "redis.call('pexpire', permitsName, ttl); "
              + "end; "
              + "return res;",
                Arrays.asList(getRawName(), getValueName(), getClientValueName(), getPermitsName(), getClientPermitsName()),
                value, System.currentTimeMillis(), random);
    }

涉及的参数
KEYS[1] token
KEYS[2] {token}:value (当前可用令牌数 key)
KEYS[3] {token}:实例id (type = 1 才需要,{token}:value和{token}:permits会加一个:实例id)
KEYS[4] {token}:permits(授权记录有序集合的 key)
ARGV[1] 2 本次请求的令牌数
ARGV[2] 1705396021850 System.currentTimeMillis()
ARGV[3] 6966135962453115904 ThreadLocalRandom.current().nextBytes(random);

  1. 获取参数,如果都为空说明没有初始化
    在这里插入图片描述
  2. 如果 rate 大于等于 ARGV[1] ,返回单次请求的令牌数不能超过一个时间窗口内产生的令牌数
  3. 如果currentValue == false,说明是首次获取令牌
    在这里插入图片描述
    设置可用令牌数
    在这里插入图片描述
    将三个参数 string.len(ARGV[3]), ARGV[3], ARGV[1] 揉成指定格式 Bc0I 的一个值,该方式可逆。后面要进行unpack,我在redis终端想执行这个命令但改来改去还是报错,没整出来,用java代码跑了一次逻辑读取了一个测试数据
"redis.call('zadd', permitsName, ARGV[2], struct.pack('Bc0I', string.len(ARGV[3]), ARGV[3], ARGV[1])); "

在这里插入图片描述

  1. 如果不是首次获取, 从sort set 获取数据, 统计可以回收的令牌数

4.1 获取score在当前时间戳之前的令牌,第二个参数是当前时间戳减去时间窗口 ARGV[2] - interval

zrangebyscore {token}:permits 0 1721203321000

在这里插入图片描述
4.2 获取打包时的请求令牌数, 进行累加得到历史的令牌数 released 模拟数据中下划线后面的数字

struct.unpack('fI', v)

4.3 如果released > 0, 移除这些令牌

zremrangebyscore {token}:permits 0 1721202999000

在这里插入图片描述
4.4 可用令牌数计算,currentValue + released(zset删除的历史请求的令牌数之和) > 初始化的令牌数, 算超了,就直接 初始化的令牌数 - zset里的个数;没超的话就currentValue = currentValue + released(zset删除的历史请求的令牌数之和) ;然后重新 set

  "if tonumber(currentValue) + released > tonumber(rate) then "
       "currentValue = tonumber(rate) - redis.call('zcard', permitsName); "
  "else "
       "currentValue = tonumber(currentValue) + released; "
  "end; "
  "redis.call('set', valueName, currentValue);"
  1. 如果回收后可用令牌数仍然不足 返回需要等待的时间, tonumber(currentValue) < tonumber(本次请求的令牌数ARGV[1])
    比如currentValue=3,请求的令牌数是5,那就不够了,会获取zset中排名第一的时间戳计算等待时间
zrangebyscore {test}:permits (1705396020850 1705396021850 withscores limit 0 1
//返回需要等待的时间
return tonumber(nearest[2]上个命令的结果) - (tonumber(ARGV[2]) - interval);

在这里插入图片描述

  1. 如果回收后足够
zadd {token}:permits 1705396021850 6966135962453115904_3  
decrby {token}:value 3

大白话总结:

  1. 首先要进行令牌数初始化,如果未初始化,会直接返回“RateLimiter is not initialized”
  2. 将令牌数、过期时间和令牌桶type(全局还是单实例)三个参数放入一个key为“token”的hash中 数据 :hsetnx key value
  3. 校验: 如果许可令牌数(rate)小于ARGV1 ,返回单次请求的令牌数不能超过一个时间窗口内产生的令牌数
  4. 获取当前可用令牌数({token}:value),如果拿不到说明是首次获取,先设置当前key的可用令牌数 set {test}:value rate, 然后当前线程任务进入zset,可用令牌数({token}:value)做减操作。如果可以拿到,则先回收zsort中的当前时间戳过去interval秒之前的令牌,可用令牌数({token}:value)+之前所有请求的令牌数,然后zset移除,然后判断可用令牌数是否足够,足够则入队(sort set),{token}:value 减减,不可用就返回等待时间。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值