应用场景
首先来说说使用场景,学习过redis的大概都知道有个叫缓存雪崩,击穿和穿透的问题。穿透(就是空吗)是指请求不存在的数据,这个时候我们可以设置为null或者使用布隆过滤器去解决这个问题。雪崩则是缓存的服务时效,这个就没什么好说的直接进行集群即可。击穿则是缓存key刚好过期了,于是大量的数据就一股脑的发送到后端造成后端压力过大。为了避免给后端过大的压力我们需要进行加锁的处理,可以加悲观锁或者乐观锁,具体则是看业务需求。前面讲了这么多为了铺垫下我们今天的主体——redisson限流器。这个限流器可以干什么呢?可以限流。那么具体是怎么限流呢?我们可以设置在规定时间内,最多可以发请求的数量。以下是代码分析。
代码解读
RRateLimiter rateLimiter = redisson.getRateLimiter("你需要限流的对象——可以是用户id这样独一无二的");
// 初始化
// 最大流速 = 每10秒钟产生1个令牌
rateLimiter.trySetRate(RateType.OVERALL, 1, 10, RateIntervalUnit.SECONDS);
//需要1个令牌
if(rateLimiter.tryAcquire(1)){
//TODO:Do something
}
`这个是一个最简单的限流器写法,我们就可以限制某个用户请求的次数了。
原理解析
这个限流器主要是有一个令牌池,在规定时间10秒内的令牌只有1(具体看上面方法的第二和三个参数,最后一个参数是秒)。
如果还可以拿到令牌我们就可以访问这个接口,不行就可以返回对应的错误信息给前端。
源码解析
下面是从该方法追到的源码。
private <T> RFuture<T> tryAcquireAsync(RedisCommand<T> command, Long value) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"local rate = redis.call('hget', KEYS[1], 'rate');" //1
+ "local interval = redis.call('hget', KEYS[1], 'interval');" //2
+ "local type = redis.call('hget', KEYS[1], 'type');" //3
+ "assert(rate ~= false and interval ~= false and type ~= false, 'RateLimiter is not initialized')" //4
+ "local valueName = KEYS[2];" //5
+ "if type == 1 then "
+ "valueName = KEYS[3];" //6
+ "end;"
+ "local currentValue = redis.call('get', valueName); " //7
+ "if currentValue ~= false then "
+ "if tonumber(currentValue) < tonumber(ARGV[1]) then " //8
+ "return redis.call('pttl', valueName); "
+ "else "
+ "redis.call('decrby', valueName, ARGV[1]); " //9
+ "return nil; "
+ "end; "
+ "else " //10
+ "redis.call('set', valueName, rate, 'px', interval); "
+ "redis.call('decrby', valueName, ARGV[1]); "
+ "return nil; "
+ "end;",
Arrays.<Object>asList(getName(), getValueName(), getClientValueName()),
value, commandExecutor.getConnectionManager().getId().toString());
}
因为这里只解析对应的对应的脚本文件首先先拿到对应的类型(type,若是1的话则设为全局,其他的话则是针对这个对象),时间间隔(interval)以及速率rate(单位内通过的令牌数量)。然后就拿到valueName,如果不存在就会走else,设置这个键值对,有对应的过期时间interval单位是毫秒,然后使用decrby减去1(因为第一次请求)。然后我们就可以判断令牌池里面的令牌数量是否充足。充足的话令牌数量减去1并且返回一个nill(为什么返回null可以自己去阅读前面的源码)。若是不充足则会通过'pttl'返回过期时间。