深入解析 Redisson 分布式限流器 RRateLimiter 的原理与实现

文章目录

    • RRateLimiter 介绍
    • 代码实现
    • Lua 脚本
    • 现实场景
    • 1. 初始化限流器
    • 2. 限流器应用场景(客人申请游玩流程)

RRateLimiter 介绍

在分布式系统中,限流(Rate Limiting)是保障系统稳定性、避免过载的重要机制。Redisson 作为一个功能强大的 Redis 客户端,不仅提供了广泛使用的分布式锁,还包含了许多其他实用的分布式工具。其中,RRateLimiter 是 Redisson 提供的分布式限流器,功能强大。本文将详细解析 RRateLimiter 的原理,深入理解其工作机制。

代码实现

首先,通过一个简单的示例了解如何使用 RRateLimiter,它创建了一个限流器并启动多个线程来获取令牌:

import org.redisson.Redisson; // 导入 Redisson 的核心类,用于创建 Redisson 客户端
import org.redisson.api.RRateLimiter; // 导入 RRateLimiter 接口,用于实现分布式限流
import org.redisson.api.RedissonClient; // 导入 RedissonClient 接口,用于与 Redis 进行交互
import org.redisson.config.Config; // 导入 Redisson 的配置类,用于配置 Redis 连接

import java.util.concurrent.CountDownLatch; // 导入 CountDownLatch 类,用于控制线程同步

public class RateLimiterDemo { // 定义一个公共类 RateLimiterDemo
    public static void main(String[] args) throws InterruptedException { // 主方法,程序入口,可能抛出 InterruptedException
        RRateLimiter rateLimiter = createRateLimiter(); // 创建一个 RRateLimiter 实例

        int totalThreads = 20; // 定义总线程数为 20
        CountDownLatch latch = new CountDownLatch(totalThreads); // 创建一个 CountDownLatch 实例,初始计数为 totalThreads

        long startTime = System.currentTimeMillis(); // 记录开始时间,用于计算总耗时
        for (int i = 0; i < totalThreads; i++) { // 循环创建并启动 20 个线程
            new Thread(() -> { // 创建一个新线程
                rateLimiter.acquire(1); // 每个线程尝试获取 1 个令牌,若令牌不足则阻塞等待
                latch.countDown(); // 线程完成后,调用 countDown() 方法减少计数器
            }).start(); // 启动线程
        }
        latch.await(); // 主线程等待,直到所有子线程完成
        System.out.println("Total elapsed time: " + (System.currentTimeMillis() - startTime) + " ms"); // 打印总耗时
    }

    /**
     * 创建并配置 RRateLimiter 的方法
     *
     * @return 配置好的 RRateLimiter 实例
     */
    private static RRateLimiter createRateLimiter() { // 创建并配置 RRateLimiter 的方法
        Config config = new Config(); // 创建一个新的 Redisson 配置实例
        config.useSingleServer() // 配置使用单一 Redis 服务器
              .setAddress("redis://127.0.0.1:6379") // 设置 Redis 服务器地址
              .setTimeout(1000000); // 设置连接超时时间(毫秒)

        RedissonClient redisson = Redisson.create(config); // 根据配置创建一个 Redisson 客户端实例
        RRateLimiter rateLimiter = redisson.getRateLimiter("myRateLimiter"); // 获取名为 "myRateLimiter" 的 RRateLimiter 实例
        rateLimiter.trySetRate(RRateLimiter.RateType.OVERALL, 1, 1, RateIntervalUnit.SECONDS); // 初始化限流器,设置全局速率为每秒 1 个令牌
        return rateLimiter; // 返回配置好的限流器实例
    }
}

Lua 脚本

为了更深入地理解 RRateLimiter 的工作原理,将进一步解析其底层的 Lua 脚本,实现分布式限流的核心逻辑。以下内容将逐行解释 Lua 脚本的功能和实现细节。

redis.call('hsetnx', KEYS[1], 'rate', ARGV[1]); -- 将速率设置到哈希表中,只有当 'rate' 字段不存在时才设置
redis.call('hsetnx', KEYS[1], 'interval', ARGV[2]); -- 将时间区间设置到哈希表中,只有当 'interval' 字段不存在时才设置
return redis.call('hsetnx', KEYS[1], 'type', ARGV[3]); -- 将类型设置到哈希表中,只有当 'type' 字段不存在时才设置,并返回结果
-- ARGV[1] 为请求令牌数
-- ARGV[2] 为请求时间戳
-- ARGV[3] 为请求类型

-- 获取限流器的速率、时间区间和类型
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") -- 确保限流器已初始化

-- 默认情况下,使用 {name}:value 和 {name}:permits
local valueName = KEYS[2] -- 当前令牌数的键名
local permitsName = KEYS[4] -- 记录请求的有序集合键名

-- 如果类型为 "1"(单机模式),则使用不同的键名
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)
-- 第一次请求直接走else
-- 第二次请求因为 valueName 更新有值,走if
if currentValue ~= false then
    -- 获取已过期的请求(初始时间 至 (当前时间(ARGV[2]-时间间隔(interval)) 准备清理失效的令牌数据
    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("fI", v)
        released = released + permits
    end

    -- 如果有释放的令牌,更新当前可用令牌数并移除过期的请求
    if released > 0 then
        redis.call("zrem", permitsName, unpack(expiredValues)) -- 清除 permitsName 中包含 expiredValues 的数据
        currentValue = tonumber(currentValue) + released -- 清理失效令牌后计算总可用令牌数
        redis.call("set", valueName, currentValue) -- 更新可用令牌
    end

    -- 如果当前令牌数不足以满足请求  
    if tonumber(currentValue) < tonumber(ARGV[1]) then
        -- 计算需要等待的时间
        local nearest = redis.call('zrangebyscore', permitsName, '(' .. (tonumber(ARGV[2]) - interval), tonumber(ARGV[2]), 'withscores', 'limit', 0, 1) -- 找到最近一次的请求时间 nearest 
        local random, permits = struct.unpack("fI", nearest[1]) -- 解压为时间戳+请求令牌数
        -- 返回等待时间,也可以写为 tonumber(nearest[2])+interval-tonumber(ARGV[2])
        return tonumber(nearest[2]) - (tonumber(ARGV[2]) - interval) -- nearest[2] 为上行的 random
    else
        -- 当前可用令牌数足够,记录此次请求并减少可用令牌数
        redis.call("zadd", permitsName, ARGV[2], struct.pack("fI", ARGV[3], ARGV[1])) -- 记录请求记录
        redis.call("decrby", valueName, ARGV[1]) -- 更新可用令牌数 valueName -= ARGV[1](请求令牌数)
        return nil -- 成功获取令牌
    end
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]) -- 更新可用令牌数 valueName -= ARGV[1](请求令牌数)
    return nil -- 成功获取令牌
end

现实场景

冰雪大世界的热门项目每天吸引着络绎不绝的顾客。为了避免人流过于集中,影响顾客的体验和项目的正常运行,管理团队制定了以下规则:

  • 每小时只接待6位客人。
  • 每位客人在进入项目游玩,一个小时后自动将入场票归还到废票区,确保不影响后续客人的入场。
    限流机制的设置

1. 初始化限流器

项目每天一开始,第一位客人进入游玩时,系统会进行以下操作:

  • 统计系统剩余票数量:记录为(valueName),代表同一时间段内的最大客容量。
  • 记录每次申请的客人及进场时间:存储在(permitsName)中。
  • 刷新实际剩余票数量:更新为(currentValue = valueName),确保系统实时掌握当前剩余的入场票数。
    通过这些步骤,系统为当天的限流工作做好了准备。

2. 限流器应用场景(客人申请游玩流程)

当一位客人申请游玩项目时,系统会按照以下流程操作:

步骤一:查询可用票

  • 计算实际剩余票(currentValue = valueName)。

步骤二:回收废票

  • 从废票区 根据入场记录(permitsName)计算(当前时间-时间间隔)之前的所有废弃入场票(released),这意味着已进入游玩的客人在系统时间间隔后已不再影响项目后续游客的体验,归还的票可以重新使用。
  • 更新实际剩余票(currentValue):将回收的票数加到实际剩余票(currentValue += released)
  • 更新系统剩余票(valueName):(valueName = currentValue),确保系统知道当前有多少可用的入场票,反映最新的入场票状态。

步骤三:判断票数是否足够

  • 检查实际剩余票 (currentValue):与当前游客申请票数(tonumber(ARGV[1]))进行比较。
    • 如果票够用:

      • 记录此次请求:将客人的申请信息和进场时间记录到(permitsName)中。
      • 更新系统剩余票:(valueName -= 申请票数)中扣除相应的票数。
      • 允许客人进入:客人成功进入项目游玩。
    • 如果票不够:

      • 计算等待时间:根据上一位客人的入场时间和设定的时间间隔,计算出客人需要等待时间(上一位客人的入场时间+间隔时间-当前时间)。
      • 告知客人:将计算出的等待时间返回给客人,游客异步再尝试进入。

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

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

相关文章

Java jni调用nnom rnn-denoise 降噪

介绍&#xff1a;https://github.com/majianjia/nnom/blob/master/examples/rnn-denoise/README_CN.md 默认提供了一个wav的例子 #include <stdint.h> #include <stdlib.h> #include <stdio.h> #include <math.h> #include <string.h>#include …

Windows系统 系统盘瘦身策略之文件迁移

1 Android Studio 1.1 .android 该文件夹路径一般在 C:\Users\<user_name>\.android 迁移步骤&#xff1a; ①关闭 Android Studio ②打开环境变量设置&#xff0c;添加以下环境变量 变量名&#xff1a;ANDROID_SDK_HOME 变量值&#xff1a;你自己的路径【不用单独创建.…

SQLiteDataBase数据库

XML界面设计 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"xmlns:tools"http://schemas.android.com/tools"android:layout_width"match_paren…

Midjourney技术浅析(七):图像风格化

Midjourney 通过风格迁移&#xff08;Style Transfer&#xff09;和图像滤镜&#xff08;Image Filters&#xff09;技术&#xff0c;使用户能够将生成的图像转换为不同的艺术风格或视觉效果。 一、风格迁移&#xff08;Style Transfer&#xff09; 1.1 风格迁移的定义 风格…

Edge安装问题,安装后出现:Could not find Edge installation

解决&#xff1a;需要再安装&#xff08;MicrosoftEdgeWebView2RuntimeInstallerX64&#xff09;。 网址&#xff1a;https://developer.microsoft.com/zh-cn/microsoft-edge/webview2/?formMA13LH#download 如果已经安装了edge&#xff0c;那就再下载中间这个独立程序安装就…

【JAVA高级篇教学】第六篇:Springboot实现WebSocket

在 Spring Boot 中对接 WebSocket 是一个常见的场景&#xff0c;通常用于实现实时通信。以下是一个完整的 WebSocket 集成步骤&#xff0c;包括服务端和客户端的实现。本期做个简单的测试用例。 目录 一、WebSocket 简介 1. 什么是 WebSocket&#xff1f; 2. WebSocket 的特…

Painter-Mortadela靶场

信息收集 枚举端口 nmap 192.168.109.132 -sS -sV -min-rate 5000 -Pn -p- -p- &#xff1a;扫描所有端口。 (65535)-sS&#xff1a;执行TCP SYN 扫描以快速扫描哪些端口打开。-sC&#xff1a;使用基本识别脚本执行扫描-sV&#xff1a;执行服务扫描–min-rate 5000&#xff1…

攻防世界pwn刷题

get_shell 这题直接给shell了 exp from pwn import* p remote(61.147.171.105,59682) p.sendline(cat flag) p.interactive() cyberpeace{8cd678c722f48327a69b2661ae8956c8} hello_pwn checksec一下 ok&#xff0c;64位的 {alarm(0x3Cu);setbuf(stdout, 0LL);puts("…

1、pycharm、python下载与安装

1、去官网下载pycharm 官网&#xff1a;https://www.jetbrains.com/pycharm/download/?sectionwindows 2、在等待期间&#xff0c;去下载python 进入官网地址&#xff1a;https://www.python.org/downloads/windows/ 3、安装pycharm 桌面会出现快捷方式 4、安装python…

epoll的ET和LT模式

LevelTriggered&#xff1a;简称LT&#xff0c;当FD有数据可读时&#xff0c;会重复通知多次&#xff0c;直至数据处理完成。是epoll的默认模式EdgeTriggered&#xff1a;简称ET&#xff0c;当FD有数据可读时&#xff0c;只通知一次&#xff0c;不管数据是否处理完成 Level是指…

CSS利用浮动实现文字环绕右下角,展开/收起效果

期望实现 文字最多展示 N 行&#xff0c;超出部分截断&#xff0c;并在右下角显示 “…” “更多”&#xff1b; 点击更多&#xff0c;文字展开全部内容&#xff0c;右下角显示“收起”。效果如下&#xff1a; 思路 尽量使用CSS控制样式&#xff0c;减少JS代码复杂度。 利…

单元测试入门和mockup

Java 新手入门&#xff1a;Java单元测试利器&#xff0c;Mock详解_java mock-CSDN博客 这个是典型的before when assert三段式&#xff0c;学一下单测思路 这个没有动态代理&#xff0c;所以是直接class(对比下面) Jmockit使用笔记_增加代码覆盖率_覆盖try catch_使用new Mock…

开发小工具:ping地址

开发小工具&#xff1a;ping地址 import socketdef tcp_port_scan(ip,port):#创建套接字socksocket.socket(socket.AF_INET,socket.SOCK_STREAM)#设置超时sock.settimeout(0.2)try:#发请求result sock.connect_ex((ip,port))if result 0:print(f{ip}--{port}接口连接成功)res…

双汇火腿肠,请勿随意喂猫

在许多家庭中&#xff0c;猫咪作为可爱的宠物成员&#xff0c;备受宠爱。当我们享受着双汇火腿肠的便捷与美味时&#xff0c;或许会有人想到与猫咪分享&#xff0c;但这种看似温馨的举动实则隐藏着诸多问题&#xff0c;双汇火腿肠并不适合喂猫。 从营养成分来看&#xff0c;双…

Unity Excel转Json编辑器工具

功能说明&#xff1a;根据 .xlsx 文件生成对应的 JSON 文件&#xff0c;并自动创建脚本 注意事项 Excel 读取依赖 本功能依赖 EPPlus 库&#xff0c;只能读取 .xlsx 文件。请确保将该脚本放置在 Assets 目录下的 Editor 文件夹中。同时&#xff0c;在 Editor 下再创建一个 Exc…

深信服云桌面系统的终端安全准入设置

深信服的云桌面系统在默认状态下没有终端的安全准入设置&#xff0c;这也意味着同样的虚拟机&#xff0c;使用云桌面终端或者桌面套件都可以登录&#xff0c;但这也给系统带来了一些安全隐患&#xff0c;所以&#xff0c;一般情况下需要设置终端的安全准入策略&#xff0c;防止…

基于SpringBoot的实验室信息管理系统【源码+文档+部署讲解】

系统介绍 视频演示 基于SpringBootVue实现的实验室信息管理系统采用前后端分离的架构方式&#xff0c;系统分为管理员、老师、用户三种角色&#xff0c;实现了用户管理、设备管理、实验室查询、公告、课程、实验室耗材管理、我的等功能 技术选型 开发工具&#xff1a;idea2…

Windows 10 自带功能实现大屏、小屏无线扩展

一、添加可选功能 在作为无线投屏对象的「第二屏」设备上&#xff0c;打开 Windows 10 设置并定位至「应用 > 应用和功能」界面&#xff0c;然后点击右侧界面中的「可选功能」选项。 点击可选功能界面顶部的「添加功能」按钮&#xff0c;搜索「无线显示器」模块并选择添加。…

大电流和大电压采样电路

大电压采样电路&#xff1a; 需要串联多个电阻进行分压&#xff0c;从而一级一级降低电压&#xff0c;防止电阻损坏或者短路直接打穿MCU。 为什么需要加电压跟随器&#xff1a;进行阻抗的隔离&#xff0c;防止MCU的IO阻抗对分压产生影响&#xff1a; 大电流检测电路&#xff…

torch.nn.functional的用法

文章目录 介绍激活函数示例 损失函数示例 卷积操作示例 池化示例 归一化操作示例 Dropout示例 torch.nn.functional 与 torch.nn 的区别 介绍 torch.nn.functional 是 PyTorch 中的一个模块&#xff0c;提供了许多函数式的神经网络操作&#xff0c;包括激活函数、损失函数、卷…