使用Lua Script实现不同的限流算法

news2024/11/16 9:45:53

文章目录

  • Redis中执行Lua Script
  • Redis中Debug Lua Script
  • 固定窗口
    • 代码关键部分解释
    • 验证
  • 滑动窗口
  • 令牌桶
  • 漏桶
  • 参考资料

Redis中执行Lua Script

redis-cli --eval /tmp/script.lua mykey somekey , arg1 arg2

特别注意:key和arg之间是空格+逗号+空格,否则脚本调用redis-cli命令时会报错

关于Redis CLI的使用建议通读Redis CLI文档,里面有很多常用的操作要熟记,例如如何通过redis-cli连接,如何带密码连接,如果指定lua脚本等

Redis中Debug Lua Script

在Redis中可以使用redis-cli作为debugLua脚本的工具

redis-cli --ldb --eval /tmp/script.lua mykey somekey , arg1 arg2

关于如何调试,官方文档说的也很清楚
在这里插入图片描述

更多细节参考Redis官方文档关于Debug Lua一节

以1分钟允许通过最多3个请求为例,分别实现四种算法。

固定窗口

例如在10:01:00 - 10:02:00,这1分钟为例
key为userId:当前窗口的起点,例如 8848:01

local key = KEYS[1]
-- 如果这个key此前不存在则返回-1
-- 关于 or 的写法参考下面图片的截图
local requests = tonumber(redis.call('GET', key) or '-1')
-- 在固定时间范围内允许的最大请求数,例如这里应该是3
local max_requests = tonumber(ARGV[1])
-- 通常是固定窗口的大小,例如60s
local expiry = tonumber(ARGV[2])
-- 当该窗口的key不存在或者未达到最大请求时
if (requests == -1) or (requests < max_requests) then
  -- 自增
  redis.call('INCR', key)
  -- 重新设置该key的过期日期为60s后
  redis.call('EXPIRE', key, expiry)
  return false
else
  return true
end

代码关键部分解释

假如当前时间是10:01:30第一次请求,由于8848:01不存在,所以此时给该键+1并且设置过期时间为60s后。
这里有的同学可能对最后的EXPIRE那里有点争议,其实只是实现方式不同而已。可能大家想到的一种方式是在确定是第一次请求的时候,将剩余的时间给计算出来,等到后面的请求过来的时候,只需要自增即可。
上面这种实现方式就是简化了一下代码,即使到了10:02,因为key变了,所以获取不到10:01的key,这个key自然会在最后一次被设置完过期时间并达到60s后删除
在这里插入图片描述

验证

在这里插入图片描述
可以看到当3次请求限制达到了,则返回的是true,true被转为1

滑动窗口

固定窗口的问题是:当在10:01:59请求了2次,在10:02:01也请求了2次,这就导致在短短2秒内请求了4次,已经超过了3次的限制。按照固定窗口的逻辑来判断,这两次在各自的时间窗口内是合理的,但是在这种边界时,是不正确的。所以滑动窗口就来解决这个问题

local key = KEYS[1]
-- 当前时间戳
local current_time = tonumber(ARGV[1])
-- 窗口大小,本例中是60 * 1000
local window_size = tonumber(ARGV[3])
-- 本例中是3
local max_requests = tonumber(ARGV[4])
-- 根据当前时间毫秒数 - 超时毫秒数,得到过期时间 expired
local has_expired = current_time - window_size
-- 清除过期的数据
redis.call('ZREMRANGEBYSCORE', key, 0, has_expired)
-- 获取 zset 中的当前元素个数
local current_num = tonumber(redis.call('ZCARD', key))
local next = current_num + 1
-- 达到限流大小 返回 0
if next > max_requests then
  return 0;
else
  -- 往 zset 中添加一个值、得分均为当前时间戳的元素,[value,score]
  redis.call("ZADD", key, current_time, current_time)
  -- 每次访问均重新设置 zset 的过期时间
  redis.call("PEXPIRE", key, window_size)
  return next
end

滑动窗口其实在实际中大部分业务场景下都可以使用,那么为什么还会有令牌桶和漏桶算法的需求呢?带着这个疑问,我在网上找了一些资料,我觉得比较靠谱的回答是流量整型

令牌桶

在实际开发中,令牌桶适用的场景是对请求我们服务器的上游(这里的上游包括来自浏览器的直接请求或者其他不属于我们系统的未知的请求)进行限制。限制请求速率以确保我们的服务器不会被打垮

令牌桶涉及到两个脚本:

  • 一个是按照一定的速率往令牌桶里加令牌
  • 另外一个是从桶里拿令牌,如果拿到则请求通过,如果拿不到则拒绝请求

添加令牌

-- 当前时间戳
local ts = tonumber(ARGV[1])
-- 设置窗口大小为1s
local min = ts -1
-- 可以为多个key设置添加令牌的速率
for i,key in pairs(KEYS) do
    -- 移除过期的的令牌
    redis.call('ZREMRANGEBYSCORE', key, '-inf', min)
    redis.call('ZADD', key, ts, ts)
    redis.call('EXPIRE', key, 10)
end

获取令牌

-- 当前时间戳
local ts  = tonumber(ARGV[1])
local key = KEYS[1]
local min = ts -1
-- 移除过期的的令牌
redis.call('ZREMRANGEBYSCORE', key, '-inf', min)
-- 只需要计算令牌桶的大小即可,如果令牌桶是有值的,则放行
return redis.call('ZCARD', key)

漏桶

漏桶的适用场景是按照一定的速率向下游服务请求,避免下游服务处理不过来而报错。如果在一段时间内请求数大于固定速率的请求数,则需要排队等待

local ts  = tonumber(ARGV[1])
-- calls per seconds,例如每秒4次,也就是250ms内允许发起1次请求
local cps = tonumber(ARGV[2])
local key = KEYS[1]
local min = ts -1
redis.call('ZREMRANGEBYSCORE', key, '-inf', min)
local last = redis.call('ZRANGE', key, -1, -1)
local next = ts
if type(last) == 'table' and #last > 0 then
  for key,value in pairs(last) do
    -- 最后一个元素 + 固定速率的时间
    next = tonumber(value) + 1/cps
    break
  end
end
if ts > next then
  -- the current ts is > than last+1/cps
  -- we'll keep ts
  next = ts
end
-- 如果ts < next,这里next还是现有zset的最后一个元素 + 1/cps 后的时间
redis.call('ZADD', key, next, next)
-- 必须等待的时间,
-- 如果是ts > next,则这里直接返回0,意味着不等待,直接调用
-- 如果是ts < next, 则说明下一个速率还没达到,则需要等到, next - ts这么长时间
return tostring(next - ts)

参考资料

  • 滑动窗口的实现
  • 令牌桶和漏桶的实现

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

API网关在API安全性中的作用

从单一应用程序切换到微服务时&#xff0c;客户端的行为不能与客户端具有该应用程序的一个入口点的行为相同。简单来说就是微服务上的某一部分功能与单独实现该应用程序时存在不同。 目前在使用微服务时&#xff0c;客户端必须处理微服务体系结构带来的所有复杂性&#xff0c;…

一个月裂变50多万人?商城引流模式玩法——分享购

随着经济进入转型的关键期&#xff0c;零售行业的营销模式正在发生转变&#xff0c;以消费者为中心驱动营销数字化成为当下企业的共识。 新零售时代数字化的核心是数据能力的全面升级&#xff0c;企业通过数字化协同能力整合线上线下全域消费场景&#xff0c;赋能消费者深度洞…

JDK16及其以后的版本Maven打包成可运行的程序的方法

Java应用的分发一直是一个比较麻烦的问题。这是因为Java应用的运行需要虚拟机的支持&#xff0c;仅有Java应用打包的JAR文件是不够的&#xff0c;目标机器还需要安装版本匹配的JDK或JRE。随着云原生和容器化技术的流行&#xff0c;Java应用可以选择以容器镜像的形式来打包和分发…

以太网,拥塞控制与 AQM

拥塞的表现是冲突&#xff0c;大量消费者挤兑有限资源&#xff0c;即拥塞&#xff0c;在一个绝对没有冲突的系统里&#xff0c;自然就没有拥塞。而冲突的来源有两个&#xff0c;一个是无序&#xff0c;一个是贪婪。 仅引入秩序不能解决冲突&#xff0c;但可减少冲突损耗&#…

JavaScript高级 浏览器的渲染原理

浏览器的渲染原理1. 网页的解析过程2. 浏览器内核3. 浏览器渲染过程1. HTML解析2. 生成CSS规则3. 构建Render Tree4. 布局&#xff08;layout&#xff09;5. 绘制&#xff08;Paint&#xff09;4. 回流和重绘解析1. 回流2. 重绘3. web 性能优化4. 合成和性能优化5. 浏览器遇到 …

【VulnHub靶场】——CHRONOS: 1

作者名&#xff1a;Demo不是emo 主页面链接&#xff1a;主页传送门创作初心&#xff1a;舞台再大&#xff0c;你不上台&#xff0c;永远是观众&#xff0c;没人会关心你努不努力&#xff0c;摔的痛不痛&#xff0c;他们只会看你最后站在什么位置&#xff0c;然后羡慕或鄙夷座右…

PaddlePaddle/PaddleX本地离线安装(分别以C++和Python为例)

目录一. 本地离线安装简介1.1 为什么需要源码编译1.2 源码编译的产物1.3 源码编译的原理二. 本地离线安装准备2.1 编译准备(C/Python通用)2.2 本地编译推荐配置(C)2.3 本地编译推荐配置(Python)三. Windows本地离线安装(C源码编译)3.2 编译工具安装3.3 打开Visual studio 终端3…

torch.cat函数

#dim0,上下拼接 #dim1&#xff0c;左右拼接 import torch # x1 x1 torch.tensor([[1,2,3],[4,5,6]],dtypetorch.int)# x2 x2 torch.tensor([[7,8,9],[10,11,12]],dtypetorch.int)#inputs为&#xff12;个形状为[2 , 3]的矩阵 inputs [x1, x2] print(inputs)#dim0,上下拼接…

如何用Python字符串进行切片操作?

嗨害大家好鸭&#xff01;我是小熊猫~ 我们基本上都知道Python的序列对象 都是可以用索引号来引用的元素的&#xff0c; 索引号可以是正数由0开始从左向右&#xff0c; 也可以是负数由-1开始从右向左。 在Python中对于具有序列结构的数据来说都可以使用切片操作&#xff0…

【C++升级之路】第五篇:C/C++内存管理(new和delete的实现原理)

&#x1f31f;hello&#xff0c;各位读者大大们你们好呀&#x1f31f; &#x1f36d;&#x1f36d;系列专栏&#xff1a;【C学习与应用】 ✒️✒️本篇内容&#xff1a;C/C内存分布&#xff0c;C/C动态内存管理方法&#xff0c;C动态内存管理方法底层函数operator new 和operat…

浅谈树状数组

学习树状数组必不可少学习树状数组的精髓&#xff0c;lowbit()运算 在计算机中存储一个正数是以二进制的形式&#xff0c;而存储一个负数则是以二进制补码的形式&#xff0c;简单说就是二进制取反1&#xff0c;lowbit运算就是提取出最后一个1以后的位置&#xff0c;比如10100进…

【C语言】结构体、共用体、位域

结构体 1、 结构体的声明方法 struct struct_name {data_type member1;data_type member2;.. };这是其中一种声明方式~ 2、定义一个结构体变量 struct struct_name variable&#xff1b;3、访问成员变量 . 运算 一个结构体变量访问其成员时&#xff0c;使用的是 . 运算 下面…

BM35 判断是不是完全二叉树

题目 给定一个二叉树&#xff0c;确定他是否是一个完全二叉树。 完全二叉树的定义&#xff1a;若二叉树的深度为 h&#xff0c;除第 h 层外&#xff0c;其它各层的结点数都达到最大个数&#xff0c;第 h 层所有的叶子结点都连续集中在最左边&#xff0c;这就是完全二叉树。&a…

深入理解ConcurrentHashMap1.7源码

1. 概述 HashMap在我们的日常生活中使用很多&#xff0c;但是它不是线程安全的。我们可以使用HashTable来代替&#xff0c;主要实现方式是在方法中加入synchronized&#xff0c;所以效率也比较低。因此&#xff0c;对于键值对&#xff0c;我们可以尝试使用ConcurrentHashMap来…

实验室规划设计方案SICOLAB

一、实验室规划设计 喜格提供实验室布局方案 根据实验室性质、实验室定位、实验室功能、实验类型、实验工艺流程以及国家相关标准合理的规划布局。 喜格提供仪器摆放布局方案 根据该实验流程来确定仪器的种类、数量、规格型号、外形尺寸、电压功率等参数以及摆放位置以及提…

【Linux】tee、tail、killall、|、||、、命令学习

|、||、&、&&辨析 竖线‘|’在linux中是管道符的意思&#xff0c;将‘|’前面命令的输出作为’|后面的输入&#xff1b; 双竖线‘||’&#xff0c;用双竖线‘||’分割的多条命令&#xff0c;执行的时候遵循如下规则&#xff1a;如果前一条命令为真&#xff0c;则…

还在喷农民歌唱家大衣哥吗?他的一个不经意间的举动却造福了乡里

农民歌唱家大衣哥&#xff0c;一直以来都饱受争议&#xff0c;有人说他是炒货专家&#xff0c;然而事实真的如此吗&#xff1f;事实上&#xff0c;大衣哥也做了很多好事&#xff0c;像修桥补路等都不说了&#xff0c;单就他的一个不经意间的举动&#xff0c;就造福了四乡八邻。…

Windows内核--CPU和内核(1.7)

Windows内核支援哪些CPU? Intel x86/x86_64 IA64已不再支持. AMD amd64 ARM (Windows On Arm: WOA) ARM具备低功耗优势, 除了高通, 还有Broadcom/NXP等都支援ARM架构. 苹果自研M系列开了头&#xff0c;ARM不仅有低功耗&#xff0c;同样有性能&#xff0c;Windows也想分一杯羹…

【vue系列-03】vue的计算属性,列表,监视属性及原理

vue的核心属性一&#xff0c;vue核心属性1&#xff0c;计算属性2&#xff0c;监视属性3&#xff0c;样式绑定3.1&#xff0c;class样式绑定3.2&#xff0c;style样式绑定4&#xff0c;条件渲染5&#xff0c;列表渲染5.1&#xff0c;遍历列表5.2&#xff0c;key的作用5.3&#x…

2022年全国职业院校技能大赛中职组网络安全竞赛——隐写术应用解析(超详细)

2022年全国职业院校技能大赛中职组网络安全竞赛——隐写术应用解析(超详细) B-8任务八:隐写术应用 *任务说明:仅能获取Server8的IP地址 环境需求私信博主 1.找出文件夹1中的文件,将文件中的隐藏信息作为Flag值提交; 解题步骤如下 2.找出文件夹2中的文件,将文件中的隐藏信息…