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);


 @Override
    public boolean trySetRate(RateType type, long rate, long rateInterval, RateIntervalUnit unit) {
        return get(trySetRateAsync(type, rate, rateInterval, unit));
    }

    @Override
    public 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' then
    valueName = 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 then
        redis.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]) then
        local nearest = redis.call('zrangebyscore', permitsName, '(' .. (tonumber(ARGV[2]) - interval), '+inf', 'withscores', 'limit', 0, 1);
        return tonumber(nearest[2]) - (tonumber(ARGV[2]) - interval);
        -- 如果 剩余令牌数量 >= 当前请求令牌数量,可直接记录签发令牌,并从剩余令牌数量中减去当前签发令牌数量
    else
        redis.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,走初始化逻辑
else
    redis.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才有设置过期时间。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Html零基础入门教程(非常详细)

文章目录 1.认识HTML2.html 框架3.HTML常见标签4.HTML语法特征5.列表 1.认识HTML html是超文本标记语言: 目前最新版本是html5,由w3c(万维网联盟)完成标准制定。 声明文档的类型是html5 超文本标记语言。 HTML &#xff0c;全称“Hyper Text Markup Language&#xff08;超文…

Python是垃圾?千万不要再学Python了?

“人生苦短&#xff0c;快学Python”这句话&#xff0c;相信大家都有看到过&#xff0c;但是有细心留意过&#xff0c;就会发现Python其实在网上的评价褒贬不一&#xff0c;有好评&#xff0c;也有差评。这就会给那些不懂Python却想要学Python的一些人造成困惑&#xff0c;我到…

mongo之常用数据库操作

目录 一、准备环境 二、日常记录及执行示范 连接数据库查询版本查询表总数模糊查询(使用正则)查询文档中数据条数排序大于等于查询有哪些库时间查询不在条件内的查询复制数据更新字段名称删除数据库 四、高阶查询 五、备份迁移数据库 总结 一、准备环境 借鉴&#xff1a;…

【算法分析与设计】最大二叉树

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;算法分析与设计 ⛺️稳中求进&#xff0c;晒太阳 题目 给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建: 创建一个根节点&#xff0c;其值为 nums 中的最…

Logic Pro:专业音乐制作软件,为你的音乐插上翅膀

Logic Pro是一款功能强大的音乐制作软件&#xff0c;专为专业音乐人和音乐爱好者设计。它提供了全面的音乐创作工具&#xff0c;包括音频录音、编辑、混音、合成以及自动化等功能&#xff0c;让你能够轻松实现音乐梦想。 Logic Pro软件获取 首先&#xff0c;Logic Pro拥有卓越…

关于网站的保姆级攻略

什么是域名&#xff1f; 域名是互联网上用于识别和定位计算机和网络服务的字符串。它提供了一个便于人们记忆和使用的名称&#xff0c;用来代替复杂的IP地址&#xff0c;可用于从客户端浏览器&#xff08;Chrome、EDGE&#xff09;访问网站。简单来说&#xff0c;域名是用户在浏…

这一次,彻底解决滚动穿透

什么是滚动穿透 如图所示&#xff0c;有一层遮罩蒙层覆盖在body上时&#xff0c;当我们滚动遮罩层&#xff0c;它下面的内容也会跟着一起滚动&#xff0c;看起来好像是上面的滚动事件穿透到下面的DOM元素上一样&#xff0c;我们称之为滚动穿透。 阻止冒泡&#xff1f; 刚开始…

Window系统禅道BUG管理系统安装配置并实现公网远程访问

文章目录 前言1. 本地安装配置BUG管理系统2. 内网穿透2.1 安装cpolar内网穿透2.2 创建隧道映射本地服务3. 测试公网远程访问4. 配置固定二级子域名4.1 保留一个二级子域名5.1 配置二级子域名6. 使用固定二级子域名远程 前言 BUG管理软件,作为软件测试工程师的必备工具之一。在…

【Linux】进程信号 --- 信号的产生 保存 捕捉递达

文章目录 信号的感知信号的结构描述 一、信号的产生1.通过键盘发送信号2.通过系统调用发送信号 二、信号的保存&#xff08;PCB内部的两张位图和一个函数指针数组&#xff09;理解三张数据结构表block pending haldler 三、通过代码编写 理解 信号的保存和递达1.信号集操作的库…

看到递归就晕?带你理解递归的本质!【基础算法精讲 09】

104 . 二叉树的最大深度 链接 : . - 力扣&#xff08;LeetCode&#xff09; 思路 : 对于题意&#xff0c;可以拆分为 : ans max(左子树的最大深度 &#xff0c; 右子树的最大深度) 1 ; 原问题 : 计算整颗树的最大深度 &#xff1b; 子问题 : 计算左右子树的最大深度 ;…

Postgresql中dblink扩展的使用

一、介绍 Postgresql数据库提供了一个dblink扩展的插件&#xff0c;能够直接在一个数据库中操作另外一个远程数据库&#xff0c;比如&#xff1a;一个数据库在服务器A上&#xff0c;另外一个数据库在服务器B上&#xff0c;我可以在A这台服务器数据库上面建立一个到B服务器数据库…

Redis是单线程还是多线程?

说Redis是单线程或者是多线程这种说法并不严谨&#xff0c;要拿版本说话&#xff0c;Redis的版本有很多3.x、4.x和6.x&#xff0c;版本不同架构也是不同的&#xff0c;不限定版本问是否单线程是不太严谨的。 版本3.x&#xff0c;最早版本&#xff0c;此时Redis是单线程的版本4…

精品ssm人事办公考勤报销管理系统

《[含文档PPT源码等]精品基于ssm办公管理系统[包运行成功]》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功&#xff01; 软件开发环境及开发工具&#xff1a; Java——涉及技术&#xff1a; 前端使用技术&#xff1a;HTML5,CSS3、JavaS…

webrtc

stun服务 阿里云服务器安全组添加端口开放 webrtc-streamer视屏流服务器搭建 - 简书

安科瑞Acrel-2000ES 储能柜能量管理系统

安科瑞戴婷 安科瑞储能能量管理系统Acrel-2000ES&#xff0c;专门针对工商业储能柜、储能集装箱研发的一款储能EMS&#xff0c; 具有完善的储能监控与管理功能,涵盖了储能系统设备(PCS、BMS、电表、消防、空调等)的详细信息,实现了数据采集、数据处理、数据存储、数据查询与分…

浅谈 Linux 网络编程 - 网络字节序

文章目录 前言核心知识关于 小端法关于 大端法网络字节序的转换 函数 前言 在进行 socket 网络编程时&#xff0c;会用到字节流的转换函数、例如 inet_pton、htons 等&#xff0c;那么为什么要用到这些函数呢&#xff0c;本篇主要就是对这部分进行介绍。 核心知识 重点需要记…

4-如何进行细分市场的分析-02 细分行业的构成和基本情况

如何快速摸清行业的构成&#xff0c;通常会看同行或自己做过的相似的行业&#xff0c;会根据不同的行业来采用不同的研究方法。对于成熟的行业和不同的行业都会有一些比较通用的研究方式。 假设我们是在分析某一个行业&#xff0c;在分析行业的时候它的本质还是市场分析&#…

Leetcode300. 最长递增子序列 -代码随想录

题目&#xff1a; 代码(首刷看解析 2024年2月29日&#xff09;&#xff1a; class Solution { public:int lengthOfLIS(vector<int>& nums) {int n nums.size();if (n < 1) return 1;vector<int> dp(n, 1);int res 0;for (int i 1; i < n; i) {for(i…

springboot+vue实现oss文件存储

前提oss准备工作 进入阿里云官网&#xff1a;阿里云oss官网 注册 搜OSS&#xff0c;点击“对象存储OSS” 第一次进入需要开通&#xff0c;直接点击立即开通&#xff0c;到右上角AccessKey管理中创建AccessKey&#xff0c;并且记住自己的accessKeyId和accessKeySecret&#…

使用 Gradle 版本目录进行依赖管理 - Android

/ 前言 / 在软件开发中&#xff0c;依赖管理是一个至关重要的方面。合理的依赖版本控制有助于确保项目的稳定性、安全性和可维护性。 Gradle版本目录&#xff08;Version Catalogs&#xff09;是 Gradle 构建工具的一个强大功能&#xff0c;它为项目提供了一种集中管理依赖…