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才有设置过期时间。

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

编码技巧——Springboot工程加密yml配置/Maven引入本地二方包

1. 背景 基于Springboot的工程项目&#xff0c;通常很多信息都是在application.yml中直接明文配置的&#xff0c;比如数据库链接信息&#xff0c;redis链接信息等&#xff1b; 为了安全考虑&#xff0c;公司打算将yml配置文件中的数据库连接信息的账号&#xff0c;密码进行加…

【两颗二叉树】【递归遍历】【▲队列层序遍历】Leetcode 617. 合并二叉树

【两颗二叉树】【递归遍历】【▲队列层序遍历】Leetcode 617. 合并二叉树 解法1 深度优先 递归 前序解法2 采用队列进行层序遍历 挺巧妙的可以再看 ---------------&#x1f388;&#x1f388;题目链接&#x1f388;&#x1f388;------------------- 解法1 深度优先 递归 前…

搭建LNMP环境并配置个人博客系统

LNMP是Linux&#xff08;操作系统&#xff09;、Nginx&#xff08;Web服务器&#xff09;、MySQL&#xff08;数据库&#xff09;和PHP&#xff08;脚本解释器&#xff09;的组合&#xff0c;常用于部署高性能的动态网站&#xff0c;如WordPress等博客平台 一、安装Linux操作系…

EMR StarRocks实战——猿辅导的OLAP演进之路

目录 一、数据需求产生 二、OLAP选型 2.1 需求 2.2 调研 2.3 对比 三、StarRocks的优势 四、业务场景和技术方案 4.1 整体的数据架构 4.2 BI自助/报表/多维分析 4.3 实时事件分析 4.5 直播教室引擎性能监控 4.4 B端业务后台—斑马 4.5 学校端数据产品—飞象星球 4…

EAP-TLS实验之Ubuntu20.04环境搭建配置(FreeRADIUS3.0)(四)

该篇主要介绍了利用配置ca.cnf、server.cnf、client.cnf在certs路径下生成证书文件&#xff08;非执行bootstrap脚本&#xff0c;网上也有很多直接通过openssl命令方式生成的文章&#xff09;&#xff0c;主要参考&#xff08;概括中心思想&#xff09;官方手册&#xff0c;以及…

【Python笔记-设计模式】备忘录模式

一、说明 备忘录模式是一种行为设计模式&#xff0c;允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。 (一) 解决问题 主要解决在不破坏封装性的前提下&#xff0c;捕获一个对象的内部状态&#xff0c;并在对象之外保存这个状态&#xff0c;以便在需要时恢复对象…

智慧应急:构建全方位、立体化的安全保障网络

一、引言 在信息化、智能化快速发展的今天&#xff0c;传统的应急管理模式已难以满足现代社会对安全保障的需求。智慧应急作为一种全新的安全管理模式&#xff0c;旨在通过集成物联网、大数据、云计算、人工智能等先进技术&#xff0c;实现对应急事件的快速响应、精准决策和高…

尚硅谷(SpringCloudAlibaba微服务分布式)学习代码Eureka部分

1.项目结构 2.cloud2024 pom <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.a…

网络架构与组网部署——补充

5G网络架构的演进趋势 &#xff08;1&#xff09; MEC&#xff1a;多接入边缘计算。首先MEC可以实现5GC的功能&#xff0c;因为5GC是集中在机房中&#xff0c;所以当有MEC后&#xff0c;就可以把MEC下发到基站旁边&#xff0c;这样减少端到端的延时。便于实现5G中不同场景的实…

【Prometheus】基于Altertmanager发送告警到多个接收方、监控各种服务、pushgateway

基于Altertmanager发送报警到多个接收方 一、配置alertmanager-发送告警到qq邮箱1.1、告警流程1.2、告警设置【1】邮箱配置【2】告警规则配置【3】 部署prometheus【4】部署service 二、配置alertmanager-发送告警到钉钉三、配置alertmanager-发送告警到企业微信3.1、注册企业微…

GO语言环境安装---VScode.2024

目录 一、下载并安装GO 二、配置环境变量 三、VScode环境安装 由于工作原因&#xff0c;需要用到go来写web后端&#xff0c;正好从零记录下环境安装 一、下载并安装GO 首先在官网根据PC系统选择对应的包下载 源地址&#xff1a;https://go.dev/dl/ 打不开的用这个也行&a…

可观测性在威胁检测和取证日志分析中的作用

在网络中&#xff0c;威胁是指可能影响其平稳运行的恶意元素&#xff0c;因此&#xff0c;对于任何希望避免任何财政损失或生产力下降机会的组织来说&#xff0c;威胁检测都是必要的。为了先发制人地抵御来自不同来源的任何此类攻击&#xff0c;需要有效的威胁检测情报。 威胁…

2W字-35页PDF谈谈自己对QT某些知识点的理解

2W字-35页PDF谈谈自己对QT某些知识点的理解 前言与总结总体知识点的概况一些笔记的概况笔记阅读清单 前言与总结 最近&#xff0c;也在对自己以前做的项目做一个知识点的梳理&#xff0c;发现可能自己以前更多的是用某个控件&#xff0c;以及看官方手册&#xff0c;但是没有更…

[云原生] k8s之pod容器

一、pod的相关知识 1.1 Pod基础概念 Pod是kubernetes中最小的资源管理组件&#xff0c;Pod也是最小化运行容器化应用的资源对象。一个Pod代表着集群中运行的一个进程。kubernetes中其他大多数组件都是围绕着Pod来进行支撑和扩展Pod功能的&#xff0c;例如&#xff0c;用于管理…

加密与安全_探索常用编码算法

文章目录 概述什么是编码编码分类ASCII码 &#xff08;最多只能有128个字符&#xff09;Unicode &#xff08;用于表示世界上几乎所有的文字和符号&#xff09;URL编码 &#xff08;解决服务器只能识别ASCII字符的问题&#xff09;实现&#xff1a;编码_URLEncoder实现&#xf…

Python3中真真假假True、False、None等含义详解

在Python中&#xff0c;不仅仅和类C一样的真假类似&#xff0c;比如1代表真&#xff0c;0代表假。Python中的真假有着更加广阔的含义范围&#xff0c;Python会把所有的空数据结构视为假&#xff0c;比如 [] (空列表)、 {} &#xff08;空集合&#xff09;、 &#xff08;空字符…

机器学习专项课程03:Unsupervised Learning, Recommenders, Reinforcement Learning笔记 Week01

Week 01 of Unsupervised Learning, Recommenders, Reinforcement Learning 本笔记包含字幕&#xff0c;quiz的答案以及作业的代码&#xff0c;仅供个人学习使用&#xff0c;如有侵权&#xff0c;请联系删除。 课程地址&#xff1a; https://www.coursera.org/learn/unsupervi…

精读《使用 css 变量生成颜色主题》

作者&#xff1a;五灵 本周工作中遇到类似颜色主题的问题&#xff0c;在查资料的时候&#xff0c;看到这个视频&#xff0c;觉得讲得很清楚&#xff0c;而且趣味性丰富&#xff0c;所以想拿出来讲讲这个很有意思的主题。 视频链接&#xff1a; CSSconf EU 2018 | Dag-Inge Aas…

STM32标准库——(12)USART串口协议

1.全双工、半双工及单工通讯 2.同步与异步通讯 在同步通讯中&#xff0c;收发设备双方会使用一根信号线表示时钟信号&#xff0c;在时钟信号的驱动下双方进行协调&#xff0c; 同步数据&#xff0c;见图 同步通讯。 通讯中通常双方会统一规定在时钟信号的上升沿或下降沿对数据线…

循环结构:for循环,while循环,do-while,死循环

文章目录 for循环for案例&#xff1a;累加for循环在开发中的常见应用场景 whilewhile循环案例&#xff1a; for和while的区别&#xff1a;do-while三种循环的区别小结死循环 快捷键 ctrlaltt for循环 看循环执行多少次&#xff0c;就看有效数字有几个 快捷键 fori 示例代码&am…