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://www.hqwc.cn/news/498329.html

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

相关文章

给MATLAB安装runtime插件

给MATLAB安装runtime插件&#xff0c;将MATLAB程序打包成exe文件方法 1.查看已安装的MATLAB是否有runtime插件。操作:在command窗口直接输入mcrinstaller。 如果如图上所示&#xff0c;那就是没有。(2019版本以后包括2019一般均没有) 2.下载MATLAB对应子版本的runtime。(很多…

测评ONLYOFFICE 8.0版本:办公利器再升级

测评ONLYOFFICE 8.0版本&#xff1a;办公利器再升级 前言注册使用升级功能速览全新外观设计wordexcelPPTPDF 协作功能强化更强大的功能复杂表单的填写 移动端优化结语 前言 随着科技的不断发展&#xff0c;办公软件在提升用户体验和工作效率方面扮演着越来越重要的角色。作为一…

王者荣耀,急于补齐内容短板

问十个人&#xff0c;有九个人知道《王者荣耀》&#xff1b;但如果再问十个知道《王者荣耀》的人&#xff0c;这款游戏到底讲了一个什么故事&#xff0c;每个角色又有怎样的背景&#xff0c;可能十个人都不知道。 整个新年档口&#xff0c;《王者荣耀》都很忙碌。 1月&#x…

网上申请手机流量卡:便捷、快速、无忧的选择

随着互联网的普及和科技的发展&#xff0c;越来越多的人选择在网上办理各种业务。其中&#xff0c;网上申请手机流量卡成为了一种便捷、快速、无忧的选择。今天小编将详细介绍网上申请手机流量卡的流程和优势&#xff0c;帮助您更好地了解这种新型的办理方式&#xff0c;希望能…

JAVA计算表达式

需求&#xff1a; 1、例如if(score>85){return 1;}else if(score>70){return 2;}else if(score>60){return 3;}else{return 4;}有这一串字符串&#xff0c;要执行这个字符串&#xff0c; 如果score为86分&#xff0c;则能得到1&#xff1b;如果score为30分&#xff…

【Vue3】回顾watch,学习watchEffect

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…

【记录】成为创作者的第 365 天

机缘 原来 365 天过得这么快&#xff0c;想想一年前的我还只是一个啥都不懂的小白&#xff08;虽然现在懂的也不算多&#xff09;&#xff0c;只能算是刚入门吧。知道 CSDN 这个平台是偶然在朋友圈看到一条有关该平台的博客转发&#xff0c;于是便注册了账号。 还是那句话&am…

归并分治问题练习和题解

文章目录 简介排序数组原理代码 交易逆序对的总数原理代码跑的过程代码 计算右侧小于当前元素的个数原理代码 翻转对代码 简介 大家好,这里是jiantaoyab,这篇文章给大家带来的是归并类型相关的题目练习和解析,涉及到递归的题目大家可以画递归展开图来方便理解,后面的题目有点绕…

【C++】用文件流的put和get成员函数读写文件

题目 编写一个mycopy程序&#xff0c;实现文件复制的功能。用法是在控制台输入&#xff1a; mycooy 源文件名 目标文件名 参数介绍 m a i n main main 函数的参数有两个&#xff0c;一个int类型参数和一个指针数组。 a r g c argc argc 表示参数的个数。参数为void时 a r g …

关于uniapp小程序的分包问题

开发uniapp小程序时&#xff0c;在打包上传代码时会出现超出2M的打包限制不能上传&#xff0c;那么我们该怎么做呢&#xff1f; 1.对于图片&#xff0c;将图片从后端服务取&#xff0c;尽量不要放在静态资源&#xff0c;图片体积会影响打包大小。 2.使用分包&#xff0c;tabb…

网络防御第6次作业

防病毒网关 按照传播方式分类 病毒 病毒是一种基于硬件和操作系统的程序&#xff0c;具有感染和破坏能力&#xff0c;这与病毒程序的结构有关。病毒攻击的宿主程序是病毒的栖身地&#xff0c;它是病毒传播的目的地&#xff0c;又是下一次感染的出发点。计算机病毒感染的一般过…

Cordova打包手机位置权限没有“始终允许“权限

1 找到AndroidManifest.xml文件 2 增加一行 <uses-permission android:name"android.permission.ACCESS_BACKGROUND_LOCATION" />