分布式限流-单位时间多实例多线程访问次数限制

接前面聊一聊redisson及优雅实现 和 说一说spring boot优雅集成redisson,简单以源码的方式给大家介绍了redisson的:可重入性、阻塞、续约、红锁、联锁、加锁解锁流程和集成spring boot注意点和优雅实现方式。

接下来在讲一讲平时用的比较多的限流模块--RRateLimiter

1.简单使用

 public static void main(String[] args) throws InterruptedException {
        RRateLimiter rateLimiter = createLimiter();
        int allThreadNum = 20;
        CountDownLatch latch = new CountDownLatch(allThreadNum);
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < allThreadNum; i  ) {
//            new Thread(() -> {
            if(i % 3 == 0) Thread.sleep(1000);
            boolean pass = rateLimiter.tryAcquire();
            if(pass) {
                log.info("get ");
            } else {
                log.info("no");
            }
//          latch.countDown();
//            }).start();
        }
//        latch.await();
        System.out.println("Elapsed "   (System.currentTimeMillis() - startTime));
    }
    public static RRateLimiter createLimiter() {
        Config config = new Config();
        config.useSingleServer()
                .setTimeout(1000000)
                .setPassword("123456")
                .setAddress("redis://xxxx:6379");
        RedissonClient redisson = Redisson.create(config);
        RRateLimiter rateLimiter = redisson.getRateLimiter("myRateLimiter3");
        // 初始化:PER_CLIENT 单实例执行,OVERALL 全实例执行
        // 最大流速 = 每10秒钟产生3个令牌
        rateLimiter.trySetRate(RateType.OVERALL, 3, 10, RateIntervalUnit.SECONDS);
        return rateLimiter;
    }

实际结果:

[2022-10-29 14:32:46.261][INFO ][main][][] RedisTest - get 
[2022-10-29 14:32:46.312][INFO ][main][][] RedisTest - get 
[2022-10-29 14:32:46.358][INFO ][main][][] RedisTest - get 
[2022-10-29 14:32:47.416][INFO ][main][][] RedisTest - no
[2022-10-29 14:32:47.469][INFO ][main][][] RedisTest - no
[2022-10-29 14:32:47.517][INFO ][main][][] RedisTest - no
[2022-10-29 14:32:48.577][INFO ][main][][] RedisTest - no
[2022-10-29 14:32:48.623][INFO ][main][][] RedisTest - no

2. 实现限流redisson使用了哪些redis数据结构

  • Hash结构 -- 限流器结构:

参数rate代表速率

参数interval代表多少时间内产生的令牌

参数type代表单机还是集群

  • ZSET结构 -- 记录获取令牌的时间戳,用于时间对比。

1667025166312 --> 2022-10-29 14:32:46

1667025166262 --> 2022-10-29 14:32:46

1667025166215 --> 2022-10-29 14:32:46

  • String结构 --记录的是当前令牌桶中的令牌数【很明显被我用完了现在是0】

3. 超过10s,我再次获取一个令牌,数据结构发生的变化

  • ZSET结构。-- 新生成一个ZSET结构,存放获取令牌的时间戳

  • String 结构 --当前令牌桶还有2个令牌

4. 源码浅析

RRateLimiter rateLimiter = redisson.getRateLimiter("myRateLimiter3");
// 初始化
// 最大流速 = 每10秒钟产生3个令牌
rateLimiter.trySetRate(RateType.PER_CLIENT, 3, 10, RateIntervalUnit.SECONDS);

初始化定义没有什么好讲的,就是创建HASH结构

主要还是讲讲: rateLimiter.tryAcquire()

private <T> RFuture<T> tryAcquireAsync(RedisCommand<T> command, Long value) {
    return this.commandExecutor.evalWriteAsync(
        this.getName(), 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;
            local currentValue = redis.call('get', valueName); 
            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('fI', v);
                released = released   permits;end; 
                if released > 0 
                then 
                redis.call('zrem', permitsName, unpack(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); 
                    local random, permits = struct.unpack('fI', nearest[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; 
                    else 
                    assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount could not exceed defined rate'); 
                    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;", Arrays.asList(this.getName(), 
                    this.getValueName(), 
                    this.getClientValueName(), 
                    this.getPermitsName(), 
                    this.getClientPermitsName()), 
                    new Object[]{value, 
                    System.currentTimeMillis(), 
                    ThreadLocalRandom.current().nextLong()
                    }
                );
}

主要就是这段lua代码,下面我详细过一下

作者目前用的3.16.3版本,刚好遇见redisson的bug,见3197,请大家用最新版本,以下为修复后解析。

-- 获取hash结构的速率
local rate = redis.call("hget", KEYS[1], "rate")
-- 获取hash结构的时间区间(ms)
local interval = redis.call("hget", KEYS[1], "interval")
-- 获取hash结构的时间类型
local type = redis.call("hget", KEYS[1], "type")
-- 判断是否初始化限流结构
assert(rate ~= false and interval ~= false and type ~= false, "RateLimiter is not initialized")
-- {name}:value string结构,这个key记录的是当前令牌桶中的令牌数
local valueName = KEYS[2]
-- {name}:permits zset结构,记录了请求的令牌数,score则为请求的时间戳
local permitsName = KEYS[4]
-- 单机限流才会用到,集群模式不用关注
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")
-- 初始化RateLimiter并不会初始化stirng结构,因此第一次获取这里currentValue是null
local currentValue = redis.call("get", valueName)
if currentValue ~= false then
    -- 第二次获取令牌执行
    -------------------------- 获取zset结构:统计之前的请求令牌数
    -- 范围是0 ~ (第二次请求时间戳 - 令牌生产的时间)
    local expiredValues = redis.call("zrangebyscore", permitsName, 0, tonumber(ARGV[2]) - interval)
    local released = 0
    -- lua迭代器,遍历expiredValues,如果有值,那么released等于之前所有请求的令牌数之和,表示应该释放多少令牌
    for i, v in ipairs(expiredValues) do
        -- 获取请求数permits
        local random, permits = struct.unpack("fI", v)
        released = released   permits
    end
    -- 之前的请求令牌数 > 0, 例如10s产生3个令牌,现在超过10s了,重置周期并计算剩余令牌数
    if released > 0 then
        -- 移除zset中所有元素【要求是同一个限流器permitsName,不然就移除不了,尴尬】 
        redis.call("zrem", permitsName, unpack(expiredValues))
        currentValue = tonumber(currentValue)   released
        ------------------------- 更新string结构:=剩下令牌数 释放令牌数
        redis.call("set", valueName, currentValue)
    end
    -- 如果当前令牌数 < 请求的令牌数
    if tonumber(currentValue) < tonumber(ARGV[1]) then
        -- 从zset中找到距离当前时间最近的那个请求,也就是上一次放进去的请求信息
        local nearest = redis.call('zrangebyscore', permitsName, '(' .. (tonumber(ARGV[2]) - interval), tonumber(ARGV[2]), 'withscores', 'limit', 0, 1); 
        local random, permits = struct.unpack("fI", nearest[1])
        -- 返回 上一次请求的时间戳 - (当前时间戳 - 令牌生成的时间间隔) 这个值表示还需要多久才能生产出足够的令牌
        return tonumber(nearest[2]) - (tonumber(ARGV[2]) - interval)
    else
        -- 如果当前令牌数 ≥ 请求的令牌数,表示令牌够多,更新zset
        ------------------------- 更新zset结构
        redis.call("zadd", permitsName, ARGV[2], struct.pack("fI", ARGV[3], ARGV[1]))
        ------------------------- 更新Stringt结构,减少一个剩下的令牌数
        redis.call("decrby", valueName, ARGV[1])
        return nil
    end
else
    --------汀雨笔记----------------- 初始化Stringt结构,当前限流器的令牌数
    redis.call("set", valueName, rate)
    --------汀雨笔记----------------- 初始化zset结构
    redis.call("zadd", permitsName, ARGV[2], struct.pack("fI", ARGV[3], ARGV[1]))
    -- struct.pack第一个参数表示格式字符串,f是浮点数、I是长整数。所以这个格式字符串表示的是把一个浮点数和长整数拼起来的结构体,
    -- ARGV[2]就是请求时间戳,ARGV[1]是请求的令牌数,统计会用到,ARGV[3]是当前时间戳为种子的随机数,具体用处还不知道,知道的网友可以留言
    ------------------------- 更新Stringt结构,因为这是获取令牌操作,减掉一个令牌
    -------------------------【本文作者认为,这里可以直接初始化string结构,值为rate - 1】
    redis.call("decrby", valueName, ARGV[1])
    return nil
end

这段lua代码也并不复杂,令牌桶的数量主要是通过时间窗口来控制,判断上一个请求是否超过了令牌生产周期。

留下一个疑问?

-- 移除zset中所有元素【要求是同一个限流器permitsName,不然就移除不了,尴尬】 
redis.call("zrem", permitsName, unpack(expiredValues))

我自己在本地测试,只要超过10s,permitsName就不一样,这就导致了这部分数据是不能移除的,就产生了冗余数据,从前面的截图也可以看出,是新生成了一个zset数据结构。

相当于直接走到了这一步:

------------------------- 更新zset结构 
redis.call("zadd", permitsName, ARGV[2], struct.pack("fI", ARGV[3], ARGV[1]))

至于为什么会产生这样的结果,会的小伙伴可以留言,或者过段时间我提个issue。

以上就是redisson分布式限流RRateLimiter源码解析的详细内容,更多关于redisson分布式限流RRateLimiter的资料请关注Devmax其它相关文章!

redisson分布式限流RRateLimiter源码解析的更多相关文章

  1. java SpringBoot 分布式事务的解决方案(JTA+Atomic+多数据源)

    这篇文章主要介绍了java SpringBoot 分布式事务的解决方案(JTA+Atomic+多数据源),文章围绕主题展开详细的内容介绍,具有一定的参考价值,感兴趣的小伙伴可以参考一下

  2. Spring Boot 集成Redisson实现分布式锁详细案例

    这篇文章主要介绍了Spring Boot 集成Redisson实现分布式锁详细案例,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的朋友可以参考一下

  3. 关于分布式锁(Redisson)的原理分析

    这篇文章主要介绍了关于分布式锁(Redisson)的原理,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  4. VUE实现分布式医疗挂号系统预约挂号首页步骤详情

    这篇文章主要为大家介绍了VUE实现分布式医疗挂号系统预约挂号首页步骤详情,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  5. Mapreduce分布式并行编程

    这篇文章主要为大家介绍了Mapreduce分布式并行编程使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  6. VUE搭建分布式医疗挂号系统后台管理页面示例步骤

    这篇文章主要为大家介绍了分布式医疗挂号系统之搭建后台管理系统页面,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  7. spring boot优雅集成redisson详解

    这篇文章主要为大家介绍了spring boot优雅集成redisson详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  8. SpringBoot 2.5.5整合轻量级的分布式日志标记追踪神器TLog的详细过程

    分布式追踪系统是一个最终的解决方案,如果您的公司已经上了分布式追踪系统,这篇文章主要介绍了SpringBoot 2.5.5整合轻量级的分布式日志标记追踪神器TLog,需要的朋友可以参考下

  9. Java Redis Redisson配置教程详解

    这篇文章主要介绍了Java Redis Redisson配置教程,包括Session共享配置及其他Redisson的Config配置方式,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  10. redisson特性及优雅实现示例

    这篇文章主要为大家介绍了redisson特性及优雅实现示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

随机推荐

  1. 基于EJB技术的商务预订系统的开发

    用EJB结构开发的应用程序是可伸缩的、事务型的、多用户安全的。总的来说,EJB是一个组件事务监控的标准服务器端的组件模型。基于EJB技术的系统结构模型EJB结构是一个服务端组件结构,是一个层次性结构,其结构模型如图1所示。图2:商务预订系统的构架EntityBean是为了现实世界的对象建造的模型,这些对象通常是数据库的一些持久记录。

  2. Java利用POI实现导入导出Excel表格

    这篇文章主要为大家详细介绍了Java利用POI实现导入导出Excel表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  3. Mybatis分页插件PageHelper手写实现示例

    这篇文章主要为大家介绍了Mybatis分页插件PageHelper手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  4. (jsp/html)网页上嵌入播放器(常用播放器代码整理)

    网页上嵌入播放器,只要在HTML上添加以上代码就OK了,下面整理了一些常用的播放器代码,总有一款适合你,感兴趣的朋友可以参考下哈,希望对你有所帮助

  5. Java 阻塞队列BlockingQueue详解

    本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景,通过实例代码介绍了Java 阻塞队列BlockingQueue的相关知识,需要的朋友可以参考下

  6. Java异常Exception详细讲解

    异常就是不正常,比如当我们身体出现了异常我们会根据身体情况选择喝开水、吃药、看病、等 异常处理方法。 java异常处理机制是我们java语言使用异常处理机制为程序提供了错误处理的能力,程序出现的错误,程序可以安全的退出,以保证程序正常的运行等

  7. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  8. 面试突击之跨域问题的解决方案详解

    跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据。那怎么解决这个问题呢?接下来我们一起来看

  9. Mybatis-Plus接口BaseMapper与Services使用详解

    这篇文章主要为大家介绍了Mybatis-Plus接口BaseMapper与Services使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  10. mybatis-plus雪花算法增强idworker的实现

    今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部