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

本文分享自华为云社区《详解 Redisson 分布式限流的实现原理》,作者: xindoo。

我们目前在工作中遇到一个性能问题,我们有个定时任务需要处理大量的数据,为了提升吞吐量,所以部署了很多台机器,但这个任务在运行前需要从别的服务那拉取大量的数据,随着数据量的增大,如果同时多台机器并发拉取数据,会对下游服务产生非常大的压力。之前已经增加了单机限流,但无法解决问题,因为这个数据任务运行中只有不到 10% 的时间拉取数据,如果单机限流限制太狠,虽然集群总的请求量控制住了,但任务吞吐量又降下来。如果限流阈值太高,多机并发的时候,还是有可能压垮下游。 所以目前唯一可行的解决方案就是分布式限流

我目前是选择直接使用 Redisson 库中的 RRateLimiter 实现了分布式限流,关于 Redission 可能很多人都有所耳闻,它其实是在 Redis 能力上构建的开发库,除了支持 Redis 的基础操作外,还封装了布隆过滤器、分布式锁、限流器…… 等工具。今天要说的 RRateLimiter 及时其实现的限流器。接下来本文将详细介绍下 RRateLimiter 的具体使用方式、实现原理还有一些注意事项,最后简单谈谈我对分布式限流底层原理的理解。

RRateLimiter 使用

RRateLimiter 的使用方式异常的简单,参数也不多。只要创建出 RedissonClient,就可以从 client 中获取到 RRateLimiter 对象,直接看代码示例。

RedissonClientredissonClient = Redisson.create();RRateLimiterrateLimiter = redissonClient.getRateLimiter("xindoo.limiter");rateLimiter.trySetRate(RateType.OVERALL,100, 1, RateIntervalUnit.HOURS); 

rateLimiter.trySetRate 就是设置限流参数,RateType 有两种,OVERALL 是全局限流 ,PER_CLIENT 是单 Client 限流(可以认为就是单机限流),这里我们只讨论全局模式。而后面三个参数的作用就是设置在多长时间窗口内(rateInterval+IntervalUnit),许可总量不超过多少(rate),上面代码中我设置的值就是 1 小时内总许可数不超过 100 个。然后调用 rateLimiter 的 tryAcquire () 或者 acquire () 方法即可获取许可。

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

使用起来还是很简单的嘛,以上代码中的两种方式都是同步调用,但 Redisson 还同样提供了异步方法 acquireAsync () 和 tryAcquireAsync (),使用其返回的 RFuture 就可以异步获取许可。

RRateLimiter 的实现

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

private <T> RFuture<T> tryAcquireAsync(RedisCommand<T> command, Long value) {byte[] random = newbyte[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 localinterval = redis.call("hget", KEYS[1], "interval")  # 3600000localtype = redis.call("hget", KEYS[1], "type")  # 0
assert(rate ~= falseandinterval ~= falseandtype ~= false, "RateLimiter is not initialized")
local valueName = KEYS[2]      # {xindoo.limiter}:value 用来存储剩余许可数量local permitsName = KEYS[4]    # {xindoo.limiter}:permits 记录了所有许可发出的时间戳 # 如果是单实例模式,name信息后面就需要拼接上clientId来区分出来了iftype == "1"thenvalueName = KEYS[3]        # {xindoo.limiter}:value:b474c7d5-862c-4be2-9656-f4011c269d54permitsName = KEYS[5]      # {xindoo.limiter}:permits:b474c7d5-862c-4be2-9656-f4011c269d54end# 对参数校验 
assert(tonumber(rate) >= tonumber(ARGV[1]), "Requested permits amount could not exceed defined rate")
# 获取当前还有多少许可 local currentValue = redis.call("get", valueName) 
local res
# 如果有记录当前还剩余多少许可 if currentValue ~= falsethen# 回收已过期的许可数量local expiredValues = redis.call("zrangebyscore", permitsName, 0, tonumber(ARGV[2]) - interval)local released = 0for i, v in ipairs(expiredValues) dolocal random, permits = struct.unpack("Bc0I", v)released = released + permitsend# 清理已过期的许可记录if released > 0thenredis.call("zremrangebyscore", permitsName, 0, tonumber(ARGV[2]) - interval)if tonumber(currentValue) + released > tonumber(rate) thencurrentValue = tonumber(rate) - redis.call("zcard", permitsName)elsecurrentValue = tonumber(currentValue) + releasedendredis.call("set", valueName, currentValue)end# ARGV  permit  timestamp  random, random是一个随机的8字节# 如果剩余许可不够,需要在res中返回下个许可需要等待多长时间 if tonumber(currentValue) < tonumber(ARGV[1]) thenlocal firstValue = redis.call("zrange", permitsName, 0, 0, "withscores")res = 3 + interval - (tonumber(ARGV[2]) - tonumber(firstValue[2]))elseredis.call("zadd", permitsName, ARGV[2], struct.pack("Bc0I", string.len(ARGV[3]), ARGV[3], ARGV[1]))# 减小可用许可量 redis.call("decrby", valueName, ARGV[1])res = nilendelse# 反之,记录到还有多少许可,说明是初次使用或者之前已记录的信息已经过期了,就将配置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
endlocal ttl = redis.call("pttl", KEYS[1])
# 重置if ttl > 0thenredis.call("pexpire", valueName, ttl)redis.call("pexpire", permitsName, ttl)
endreturn 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 字节的随机值和许可的数量,如下图:

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

看到这大家应该能理解这段 Lua 代码的逻辑了,可以看到 Redis 用了多个字段来存储限流的信息,也有各种各样的操作,那 Redis 是如何保证在分布式下这些限流信息数据的一致性的?答案是不需要保证,在这个场景下,信息天然就是一致性的。原因是 Redis 的单进程数据处理模型,在同一个 Key 下,所有的 eval 请求都是串行的,所有不需要考虑数据并发操作的问题。在这里,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 封装出来,但是封装出来的限流器,其限流的上限就取决于 Mysql 实例的性能上限。

最近 chatGPT 比较火,我也问了下它对分布式限流本质原理的理解,下面是它的回答,大家觉得怎么样?

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

这是通过使用一些共享的存储组件,如数据库,缓存,分布式锁等来实现的。在每次请求时,系统会检查当前的请求数量是否超过了预先设定的限制,如果超过了限制,请求就会被拒绝;如果未超过限制,请求就会被允许。

通过使用分布式限流技术,系统可以在高并发情况下保持稳定的性能,并避免因流量过大而导致的系统崩溃。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/49888.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AI日报|哈佛“AI教授”即将上线;首个生成式AI技能专业证书来了;电话推销员很烦?AI帮你“制裁”他

今日值得关注的人工智能新动态&#xff1a; 将GPT-4用在课程设计中 哈佛大学“AI教授”即将上线 微软推出首个生成式AI技能专业证书 纽约州议会&#xff1a;伤害或羞辱他人的deepfake是非法的 阿诺德施瓦辛格&#xff1a;《终结者》中的AI已成现实 AI诊断“老年痴呆”&…

甜品网站界面

最开始就是logo部分和导航栏部分 logo部分就是用的div里面写img然后给浮动就可以了 第二个是导航栏部分 用的也是无序标签 代码如下&#xff1a; 轮播图部分 没做出来效果 甜蜜约会品牌简介 红色框框以外的我用的是 div img 和 p标签做的 这些很简单 就不展示代码了 红色边…

教你简单学会用Python画长草颜团子

相信大家都知道可爱的长草颜团子&#xff0c;它应该在很多人的表情包中占有一席之地&#xff0c;那么就今天我们试着用Python的turtle模板来画一个长草颜团子吧&#xff01; 这么可爱的团子谁不想要试着自己画出来呢哈哈哈 源码如下&#xff1a; #8X_I import turtle as t t.…

鱼香肉丝(集锦)

1&#xff1a; 鱼香肉丝的简单制作 2 &#xff1a;鱼香肉丝的来历与制作 3&#xff1a; 鱼香肉丝制作 4&#xff1a;鱼香肉丝 1 鱼香肉丝的简单制作 作者&#xff1a; 时间&#xff1a; 2006-1-17 10:17:00 摘自&#xff1a;http://www.517sc.com/food/xyz/17_54_51330.html 材…

用 Python 画如此漂亮的插图 ,So easy

人生苦短&#xff0c;快学Python&#xff01; 今天我们进行一次实战案例分享&#xff0c;以全球预期寿命与人均 GPD数据为例&#xff0c;写一篇 Python 中漂亮散点图的快速指南。除了正常的数据清洗/处理、还会进行简单的统计分析&#xff0c;实现数据处理-统计分析-可视化一条…

做个合格的吃货~Python爬取全国火锅店,并利用地图可视化展示~

导语&#xff1a;天越来越冷啦~ 前段时间又刮起了入冬四件套&#xff08;烤红薯、热奶茶、糖炒栗子、糖霜山楂&#xff09;的热风~ 小编也紧跟着潮流下班兴冲冲的跑去买~&#xff08;附近店面的排队的人实在是太多了~风还大&#x1f637;&#x1f637;&#xff09; 一到手…

美食杰项目(七)菜谱大全

本文目录 前言&#xff1a;1.具体样式2.实现的具体功能和代码思路3.element ui具体样式的网址4.相关代码5.总结&#xff1a; 前言&#xff1a; 本文给大家讲的是美食杰项目中菜谱大全项目的具体样式&#xff0c;代码思路和具体代码&#xff0c;希望能帮助到你 1.具体样式 2.实…

鱼香肉丝里到底有没有鱼?

鱼香肉丝&#xff0c;算是我最爱的一道菜了&#xff0c;无论饭店大小&#xff0c;他都是我首先就要找的菜&#xff0c;可谓痴迷&#xff0c;但是鱼香肉丝里到底有没有鱼&#xff1f;这是个千古之谜&#xff0c;这篇来自三个料理人的文章《千古之谜&#xff0c;鱼香肉丝里到底有…

Python爬虫:简单爬取粤菜菜谱

项目场景&#xff1a; 简单爬取粤菜菜谱。 实现思路&#xff1a; 访问主页&#xff0c;获取每个菜品的菜名、图片、详情页面网址。 访问上一步中获得的所有详情页面&#xff0c;获取工艺、口味、时间、主料、辅料信息。 清洗所获得的数据。 保存至本地文件。 实现过程&a…

文心一言的魔性作图,我头都笑掉了...

这几天看到网友们用文心一言作的图&#xff0c;看了后我都愣住了。。。 AI 作画 -- 三得利乌龙茶 AI 作画 -- 娃娃菜 AI 作画 -- 车水马龙 AI 作画 -- 驴肉2火烧 AI 作画 -- 唐伯虎点秋香 AI 作画 -- 鱼香肉丝 AI 作画 -- 胸有成竹 AI 作画 -- 夫妻肺片 AI 作画 -- 红烧狮子头 …

使用chrome浏览器插件postman模拟post、get请求

使用chrome浏览器插件postman模拟post、get请求 postman为chrome浏览器的一个插件&#xff0c;用来模拟post请求&#xff0c;get请求等。可以在chrome浏览器里安装插件&#xff08;前提是你得访问了Google应用商店&#xff09;。 如果不能访问Google&#xff0c;那个下载一个p…

chrome浏览器无法开启同步功能 request cancel

找了很多亲测最新100版本可用&#xff01;&#xff01;&#xff01; 步骤 1、从下面链接提取google插件【Chrome-Sync-Helper】 链接: https://pan.baidu.com/s/1FTxrQ-IRjRmYdW5HcNateA 提取码: htga &#xff08;如链接失效&#xff0c;请留言反馈&#xff01;&#xff09; …

Chrome 添加【微信 / QQ】内置浏览器(解决 “请在微信客户端打开链接” 提示)

前言 有些链接&#xff0c;是需要在微信客户端内才能打开的&#xff0c;那么想在 PC 端的浏览器上打开&#xff0c;怎么办呢&#xff1f; UA 不明白的话先不用管&#xff0c;继续往下看。 【安卓QQ内置浏览器UA】 Mozilla/5.0 (Linux; Android 5.0; SM-N9100 Build/LRX21V…

chrome浏览器控制台发送post请求

谷歌浏览器&#xff0c;点击F12&#xff0c;在控制台中输入下面代码&#xff0c;直接回车即可&#xff1a;&#xff08;需要修改一下Admin-Token的值即可&#xff09; 设置访问的Controller路径&#xff0c;“http://127.0.0.1:8090/api/dwStandard/superUploadBigFile” met…

和 if else说再见,SpringBoot 这样做参数校验才足够优雅!

大家好&#xff0c;我是老赵! 一、概述 当我们想提供可靠的 API 接口&#xff0c;对参数的校验&#xff0c;以保证最终数据入库的正确性&#xff0c;是 必不可少 的活。比如下图就是 我们一个项目里 新增一个菜单校验 参数的函数&#xff0c;写了一大堆的 if else 进行校验&…

全球诺贝尔奖得主最多的30所大学

自1901年以来&#xff0c;诺贝尔奖得主全球最多的30所大学&#xff0c;这些大学堪称是真正的世界一流大学。世界一流大学的指标很多&#xff0c;但是有一项重要指标不可缺失&#xff0c;那就是至少有10位以上诺贝尔奖得主。以下是笔者根据维基百科整理的1901年至2018年间&#…

【娜家花园养花小记】

种花的话&#xff0c;看花开花落&#xff0c;经历寒冬酷暑&#xff0c;都是生命的一个体验的过程。月季花很坚强&#xff0c;酷暑来了&#xff0c;寒冬来了&#xff0c;它休眠一下。然后在其他时间呢&#xff0c;它就尽情的拿生命去绽放。种花更多的感受它带给你的快乐。带给你…

一生必看的 100 幅世界名画

智慧与美&#xff0c;是我之最爱。 从早期的叙事性绘画&#xff0c;直至后期更加侧重抒情与抽象的现代派绘画。希望这篇用心的长文&#xff0c;可以成为你开启艺术之们的钥匙。如果有幸有一幅画面&#xff0c;能够触及你内心柔软的角落抑或隐秘的激情&#xff0c;也请你静下心来…

2022-09-11 stonedb-宣讲-第二讲-一条SQL在Tianmu引擎中的运行

摘要: 记录列存储引擎第二讲的绸缪规划。 宣传语: 标题: 一条查询SQL在Tianmu引擎中的代码实现 宣讲语: 你是否只读过数据库理论的书籍, 但是一遇到代码就头疼呢? 你是否只会在理论上和数学公式上推导数据库内核, 但是从没亲自做过数据库内核的实现呢? 你是否对于数据库…

「上帝粒子」发现10周年

来源&#xff1a;FUTURE | 远见 选编&#xff1a;闵青云 2012年7月4日&#xff0c;欧洲核子研究中心&#xff08;CERN&#xff09;宣布发现了「上帝粒子」&#xff08;希格斯玻色子&#xff09;。希格斯玻色子是粒子物理学标准模型预言的一种玻色子&#xff0c;正是它的存在&…