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);
- 获取参数,如果都为空说明没有初始化
- 如果 rate 大于等于 ARGV[1] ,返回单次请求的令牌数不能超过一个时间窗口内产生的令牌数
- 如果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])); "
- 如果不是首次获取, 从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);"
- 如果回收后可用令牌数仍然不足 返回需要等待的时间, 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);
- 如果回收后足够
zadd {token}:permits 1705396021850 6966135962453115904_3
decrby {token}:value 3
大白话总结:
- 首先要进行令牌数初始化,如果未初始化,会直接返回“RateLimiter is not initialized”
- 将令牌数、过期时间和令牌桶type(全局还是单实例)三个参数放入一个key为“token”的hash中 数据 :hsetnx key value
- 校验: 如果许可令牌数(rate)小于ARGV1 ,返回单次请求的令牌数不能超过一个时间窗口内产生的令牌数
- 获取当前可用令牌数({token}:value),如果拿不到说明是首次获取,先设置当前key的可用令牌数 set {test}:value rate, 然后当前线程任务进入zset,可用令牌数({token}:value)做减操作。如果可以拿到,则先回收zsort中的当前时间戳过去interval秒之前的令牌,可用令牌数({token}:value)+之前所有请求的令牌数,然后zset移除,然后判断可用令牌数是否足够,足够则入队(sort set),{token}:value 减减,不可用就返回等待时间。