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…...
MPNet:旋转机械轻量化故障诊断模型详解python代码复现
目录 一、问题背景与挑战 二、MPNet核心架构 2.1 多分支特征融合模块(MBFM) 2.2 残差注意力金字塔模块(RAPM) 2.2.1 空间金字塔注意力(SPA) 2.2.2 金字塔残差块(PRBlock) 2.3 分类器设计 三、关键技术突破 3.1 多尺度特征融合 3.2 轻量化设计策略 3.3 抗噪声…...
conda相比python好处
Conda 作为 Python 的环境和包管理工具,相比原生 Python 生态(如 pip 虚拟环境)有许多独特优势,尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处: 一、一站式环境管理:…...
【Oracle APEX开发小技巧12】
有如下需求: 有一个问题反馈页面,要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据,方便管理员及时处理反馈。 我的方法:直接将逻辑写在SQL中,这样可以直接在页面展示 完整代码: SELECTSF.FE…...
day52 ResNet18 CBAM
在深度学习的旅程中,我们不断探索如何提升模型的性能。今天,我将分享我在 ResNet18 模型中插入 CBAM(Convolutional Block Attention Module)模块,并采用分阶段微调策略的实践过程。通过这个过程,我不仅提升…...
【论文笔记】若干矿井粉尘检测算法概述
总的来说,传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度,通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...
基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...
让回归模型不再被异常值“带跑偏“,MSE和Cauchy损失函数在噪声数据环境下的实战对比
在机器学习的回归分析中,损失函数的选择对模型性能具有决定性影响。均方误差(MSE)作为经典的损失函数,在处理干净数据时表现优异,但在面对包含异常值的噪声数据时,其对大误差的二次惩罚机制往往导致模型参数…...
动态 Web 开发技术入门篇
一、HTTP 协议核心 1.1 HTTP 基础 协议全称 :HyperText Transfer Protocol(超文本传输协议) 默认端口 :HTTP 使用 80 端口,HTTPS 使用 443 端口。 请求方法 : GET :用于获取资源,…...
RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...
