利用Redisson封装一个限流工具并解析限流原理
(此篇文章以默认明白令牌桶算法原理,就没过多概述令牌桶算法过多细节)
首先封装了一个tryAcquire方法,三个参数分别代表限流器RRateLimiter的key以及每windowSize时间内最多limit个请求。例如tryAcquire("test", 3, 5)表示使用test限流器且每五秒钟最多允许3个请求。
以下是封装的限流工具的tryAcquire方法,执行逻辑是这样的,首先redissonClient会根据key去判断是否已经存在对应的限流器RRateLimiter对象,如果不存在则先创建一个,OVERALL参数表示整个系统的请求总量限流,还有一个PER_CLIENT的RateType指的是对不同的客户端单独统计限流。
@Override
public Boolean tryAcquire(String key, int limit, int windowSize) {
RRateLimiter rRateLimiter = redissonClient.getRateLimiter(key);
if (!rRateLimiter.isExists()) {
rRateLimiter.trySetRate(RateType.OVERALL, limit, windowSize, RateIntervalUnit.SECONDS);
}
return rRateLimiter.tryAcquire();
}
创建好对应的限流器对象后就会调用限流器的tryAcquire方法进行真正的限流处理。
以下是在Redis中创建的一个跟限流器对象相关的哈希表,里面记录了一些相关参数。
具体的限流操作是通过调用下面的这段lua脚本实现的,感兴趣的可以看一下试着理解一下。
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)
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
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
为了方便理解,我提供KEYS[1]到KEYS[5]对应的value如下图
ARGV[1]指的是请求的令牌数量,ARGV[2]是当前时间戳,ARGV[3]是一个随机数
接下来我试着解读一下这段lua脚本。
首先会定义几个参数:
- rate: 去前面创建的哈希表中获取对应的rate,表示在指定的时间间隔内允许的最大操作次数。
- interval:去前面创建的哈希表中获取对应的interval, 这里与前面我们传入的windowSize有一点区别,这里将时间转成了毫秒级别。用来表示速率限制的时间窗口。
- type: 表示限流策略
- valueName->{test}:value(就是后续创建的set的key)
- permitsName->{test}:permits(就是后续创建的zset的key)
- currentValue,表示当前剩余的令牌数
首先会根据以valueName为key去Redis中get剩余令牌数。
情况一:currentValue查不到,也就是在Redis中还没创建对应的set,说明此时是第一次请求限流操作
- 此时会先去Redis中创建一个set,该set的key就是valueName,value就是rate,也就是令牌总数。所以该set结构就是用来存储当前以valueName为key的令牌桶中剩余的令牌数量。
- 然后创建一个zset,该zset的key就是permitsName,score为当前时间戳,value是是由ARGV[3], ARGV[1]也就是请求的令牌数和随机值拼接成的一个结构。可以理解为记录了当前时间戳下请求的令牌数吧我觉得。而每一条记录就对应了一次申请令牌的操作,表示在某个时间下申请了多少个令牌。
- 然后就会将set中的value-1,应为前面的zset中有了一次操作的记录,等于说消耗掉了一个令牌。(为什么-1呢,因为默认每次操作都只申请一个令牌),最后返回nil。
情况二:currentValue有值,不管是多少,总之代表已经有了对应的set
- 首先会去zset中取出所有时间范围为(0,当前时间戳-令牌刷新时间)的操作记录,可以理解为取出所有已经过期的操作。
- 然后通过循环利用released记录一个总数。此时released的值就可以理解为当前可以清除掉的过期的操作总数。
- 所以如果此时released>0,则代表有过期操作需要清除掉释掉所放占用的令牌。就会去zset中remove掉所有过期的操作,并且刷新set中剩余的令牌数。
- 上述操作都是在更新令牌桶和操作记录,接下来会判断当前令牌桶中剩余的令牌数量能否满足此次请求的总令牌数,如果不够了,就会返回一个时间值,表示距离令牌下次刷新还有多久。如果足够,就会去set中做相应的令牌扣减同时去zset中记录操作。同时返回nil。
以上就是具体的操作流程,只有返回nil时才代表此次请求被放行了,否则代表此时流量过大被阻止通过。