Redisson限流算法
引入依赖
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.12.3</version>
</dependency>
建议版本使用3.15.5以上
使用
这边写了一个demo示例,定义了一个叫 “LIMITER_NAME” 的限流器,设置每1秒钟生成3个令牌。
public static void main(String[] args) throws UnsupportedEncodingException, InterruptedException {Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");RedissonClient redisson = Redisson.create(config);String key = "test:rateLimiter01";RRateLimiter rateLimiter = redisson.getRateLimiter(key);boolean trySetRate = rateLimiter.trySetRate(RateType.OVERALL, 3, 1, RateIntervalUnit.SECONDS);for (int i = 0; i < 30; i++) {boolean b = rateLimiter.tryAcquire(1,100, TimeUnit.MILLISECONDS);System.out.println(Thread.currentThread().getName() + " testRate第" + (i + 1) + "次:" + b);}// redisson.shutdown();}

redis的数据结构
PRateLimiter接口的实现类几乎都在RedissonRateLimiter上,我们看看前面调用PRateLimiter方法时,这些方法的对应源码实现。
接下来下面就着重讲讲限流算法中,一共用到的3个redis key。
key1 Hash结构
就是前面trySetRate设置的hash key。按照之前限流器命名“LIMITER_NAME”,这个名字就是LIMITER_NAME。一共有3个值。
1,rate:代表速率
2,interval:代表多少时间内产生的令牌
3,type:代表时单机还是集群。

key 2: Zset结构
zset记录获取令牌的时间戳,用于时间对比,redis key的名字是{LIMITER_NAME}:permits。下面讲讲zset中每个元素的member和score
- member:包含两个内容,
1)一段8位随机字符串,为了唯一标志性当次获取令牌
2)数字,即当次获取令牌的数量。不过这些都是压缩后存储在redis中的,在工具上看时会发现乱码。 - score:记录获取令牌的时间戳,eg:1709026371728 转成日期是2024-02-27 17:32:51

key 3 string 结构
记录当前令牌桶中剩余的令牌数。redis key的名字是{LIMITER_NAME}:value。

算法源码分析
trySetRate尝试设置
尝试设置是:当没有对应的key的时候设置,如果已经有值了,就不做任何处理。对应实现类中的源码是:
/*** Initializes RateLimiter's state and stores config to Redis server.* * @param mode - rate mode* @param rate - rate* @param rateInterval - rate time interval* @param rateIntervalUnit - rate time interval unit* @return {@code true} if rate was set and {@code false}* otherwise*/boolean trySetRate(RateType mode, long rate, long rateInterval, RateIntervalUnit rateIntervalUnit);@Overridepublic boolean trySetRate(RateType type, long rate, long rateInterval, RateIntervalUnit unit) {return get(trySetRateAsync(type, rate, rateInterval, unit));}@Overridepublic RFuture<Boolean> trySetRateAsync(RateType type, long rate, long rateInterval, RateIntervalUnit unit) {return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"redis.call('hsetnx', KEYS[1], 'rate', ARGV[1]);"+ "redis.call('hsetnx', KEYS[1], 'interval', ARGV[2]);"+ "return redis.call('hsetnx', KEYS[1], 'type', ARGV[3]);",Collections.singletonList(getName()), rate, unit.toMillis(rateInterval), type.ordinal());}
核心是lua脚本,摘出来如下:
redis.call('hsetnx', KEYS[1], 'rate', ARGV[1]);
redis.call('hsetnx', KEYS[1], 'interval', ARGV[2]);
return redis.call('hsetnx', KEYS[1], 'type', ARGV[3]);
发现基于一个hash类型的redis key设置了3个值。
不过这里的命令是hsetnx,redis hsetnx命令用于哈希表中不存在的字段赋值。
如果哈希表不存在,一个新的哈希表被创建并进行hset操作。
如果字段已经存在hash表中,操作无效。
如果key不存在,一个新的哈希表被创建并执行hsetnx命令。
这意味着,这个方法只能做配置初始化,如果后期想要修改配置参数,该方法并不会生效。我们来看看另外一个方法。
setRete重新设置
重新设置是,不管该key之前有没有用,一切清空回到初始化,重新设置。对应类中实现类的源码是。
/*** Updates RateLimiter's state and stores config to Redis server.** @param mode - rate mode* @param rate - rate* @param rateInterval - rate time interval* @param rateIntervalUnit - rate time interval unit*/void setRate(RateType mode, long rate, long rateInterval, RateIntervalUnit rateIntervalUnit);public RFuture<Void> setRateAsync(RateType type, long rate, long rateInterval, RateIntervalUnit unit) {return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,"redis.call('hset', KEYS[1], 'rate', ARGV[1]);"+ "redis.call('hset', KEYS[1], 'interval', ARGV[2]);"+ "redis.call('hset', KEYS[1], 'type', ARGV[3]);"+ "redis.call('del', KEYS[2], KEYS[3]);",Arrays.asList(getName(), getValueName(), getPermitsName()), rate, unit.toMillis(rateInterval), type.ordinal());}
核心是lua脚本
redis.call('hset', KEYS[1], 'rate', ARGV[1]);
redis.call('hset', KEYS[1], 'interval', ARGV[2]);
redis.call('hset', KEYS[1], 'type', ARGV[3]);
redis.call('del', KEYS[2], KEYS[3]);
上述参数如下
- KEYS[1]:hash key name
- KEYS[2]:string(value) key name
- KEYS[3]:zset(permits) key name
- ARGV[1]:rate
- ARGV[2]:interval
- ARGV[3]:type
通过这个lua的逻辑,就能看出直接用的是hset,会直接重置配置参数,并且同时会将已产生数据的string(value)、zset(ppermis)两个key删掉。是一个彻底的重置方法。
这里回顾一下trySetRate和setRate(注意setRate在3.12.3这个版本是没有这个方法的),在限流器不变的场景下,我们可以多次调用trySetRate,但是不能调用setRate。因为每调用一次,redis.call(‘del’,keys[2],keys[3])就会将限流器中数据清空,也就达不到限流功能。
设置过期时间
有没有发现前面针对限流器设置的3个key,都没有设置过期时间。PRateLimiter接口设计上,将设置过期时间单独拎出来了。
// 设置过期boolean expire(long var1, TimeUnit var3);// 清除过期(永不过期)boolean clearExpire();
这个方法是针对3个key一起设置过期时间。
获取令牌(核心)tryAcquire
获取令牌
private <T> RFuture<T> tryAcquireAsync(RedisCommand<T> command, Long value) {byte[] random = getServiceManager().generateIdArray();return commandExecutor.evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,"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); "+ "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;"+ "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 "+ "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;",Arrays.asList(getRawName(), getValueName(), getClientValueName(), getPermitsName(), getClientPermitsName()),value, System.currentTimeMillis(), random);}
我们先看看执行lua脚本时,所有要传入的参数内容:
KEYS[1]: hash key name
KEYS[2]:全局string(value) key name
KEYS[3]:单机string(value) key name
KEYS[4]:全局zset(permits) key name
KEYS[5]:单机zset(permits) key name
ARGV[1]:当前请求令牌数量
ARGV[2]:当前时间
ARGV[3]:8位随机字符串
然后我们再将其中lua部分提取出来,我再根据自己的理解
-- rate:间隔时间内产生令牌数量
-- interval:间隔时间
-- type:类型:0-全局限流;1-单机限
local rate = redis.call('hget', KEYS[1], 'rate');
local interval = redis.call('hget', KEYS[1], 'interval');
local type = redis.call('hget', KEYS[1], 'type');
-- 如果3个参数存在空值,错误提示初始化未完成
assert(rate ~= false and interval ~= false and type ~= false, 'RateLimiter is not initialized')
local valueName = KEYS[2];
local permitsName = KEYS[4];
-- 如果是单机限流,在全局key后拼接上机器唯一标识字符
if type == '1' thenvalueName = KEYS[3];permitsName = KEYS[5];
end ;
-- 如果:当前请求令牌数 < 窗口时间内令牌产生数量,错误提示请求令牌不能超过rate
assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount could not exceed defined rate');
-- currentValue = 当前剩余令牌数量
local currentValue = redis.call('get', valueName);
-- 非第一次访问,存储剩余令牌数量的 string(value) key 存在,有值(包括 0)
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-- lua struct的pack/unpack方法,可以理解为文本压缩/解压缩方法local random, permits = struct.unpack('fI', v);released = released + permits;end ;-- 移除 zset(permits) 中过期的令牌签发记录-- 将过期令牌回收回来,重新更新剩余令牌数量if released > 0 thenredis.call('zremrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval);currentValue = tonumber(currentValue) + released;redis.call('set', valueName, currentValue);end ;-- 如果 剩余令牌数量 < 当前请求令牌数量,返回推测可以获得所需令牌数量的时间-- (1)最近一次签发令牌的释放时间 = 最近一次签发令牌的签发时间戳 + 间隔时间(interval)-- (2)推测可获得所需令牌数量的时间 = 最近一次签发令牌的释放时间 - 当前时间戳-- (3)"推测"可获得所需令牌数量的时间,"推测",是因为不确定最近一次签发令牌数量释放后,加上到时候的剩余令牌数量,是否满足所需令牌数量if tonumber(currentValue) < tonumber(ARGV[1]) thenlocal nearest = redis.call('zrangebyscore', permitsName, '(' .. (tonumber(ARGV[2]) - interval), '+inf', 'withscores', 'limit', 0, 1);return tonumber(nearest[2]) - (tonumber(ARGV[2]) - interval);-- 如果 剩余令牌数量 >= 当前请求令牌数量,可直接记录签发令牌,并从剩余令牌数量中减去当前签发令牌数量elseredis.call('zadd', permitsName, ARGV[2], struct.pack('fI', ARGV[3], ARGV[1]));redis.call('decrby', valueName, ARGV[1]);return nil;end ;-- 第一次访问,存储剩余令牌数量的 string(value) key 不存在,为 null,走初始化逻辑
elseredis.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 ;
注意事项
trySetRate是基于hsetnx,这意味着一旦设置过Hash中的限流参数,就没法修改。那么如何保证可以修改?
1,当需要修改时,执行setRate,但最好注意执行时间,因为涉及到zset,string两个key,可能会影响当前的限流窗口。
2,给限流设置过期时间expire,当到达时间后,自行删除。注意的是:
expire 要在执行tryAcquire之后执行,才能保证3个key都成功设置过期时间。否则可能只有hash的key才有设置过期时间。
相关文章:
Redisson限流算法
引入依赖 <dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.12.3</version> </dependency>建议版本使用3.15.5以上 使用 这边写了一个demo示例,定…...
GPT与MBR:硬盘分区表格式的革新与区别
概述 在计算机存储领域,硬盘分区是管理数据和操作系统部署的基础。两种广泛使用的分区表格式——MBR(Master Boot Record)和GPT(GUID Partition Table),各自代表了不同的技术阶段和发展需求。本文将详细介…...
机器学习-1
文章目录 前言机器学习基本定义 练习题 前言 在本片开始将为大家介绍机器学习相关的知识点。 机器学习基本定义 夏天,我们通常会去水果店里买西瓜,我们看到一个根蒂蜷缩、敲起来声音浑浊的青绿色的西瓜,我们提着西瓜就去结账了,…...
Stream流详解
当我们对一个集合中的元素进行多次过滤应该怎样做? 下面看一个案例 按照下面的要求完成集合的创建和遍历 创建一个集合,存储多个字符串元素 把集合中所有以"张"开头的元素存储到一个新的集合 把"张"开头的集合中的长度为3的元素存储到一个新…...
javaweb学习(day05-TomCat)
一、介绍 1 官方文档 地址: https://tomcat.apache.org/tomcat-8.0-doc/ 2 WEB 开发介绍 2.1 WEB 在英语中 web 表示网/网络资源(页面,图片,css,js)意思,它用于表示 WEB 服务器(主机)供浏览器访问的资源 2.2 Web 资源 WEB 服务器 ( 主机 ) 上供外界访问的 …...
【Unity】构建简单实用的年份选择器(简单原理示范)
在许多应用程序和游戏中,年份选择是一个常见的需求。无论是在日历应用程序中查看事件,还是在历史类游戏中选择时间段,年份选择器都是用户体验的重要组成部分,下面实现一个简易的年份选择器。 一、效果预览: 目录 一、…...
LeetCode 2120.执行所有后缀指令
现有一个 n x n 大小的网格,左上角单元格坐标 (0, 0) ,右下角单元格坐标 (n - 1, n - 1) 。给你整数 n 和一个整数数组 startPos ,其中 startPos [startrow, startcol] 表示机器人最开始在坐标为 (startrow, startcol) 的单元格上。 另给你…...
租赁小程序|租赁系统|租赁软件开发带来高效运营
随着社会的不断发展和科技的不断进步,越来越多的企业开始关注设备租赁业务。设备租赁作为一种短期使用设备的方式,为企业提供了灵活和成本节约的优势。针对设备租赁业务的管理和提升企业竞争力的需求,很多企业选择定制开发设备租赁系统。本文…...
大数据集群管理软件 CDH、Ambari、DataSophon 对比
文章目录 引言工具介绍CDHAmbariDataSophon 对比分析 引言 大数据集群管理方式分为手工方式和工具方式,手工方式一般指的是手动维护平台各个组件,工具方式是靠大数据集群管理软件对集群进行管理维护。本文针对于常见的方法和工具进行比较,帮助…...
插值、逼近、拟合、光顺
插值 插值(Interpolation)是数学和计算科学中的一个重要概念,它指的是通过已知的一系列数据点,构造一个函数或曲线,并据此估计未知数据点的值。这个过程通常发生在已知数据点之间,用于预测或估算在这些已知…...
Java单元测试 - mock静态方法
文章目录 1. mock 静态方法2. 升级 maven 依赖3. 示例 1. mock 静态方法 mockito 在 3.4.0 版本之后,开始支持 mock static method。 2. 升级 maven 依赖 <dependency><groupId>org.mockito</groupId><artifactId>mockito-core</artif…...
Unity使用PlayableAPI 动态播放动画
1.初始化animator,创建Playable图,创建动画Playable private void InitAnimator(GameObject headGo) {if (headGo){_headAnimator headGo.GetComponent<Animator>();if (_headAnimator){_headAnimator.cullingMode AnimatorCullingMode.AlwaysA…...
unity使用Registry类将指定内容写入注册表
遇到一个新需求,在exe执行初期把指定内容写入注册表,Playerprefs固然可以写入,但是小白不知道怎么利用Playerprefs写入DWORD类型的数据,因此使用了Registry类 一. 对注册表中键的访问 注册表中共可分为五类 一般在操作时&#…...
Python进阶学习:Pandas--将一种的数据类型转换为另一种类型(astype())
Python进阶学习:Pandas–将一种的数据类型转换为另一种类型(astype()) 🌈 个人主页:高斯小哥 🔥 高质量专栏:Matplotlib之旅:零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&…...
OpenCV开发笔记(七十五):相机标定矫正中使用remap重映射进行畸变矫正
若该文为原创文章,转载请注明原文出处 本文章博客地址:https://blog.csdn.net/qq21497936/article/details/136293833 各位读者,知识无穷而人力有穷,要么改需求,要么找专业人士,要么自己研究 红胖子(红模仿…...
光伏预测 | Matlab基于CNN-SE-Attention-ITCN的多特征变量光伏预测
光伏预测 | Matlab基于CNN-SE-Attention-ITCN的多特征变量光伏预测 目录 光伏预测 | Matlab基于CNN-SE-Attention-ITCN的多特征变量光伏预测预测效果基本描述模型简介程序设计参考资料 预测效果 基本描述 Matlab基于CNN-SE-Attention-ITCN的多特征变量光伏预测 运行环境: Matla…...
k8s学习笔记-基础概念
(作者:陈玓玏) deployment特别的地方在于replica和selector,docker根据镜像起容器,pod控制容器,job、cronjob、deployment控制pod,job做离线任务,pod大多一次性的,cronj…...
C语言 变量
变量其实只不过是程序可操作的存储区的名称。C 中每个变量都有特定的类型,类型决定了变量存储的大小和布局,该范围内的值都可以存储在内存中,运算符可应用于变量上。 变量的名称可以由字母、数字和下划线字符组成。它必须以字母或下划线开头…...
2024年2月16日优雅草蜻蜓API大数据服务中心v1.1.1大更新-UI全新大改版采用最新设计ui·增加心率计算器·退休储蓄计算·贷款还款计算器等数接口
2024年2月16日优雅草蜻蜓API大数据服务中心v1.1.1大更新-UI全新大改版采用最新设计ui增加心率计算器退休储蓄计算贷款还款计算器等数接口 更新日志 前言:本次更新中途跨越了很多个版本,其次本次ui大改版-同步实时发布教程《带9.7k预算的实战项目layuiph…...
WEB漏洞 逻辑越权之支付数据篡改安全
水平越权 概述:攻击者尝试访问与他拥有相同权限的用户的资源 测试方法:能否通过A用户操作影响到B用户 案例:pikachu-本地水平垂直越权演示-漏洞成因 1)可以看到kobe很多的敏感信息 2)burp抓包,更改user…...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(二)
HoST框架核心实现方法详解 - 论文深度解读(第二部分) 《Learning Humanoid Standing-up Control across Diverse Postures》 系列文章: 论文深度解读 + 算法与代码分析(二) 作者机构: 上海AI Lab, 上海交通大学, 香港大学, 浙江大学, 香港中文大学 论文主题: 人形机器人…...
从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...
将对透视变换后的图像使用Otsu进行阈值化,来分离黑色和白色像素。这句话中的Otsu是什么意思?
Otsu 是一种自动阈值化方法,用于将图像分割为前景和背景。它通过最小化图像的类内方差或等价地最大化类间方差来选择最佳阈值。这种方法特别适用于图像的二值化处理,能够自动确定一个阈值,将图像中的像素分为黑色和白色两类。 Otsu 方法的原…...
屋顶变身“发电站” ,中天合创屋面分布式光伏发电项目顺利并网!
5月28日,中天合创屋面分布式光伏发电项目顺利并网发电,该项目位于内蒙古自治区鄂尔多斯市乌审旗,项目利用中天合创聚乙烯、聚丙烯仓库屋面作为场地建设光伏电站,总装机容量为9.96MWp。 项目投运后,每年可节约标煤3670…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院查看报告小程序
一、开发环境准备 工具安装: 下载安装DevEco Studio 4.0(支持HarmonyOS 5)配置HarmonyOS SDK 5.0确保Node.js版本≥14 项目初始化: ohpm init harmony/hospital-report-app 二、核心功能模块实现 1. 报告列表…...
04-初识css
一、css样式引入 1.1.内部样式 <div style"width: 100px;"></div>1.2.外部样式 1.2.1.外部样式1 <style>.aa {width: 100px;} </style> <div class"aa"></div>1.2.2.外部样式2 <!-- rel内表面引入的是style样…...
Spring AI 入门:Java 开发者的生成式 AI 实践之路
一、Spring AI 简介 在人工智能技术快速迭代的今天,Spring AI 作为 Spring 生态系统的新生力量,正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务(如 OpenAI、Anthropic)的无缝对接&…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...
Android15默认授权浮窗权限
我们经常有那种需求,客户需要定制的apk集成在ROM中,并且默认授予其【显示在其他应用的上层】权限,也就是我们常说的浮窗权限,那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...
Angular微前端架构:Module Federation + ngx-build-plus (Webpack)
以下是一个完整的 Angular 微前端示例,其中使用的是 Module Federation 和 npx-build-plus 实现了主应用(Shell)与子应用(Remote)的集成。 🛠️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…...
