Redisson 分布式限流器 RRateLimiter 的使用及原理

2024-01-17 06:20

本文主要是介绍Redisson 分布式限流器 RRateLimiter 的使用及原理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 一、基本使用
    • 1.1 创建限流器
    • 1.2 获取令牌
    • 1.3 使用示例
  • 二、实现原理


一、基本使用

1.1 创建限流器

/*** Returns rate limiter instance by name* * @param name of rate limiter* @return RateLimiter object*/
RRateLimiter getRateLimiter(String name);
/*** 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 true if rate was set and false otherwise*/
boolean trySetRate(RateType mode, long rate, long rateInterval, RateIntervalUnit rateIntervalUnit);

trySetRate 用于设置限流参数。其中 RateType 包含 OVERALLPER_CLIENT 两个枚举常量,分别表示全局限流和单机限流。后面三个参数表明了令牌的生成速率,即每 rateInterval 生成 rate 个令牌,rateIntervalUnitrateInterval 的时间单位。

1.2 获取令牌

/*** Acquires a specified permits from this RateLimiter, * blocking until one is available.** Acquires the given number of permits, if they are available * and returns immediately, reducing the number of available permits * by the given amount.* * @param permits the number of permits to acquire*/
void acquire(long permits);/*** Acquires the given number of permits only if all are available* within the given waiting time.** Acquires the given number of permits, if all are available and returns immediately,* with the value true, reducing the number of available permits by one.** If no permit is available then the current thread becomes* disabled for thread scheduling purposes and lies dormant until* the specified waiting time elapses.** If a permits is acquired then the value true is returned.** If the specified waiting time elapses then the value false* is returned.  If the time is less than or equal to zero, the method* will not wait at all.** @param permits amount* @param timeout the maximum time to wait for a permit* @param unit the time unit of the timeout argument* @return true if a permit was acquired and false*         if the waiting time elapsed before a permit was acquired*/
boolean tryAcquire(long permits, long timeout, TimeUnit unit);

acquiretryAcquire 均可用于获取指定数量的令牌,不过 acquire 会阻塞等待,而 tryAcquire 会等待 timeout 时间,如果仍然没有获得指定数量的令牌直接返回 false

1.3 使用示例

@Slf4j
@SpringBootTest
class RateLimiterTest {@Autowiredprivate RedissonClient redissonClient;private static final int threadCount = 10;@Testvoid test() throws InterruptedException {RRateLimiter rateLimiter = redissonClient.getRateLimiter("my_limiter");rateLimiter.trySetRate(RateType.OVERALL, 10, 1, RateIntervalUnit.SECONDS);CountDownLatch latch = new CountDownLatch(threadCount);for (int i = 0; i < threadCount; i++) {new Thread(() -> {rateLimiter.tryAcquire(5, 3, TimeUnit.SECONDS);latch.countDown();log.info("latch count {}", latch.getCount());}).start();}latch.await();}
}
2024-01-16 20:14:27 INFO  [Thread-2] atreus.ink.rate.RateLimiterTest : latch count 9
2024-01-16 20:14:27 INFO  [Thread-3] atreus.ink.rate.RateLimiterTest : latch count 8
2024-01-16 20:14:28 INFO  [Thread-1] atreus.ink.rate.RateLimiterTest : latch count 7
2024-01-16 20:14:29 INFO  [Thread-10] atreus.ink.rate.RateLimiterTest : latch count 6
2024-01-16 20:14:29 INFO  [Thread-8] atreus.ink.rate.RateLimiterTest : latch count 5
2024-01-16 20:14:30 INFO  [Thread-5] atreus.ink.rate.RateLimiterTest : latch count 4
2024-01-16 20:14:30 INFO  [Thread-4] atreus.ink.rate.RateLimiterTest : latch count 3
2024-01-16 20:14:30 INFO  [Thread-6] atreus.ink.rate.RateLimiterTest : latch count 2
2024-01-16 20:14:30 INFO  [Thread-7] atreus.ink.rate.RateLimiterTest : latch count 1
2024-01-16 20:14:30 INFO  [Thread-9] atreus.ink.rate.RateLimiterTest : latch count 0

二、实现原理

Redisson 的 RRateLimiter 基于令牌桶实现,令牌桶的主要特点如下:

  • 令牌以固定速率生成。
  • 生成的令牌放入令牌桶中存放,如果令牌桶满了则多余的令牌会直接丢弃,当请求到达时,会尝试从令牌桶中取令牌,取到了令牌的请求可以执行。
  • 如果桶空了,那么尝试取令牌的请求会被直接丢弃。

RRateLimiter 在创建限流器时通过下面 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]);

而获取令牌则是通过以下的 Lua 脚本实现:

-- 请求参数示例
-- KEYS[1] my_limiter
-- KEYS[2] {my_limiter}:value
-- KEYS[4] {my_limiter}:permits
-- ARGV[1] 3 本次请求的令牌数
-- ARGV[2] 1705396021850 System.currentTimeMillis()
-- ARGV[3] 6966135962453115904 ThreadLocalRandom.current().nextLong()-- 读取 RRateLimiter.trySetRate 中配置的限流器信息
local rate = redis.call('hget', KEYS[1], 'rate');  -- 10 一个时间窗口内产生的令牌数
local interval = redis.call('hget', KEYS[1], 'interval');  -- 1000 一个时间窗口对应的毫秒数
local type = redis.call('hget', KEYS[1], 'type');  -- 0 全局限流
assert(rate ~= false and interval ~= false and type ~= false, 'RateLimiter is not initialized')local valueName = KEYS[2];  -- {my_limiter}:value 当前可用令牌数字符串的 key
local permitsName = KEYS[4];  -- {my_limiter}:permits 授权记录有序集合的 key-- 单机限流配置 无需考虑
if type == '1' thenvalueName = KEYS[3];permitsName = KEYS[5];
end;-- 查询当前可用的令牌数 查询失败表明是首次请求令牌
local currentValue = redis.call('get', valueName);
if currentValue == false then -- 首次请求令牌-- 单次请求的令牌数不能超过一个时间窗口内产生的令牌数assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount could not exceed defined rate');-- 更新当前可用令牌数以及令牌授权记录 {my_limiter}:permits-- set {my_limiter}:permits 10redis.call('set', valueName, rate);-- zadd {my_limiter}:permits 1705396021850 6966135962453115904_1redis.call('zadd', permitsName, ARGV[2], struct.pack('fI', ARGV[3], ARGV[1]));-- decrby {my_limiter}:permits 3redis.call('decrby', valueName, ARGV[1]);return nil;
else -- 再次请求令牌-- 查询可以回收的令牌对应的授权记录 即一个时间窗口前的所有授权记录且包括一个时间窗口前这一时刻-- 旧令牌回收的本质是新令牌的加入 如果一个令牌是在一个时间窗口前被分配的 那经过一个时间窗口后这个空出的位置应该已经由新令牌填充-- zrangebyscore {my_limiter}:permits 0 1705396020850local expiredValues = redis.call('zrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval); -- [1936135962853113704_2, 536135765023123704_5]-- 统计可以回收的令牌数local released = 0;for i, v in ipairs(expiredValues) dolocal random, permits = struct.unpack('fI', v);-- released = released + 2-- released = released + 5released = released + permits;end;-- 删除授权记录并回收令牌if released > 0 then-- zrem {my_limiter}:permits 1936135962853113704_2 536135765023123704_5redis.call('zrem', permitsName, unpack(expiredValues));currentValue = tonumber(currentValue) + released;-- incrby {my_limiter}:value 7redis.call('set', valueName, currentValue);end;if tonumber(currentValue) < tonumber(ARGV[1]) then-- 如果回收后可用令牌数仍然不足 返回需要等待的时间-- zrangebyscore {my_limiter}:permits (1705396020850 1705396021850 withscores limit 0 1local nearest = redis.call('zrangebyscore', permitsName, '(' .. (tonumber(ARGV[2]) - interval), tonumber(ARGV[2]), 'withscores', 'limit', 0, 1);local random, permits = struct.unpack('fI', nearest[1]);-- 1705396021650 - 1705396021850 + 1000 = 800return 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;
end;

参考:

https://github.com/oneone1995/blog/issues/13
https://www.infoq.cn/article/Qg2tX8fyw5Vt-f3HH673

这篇关于Redisson 分布式限流器 RRateLimiter 的使用及原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/615103

相关文章

Android Paging 分页加载库使用实践

《AndroidPaging分页加载库使用实践》AndroidPaging库是Jetpack组件的一部分,它提供了一套完整的解决方案来处理大型数据集的分页加载,本文将深入探讨Paging库... 目录前言一、Paging 库概述二、Paging 3 核心组件1. PagingSource2. Pager3.

python使用try函数详解

《python使用try函数详解》Pythontry语句用于异常处理,支持捕获特定/多种异常、else/final子句确保资源释放,结合with语句自动清理,可自定义异常及嵌套结构,灵活应对错误场景... 目录try 函数的基本语法捕获特定异常捕获多个异常使用 else 子句使用 finally 子句捕获所

C++11右值引用与Lambda表达式的使用

《C++11右值引用与Lambda表达式的使用》C++11引入右值引用,实现移动语义提升性能,支持资源转移与完美转发;同时引入Lambda表达式,简化匿名函数定义,通过捕获列表和参数列表灵活处理变量... 目录C++11新特性右值引用和移动语义左值 / 右值常见的左值和右值移动语义移动构造函数移动复制运算符

Python对接支付宝支付之使用AliPay实现的详细操作指南

《Python对接支付宝支付之使用AliPay实现的详细操作指南》支付宝没有提供PythonSDK,但是强大的github就有提供python-alipay-sdk,封装里很多复杂操作,使用这个我们就... 目录一、引言二、准备工作2.1 支付宝开放平台入驻与应用创建2.2 密钥生成与配置2.3 安装ali

Spring Security 单点登录与自动登录机制的实现原理

《SpringSecurity单点登录与自动登录机制的实现原理》本文探讨SpringSecurity实现单点登录(SSO)与自动登录机制,涵盖JWT跨系统认证、RememberMe持久化Token... 目录一、核心概念解析1.1 单点登录(SSO)1.2 自动登录(Remember Me)二、代码分析三、

C#中lock关键字的使用小结

《C#中lock关键字的使用小结》在C#中,lock关键字用于确保当一个线程位于给定实例的代码块中时,其他线程无法访问同一实例的该代码块,下面就来介绍一下lock关键字的使用... 目录使用方式工作原理注意事项示例代码为什么不能lock值类型在C#中,lock关键字用于确保当一个线程位于给定实例的代码块中时

MySQL 强制使用特定索引的操作

《MySQL强制使用特定索引的操作》MySQL可通过FORCEINDEX、USEINDEX等语法强制查询使用特定索引,但优化器可能不采纳,需结合EXPLAIN分析执行计划,避免性能下降,注意版本差异... 目录1. 使用FORCE INDEX语法2. 使用USE INDEX语法3. 使用IGNORE IND

C# $字符串插值的使用

《C#$字符串插值的使用》本文介绍了C#中的字符串插值功能,详细介绍了使用$符号的实现方式,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧... 目录$ 字符使用方式创建内插字符串包含不同的数据类型控制内插表达式的格式控制内插表达式的对齐方式内插表达式中使用转义序列内插表达式中使用

flask库中sessions.py的使用小结

《flask库中sessions.py的使用小结》在Flask中Session是一种用于在不同请求之间存储用户数据的机制,Session默认是基于客户端Cookie的,但数据会经过加密签名,防止篡改,... 目录1. Flask Session 的基本使用(1) 启用 Session(2) 存储和读取 Se

springboot自定义注解RateLimiter限流注解技术文档详解

《springboot自定义注解RateLimiter限流注解技术文档详解》文章介绍了限流技术的概念、作用及实现方式,通过SpringAOP拦截方法、缓存存储计数器,结合注解、枚举、异常类等核心组件,... 目录什么是限流系统架构核心组件详解1. 限流注解 (@RateLimiter)2. 限流类型枚举 (