前言
本文基于Redisson版本3.13.2
Redisson中的限流接口和延迟任务接口比起来,相对是比较简单的,里面涉及的redis数据结构只有两种:HASH和STRING。
介绍
HASH里存放的是限流参数,STRING里存放的是衍生key的值;衍生key用来设定在固定的时间周期内可以投放的许可数量,其过期时间就是限流参数里的interval;比如限流参数:10秒内限定5个许可,那么衍生key的过期时间为10秒,初始值为5,递减到0之后,许可发放完毕,后续请求都会被拒绝;
方法说明
RRateLimiter,里面的方法如下
void acquire() 从此RateLimiter处获取许可,直到获得许可为止。
void acquire(long permits) 从此RateLimiter获取一个指定值,直到获得一个指定值为止一直阻塞。
boolean tryAcquire() 仅在调用时可用时才获得许可。
boolean tryAcquire(long permits) 仅在调用时全部可用时才获取给定数目。
boolean tryAcquire(long permits, long timeout, TimeUnit unit) 仅在给定的等待时间内可用时,才获取给定的数量。
boolean tryAcquire(long timeout, TimeUnit unit)如果在给定的等待时间内可用,则从此RateLimiter获得许可。
boolean trySetRate(RateType mode, long rate, long rateInterval, RateIntervalUnit rateIntervalUnit) 初始化RateLimiter的状态,并将配置存储到Redis服务器。
使用
真正使用时,会用到两个方法
- 初始化限流数据【trySetRate】,将rate,interval,type三个参数写到一个HASH结构里;
- 尝试获得访问许可【tryAcquire】,递减一个衍生key,来分析是否可以获得许可;关于衍生key,文章末尾有个小彩蛋;
源码分析
Redisson实现这个功能,也使用了lua脚本,trySetRate的方法比较简单,只是将相关的三个参数写到redis中。我们着重介绍一下获取许可的方法;
参数:
KEYS[1]:限流key;
KEYS[2]:全局衍生key,所有的客户端共享;
KEYS[3]:随机衍生key,仅仅在redisson客户端层面共享;
ARGV[1]:请求的许可数量;
lua源码如下:
// hash结构中保存的是限流配置
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')
// 衍生key,记录当前剩余许可数量的key
local valueName = KEYS[2];
if type == '1' then
valueName = KEYS[3];
end;
// 返回剩余许可
local currentValue = redis.call('get', valueName);
// 如果还有许可
if currentValue ~= false then
// 如果请求的许可数量超过剩余数量,则返回超时时间【false】
if tonumber(currentValue) < tonumber(ARGV[1]) then
return redis.call('pttl', valueName);
else
// 否则递减许可数,返回true
redis.call('decrby', valueName, ARGV[1]);
return nil;
end;
else
assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount could not exceed defined rate');
// 如果currentValue为空,表示还未进行初始化,下面是初始化代码,用限流间隔时间来设置超时时间
redis.call('set', valueName, rate, 'px', interval);
redis.call('decrby', valueName, ARGV[1]);
return nil;
end;
流程图如下
关于衍生key的小彩蛋
lua脚本在redis集群上执行有局限性【哨兵模式没有】,在同一个脚本内不能跨slot进行读取,否则会报错:【ERR CROSSSLOT Keys in request don't hash to the same slot】。所以我们如果在脚本内需要访问多个redis key,需要一点点技巧。redis api中,允许我们指定用key中的一部分来计算所在的slot,用大括号圈起来。例如{test:limit}:value,这里用来计算slot的数据为【test:limit】。这样就可以保证限流key和衍生key都在一个slot中了。