首页 > 数据库 >Redis使用案例之限流器

Redis使用案例之限流器

时间:2024-11-24 22:34:35浏览次数:11  
标签:令牌 Redis redis ARGV 案例 限流 call local

Redis使用案例之限流器

一、什么是限流器

限流器是一种流量限制的工具,可用于防止接口在同一时间有过高的并发

Redis有两种实现限流器的算法

1.基于桶令牌的算法

​ 以时间段为单位,恒定的给桶中放入令牌,每次请求从桶中获取令牌

2.基于漏桶算法

​ 控制接口流速,先创建一个桶,所有请求进来都放到桶中,再以固定的速度放出请求

本文介绍的是Redission实现的基于桶令牌的限流器

二、基于桶令牌算法限流器使用的数据结构

Redission实现的限流器中使用了Redis的hash、zset、字符串数据结构

为什么使用zset作为限流数据保存的底层结构
  1. zset的底层数据结构

    Redis中的zset数据由字典hash和跳表skiplist实现

    每一个zset对象都包含两个值,一个成员member,一个分数score

    字典部分是为了方便查询成员信息时使用hash算法

    跳表部分是为了方便根据分数进行范围查询(因为跳表中的每一层都是根据分数排序的单链表结构)

  2. 使用zset的原因主要是因为其中的跳表结构

    可以将调用的时间当做分数,方便统计一段时间内的访问量

三、具体实现

  1. 首先需要记录限流器的速率以及窗口大小,可以采用hash结构进行记录,方便获取值

    public RFuture<Boolean> trySetRateAsync(RateType type, long rate, long rateInterval, RateIntervalUnit unit) {
            return commandExecutor.evalWriteNoRetryAsync(getRawName(), 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]);",//设置限流器类型,这里redission默认0
                    Collections.singletonList(getRawName()), rate, unit.toMillis(rateInterval), type.ordinal());
        }
    

    限流器使用之前需要先设置初始值,Redission的内部使用了lua脚本的方式来保证原子性

  2. 如果限流器的初始值之前已经设置过就可以尝试获取令牌资源

    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];"//定义value的key值,保存的是当前令牌的剩余数量
                  + "local permitsName = KEYS[4];"//定义限流保存的相关的入参,使用zset保存
                  + "if type == '1' then "//redission中默认是0
                      + "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脚本来保障原子性操作的

标签:令牌,Redis,redis,ARGV,案例,限流,call,local
From: https://blog.csdn.net/weixin_43649130/article/details/144015306

相关文章

  • 深入理解MySQL中的AUTO_INCREMENT属性:从案例到原理
    深入理解MySQL中的AUTO_INCREMENT属性:从案例到原理引言在数据库设计中,AUTO_INCREMENT是一种常见的属性,用于确保表中的某个字段在插入新记录时自动递增。尽管它不是传统意义上的约束,但在功能上确实起到了约束的作用,确保了字段的唯一性、非空性和自动递增性。本文将通过一个具体......
  • 大厂案例:Shopee 百亿级商品数据如何平稳实现千万级服务器成本缩减
    声明:本文中的细节来源于Shopee研发团队于2024年深圳ArchSummit上的分享。所有的技术细节都要归功于Shopee研发团队。原始内容和其他参考资料的链接在文章末尾的参考资料部分。本文试图分析细节并提供我们的意见。如果您发现任何不准确或遗漏,请留下评论,我们将尽力修复......
  • MySQL原理简介—6.简单的生产优化案例
    大纲1.MySQL日志的顺序写和数据文件的随机读指标2.Linux存储系统软件层原理及IO调度优化原理3.数据库服务器使用的RAID存储架构介绍4.数据库Toomanyconnections故障定位 1.MySQL日志的顺序写和数据文件的随机读指标(1)磁盘随机读操作(2)磁盘顺序写操作 (1)磁盘随机......
  • 道德与法治案例分析题答题技巧
    例如1:随着经济的发展、人民生活水平的提高,在许多学校的学生中,也出现了“手机族”。校园里,朗朗的读书中经常夹杂着此起彼伏的手机声。而有许多同学对手机的档次要求,也越来越高,你的是“彩玲”的,我就要“拍照”的,更有的同学上课也发起了短信。同学们议论纷纷,有的同学认为:“现在生活条......
  • 鸿蒙NEXT开发案例:二维码的生成与识别
    【引言】在本篇文章中,我们将探讨如何在鸿蒙NEXT平台上实现二维码的生成与识别功能。通过使用ArkUI组件库和相关的媒体库,我们将创建一个简单的应用程序,用户可以生成二维码并扫描识别。【环境准备】•操作系统:Windows10•开发工具:DevEcoStudioNEXTBeta1BuildVersion:5......
  • Playwright进行异步爬取案例
    1.代码功能概述该代码使用Playwright异步库编写,用于抓取一个目标网站的数据。主要任务包括:加载网页:访问指定页面并等待加载完成。解析网页内容:提取数据如标题、封面图片、分类、评分、简介等。存储数据:将抓取到的数据以特定格式保存到本地文件中。2.代码结构解析1.......
  • 鸿蒙NEXT开发案例:随机密码生成
    【引言】本案例将实现一个随机密码生成器。用户可以自定义密码的长度以及包含的字符类型(大写字母、小写字母、数字、特殊字符),最后通过点击按钮生成密码,并提供一键复制功能。【环境准备】•操作系统:Windows10•开发工具:DevEcoStudioNEXTBeta1BuildVersion:5.0.3.806......
  • 24最新多目标(MORBMO_PSORF)基于粒子群算法优化随机森林的多目标红嘴蓝鹊优化算法自变
    接代码定制,算法改进等任意多目标都可以用(目标个数可变)含约束的多目标优化vs不含约束的多目标优化带具体数学表达式(白箱)vs不带具体数学表达式的(灰箱)连续版本的多目标参数寻优vs离散版本的多目标参数寻优连续+离散组合版本的多目标参数寻优白箱模型+灰箱模型组合版本的多目......
  • SpringBoot 2.x 整合 Redis
    整合1)添加依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--如果没有使用下面给出的工具类,那么就不需要引入--><dependency><groupId>c......
  • 第54篇 Redis简单介绍
    前言Redis,作为一种开源的、基于内存且支持持久化的键值存储系统,以其卓越的性能、丰富灵活的数据结构和高度可扩展性在全球范围内广受欢迎。Redis不仅提供了一种简单直观的方式来存储和检索数据,更因其支持数据结构如字符串、哈希、列表、集合、有序集合等多种类型,使得其在众多场景......