详解Redisson分布式限流的实现原理

2023-05-27 0 328

译者 | 宏碁云合作开发人员国联——xindoo

书名镜像:https://my.oschina.net/u/4526289/blog/7787170

全文: 责任编辑将详尽如是说下 RRateLimiter 的具体内容采用形式、同时实现基本原理除许多小常识。

他们现阶段沃苏什卡碰到一个操控性问题,他们有位间歇各项任务须要处置大批的统计数据,为了提高客运量,因此布署了很几台电脑,但那个各项任务在运转前须要从别的服务项目那拉取大批的统计数据,随着统计信息量的减小,假如同时几台电脑mammalian拉取统计数据,会对上游服务项目产生非常大的阻力。以后已经增加了FPS开闭,但难以补救,因为那个统计数据各项任务运转中只有不出 10% 的时间拉取统计数据,如果FPS开闭管制朱泽内,虽然软件产业总的允诺量控制住了,但各项任务客运量又拉高。假如开闭共振频率太高,多机mammalian的时候,还是有可能拖垮上游。因此现阶段惟一可取的软件系统是分布式系统开闭。

我现阶段是优先选择间接采用 Redisson 复本的 RRateLimiter 同时实现了分布式系统开闭,关于 Redission 可能许多人都略有听闻,它只不过是在 Redis 能力上构筑的合作开发库,除全力支持 Redis 的基础操作方式外,还PCB了戈德过滤器、分布式系统锁、开闭器…… 等辅助工具。今天没错的 RRateLimiter 及时处置其同时实现的开闭器。接下去责任编辑将详尽如是说下 RRateLimiter 的具体内容采用形式、同时实现基本原理除许多小常识,最后单纯聊聊我对分布式系统开闭下层基本原理的认知。

RRateLimiter 采用

teLimiter 第一类,间接看标识符实例。

RedissonClient redissonClient = Redisson.create();

RRateLimiter rateLimiter = redissonClient.getRateLimiter(“xindoo.limiter”);

rateLimiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.HOURS);

rateLimiter.trySetRate 是增设开闭模块,RateType 有三种,OVERALL 是自上而下开闭 ,PER_CLIENT 是单 Client 开闭(可以认为是FPS开闭),这里他们只讨论自上而下模式。而后面三个模块的作用是增设在多长时间窗口内(rateInterval+IntervalUnit),许可总量不超过多少(rate),上面标识符中我设

rateLimiter.acquire(1); // 申请1份许可,直到成功

boolean res = rateLimiter.tryAcquire(1, 5, TimeUnit.SECONDS); // 申请1份许可,假如5s内未申请到就放弃

采用起来还是很单纯的嘛,以上标识符中的三种形式都是同步调用,但 Redisson 还同样提供了异步方法 acquireAsync () 和 tryAcquireAsync (),采用其返回的 RF

RRateLimiter 的同时实现

接下去他们顺着 tryAcquire () 方法来看下它的同时实现形式,在 RedissonRateLimiter 类中,他们可以看到最下层的 tryAcquireAsync () 方法。

private RFuture tryAcquireAsync(RedisCommand command, Long value) {

byte[] random = new byte[8];

ThreadLocalRandom.current().nextBytes(random);

return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,

“——————————————————————————————————————”

+ “这里是一大段lua标识符”

+ “____________________________________”,

Arrays.asList(getRawName(), getValueName(), getClientValueName(), getPermitsName(), getClientPermitsName()),

value, System.currentTimeMillis(), random);

映入眼帘的是一大段 lua 标识符,只不过这段 Lua 标识符是开闭同时实现的核心,我把这段 lua 标识符摘出来,并加了许多注释,他们来详尽看下。

local rate = redis.call(“hget”, KEYS[1], “rate”) # 100

local interval = redis.call(“hget”, KEYS[1], “interval”) # 3600000

local type = redis.call(“hget”, KEYS[1], “type”) # 0

assert(rate ~= false and interval ~= false and type ~= false, “RateLimiter is not initialized”)

local valueName = KEYS[2] # {xindoo.limiter}:value 用来存储剩余许可数量

local permitsName = KEYS[4] # {xindoo.limiter}:permits 记录了所有许可发出的时间戳

# 假如是单实例模式,name信息后面就须要拼接上clientId来区分出来了

if type == “1” then

valueName = KEYS[3] # {xindoo.limiter}:value:b474c7d5-862c-4be2-9656-f4011c269d54

permitsName = KEYS[5] # {xindoo.limiter}:permits:b474c7d5-862c-4be2-9656-f4011c269d54

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

# ARGV permit timestamp random, random是一个随机的8字节

# 假如剩余许可不够,须要在res中返回下个许可须要等待多长时间

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 # 反之,记录到除多少许可,说明是初次采用或者以后已记录的信息已经过期了,就将配置rate写进去,并减少许可数

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

即便是加了注释,相信你还是很难一下子看懂这段标识符的,接下去我就以其在 Redis 中的统计数据存储形式,然辅以流程图让大家彻底了解其同时实现同时实现基本原理。

首先用 RRateLimiter 有位 name,在我标识符中是 xindoo.limiter,用那个作为 KEY 你就可以在 Redis 中找到一个 map,里面存储了 limiter 的工作模式 (type)、可数量 (rate)、时间窗口大小 (interval),这些都是在 limiter 创建时写入到的 redis 中的,在上面的 lua 标识符中也采用到了。

其次还俩很重要的 key,valueName 和 permitsName,其中在我的标识符同时实现中 valueName 是 {xindoo.limiter}:value ,它存储的是当前可用的许可数量。我标识符中 permitsName 的具体内容值是 {xindoo.limiter}:permits,它是一个 zset,其中存储了当前所有的许可授权记录(含有许可授权时间戳),其中 SCORE 间接采用了时间戳,而 VALUE 中包含了 8 字节的随机值和许可的数量,如下图:

详解Redisson分布式限流的实现原理

详解Redisson分布式限流的实现原理

{xindoo.limiter}:permits 那个 zset 中存储了所有的历史授权记录,直到了这些信息,相信你也就认知了 RRateLimiter 的同时实现基本原理,他们还是将上面的那大段 Lua 标识符的流程图绘制出来,整个执行的流程会更直观。

详解Redisson分布式限流的实现原理

看到这大家应该能认知这段 Lua 标识符的逻辑了,可以看到 Redis 用了多个字段来存储开闭的信息,也有各种各样的操作方式,那 Redis 是如何保证在分布式系统下这些开闭信息统计数据的一致性的?答案是不须要保证,在那个场景下,信息天然是一致性的。原因是 Redis 的单进程统计数据处置模型,在同一个 Key 下,所有的 eval 允诺都是串行的,所有不须要考虑统计数据mammalian操作方式的问题。在这里,Redisson 也采用了 HashTag,保证所有的开闭信息都存储在同一个 Redis 实例上。

RRateLimiter 采用时小常识

了解了 RRateLimiter 的下层基本原理,再结合 Redis 自身的特性,我想到了 RRateLimiter 采用的几个局限点 (问题点)。

RRateLimiter 是非公平开闭器

那个是我查阅资料得知,并且在自己标识符实践的过程中也得到了验证,具这种情况,假如不能接受就得考虑用某些形式尽可能让其变公平。

Rate 不要增设太大

从 RRateLimiter 的同时实现基本原理你也看出了,它采用的是滑动窗口的模式来开闭的,而且记录了所有的许可授权信息,因此假如你增设的 Rate 值过大,在 Redis 中存储的信息 (permitsName 对应的 zset) 也就越多,每次执行那段 lua 脚本的操控性也就越差,这对 Redis 实例也是一种阻力。个人建议假如你是想增设较大的开闭共振频率,倾向于小 Rate + 小时间窗口的形式,而且这种增设形式允诺也会更均匀许多。

开闭的上限取决于 Redis 单实例的操控性

从基本原理上看,RRateLimiter 在 Redis 上所存储的信息都必须在一个 Redis 实例上,因此它的开闭 QPS 的上限是 Redis 单实例的上限,比如你 Redis 实例是 1w QPS,你想用 RRateLimiter 同时实现一个 2w QPS 的开闭器,必然同时实现不了。那有没有突破 Redis 单实例操控性上限的形式?单开闭器肯定是同时实现不了的,他们可以拆分多个开闭器,比如我搞 10 个开闭器,名词用不一样的,然后每台电脑随机使用一个开闭器开闭,实际的流量不就被分散到不同的开闭器上了吗,总的开闭上线不也就上来了。

分布式系统开闭的本质

分布式系统开闭的本质实际上是协同,协同的本质是信息交换,信息交换最重要的的是信息的准确性和一致性。 更单纯粗暴认知,分布式系统开闭的本质基本原理只不过还是分布式系统统计数据一致性的基本原理,而开闭只是统计数据结果的一种决策。因此只要以任何形式能让信息同步,且保证信息的正确性就可以同时实现一个分布式系统开闭器了,这是我认知的本质思路。

只不过从上面的 RRateLimiter 的同时实现基本原理也可以看出来,它不是存储了许多信息吗!那我不用 Redis,而是采用 mysql 行不行。实际肯定是可以的,只要将的上面 Lua 标识符中的所有操作方式都放到一个事务里,且事务的级别改成串行化,依旧能同时实现 RRateLimiter 同样的功能。假如你具备 Mysql 相关知识的话,肯定也能基于 Mysql 将 RRateLimiter 的 API PCB出来,但是PCB出来的开闭器,其开闭的上限就取决于 Mysql 实例的操控性上限。

最近 chatGPT 比较火,我也问了下它对分布式系统开闭本质基本原理的认知,下面是它的回答,大家觉得怎么样?

分布式系统开闭的本质基本原理是通过在分布式系统系统中共享开闭状态来管制系统中单位时间内的允诺数量,从而避免系统因流量过大而崩溃。

这是通过采用许多共享的存储组件,如统计数据库,缓存,分布式系统锁等来同时实现的。在每次允诺时,系统会检查当前的允诺数量是否超过了预先设定的管制,假如超过了管制,允诺就会被拒绝;假如未超过管制,允诺就会被允许。

通过采用分布式系统开闭技术,系统可以在高mammalian情况下保持稳定的操控性,并避免因流量过大而导致的系统崩溃。

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务