【限流】基于springboot(拦截器) + redis(执行lua脚本)实现注解限流

实现了滑动窗口,固定窗口,令牌桶,漏桶四种限流算法,并且支持各种扩展和修改,源码简单易上手。
Giteehttps://gitee.com/sir-tree/rate-limiter-spring-boot-starter

一、令牌桶算法—入桶量限制

在客户端请求打过来的时候,会去桶里拿令牌,拿到就请求成功,拿不到不好意思,服务器拒绝服务。

关键点
  • 桶有多大?您定😊
  • 桶里的令牌一开始有多少?您定😊
  • 桶啥时候补充令牌?您定😊
  • 补充多少令牌?您定😊

在想好以上关键点后,我想你已经明白了。

主要步骤

请求打过来

  1. 看一下是否到补充桶的时候了,没到就到第2步,到了就补充完后再继续第2步
  2. 看一下桶里有没有令牌,有就拿走,没有就不好意思,服务器拒绝服务。
扩展:加个黑名单

请求打过来

  1. 看一下在不在黑名单,在就拒绝服务,不在,就进行第2步
  2. 看一下是否到补充桶的时候了,没到就到第2步,到了就补充完后再继续第3步
  3. 看一下桶里有没有令牌,有就拿走,没有就不好意思,加黑名单
Lua实现
-- 参数初始化
local key = KEYS[1]
local maxCapacity = tonumber(ARGV[1]) -- <= 桶有多大/桶里的令牌一开始有多少 我写成一样的了
local fill = tonumber(ARGV[2]) -- <= 补充多少令牌
local now = tonumber(ARGV[3])
local duration = tonumber(ARGV[4]) -- <= 桶啥时候补充令牌
local blackKey = key..'-black'
local blackDuration = tonumber(ARGV[5]) -- <= 黑名单多久后放开
local expire = math.ceil(now / fill) * duration

-- 黑名单
local blackValue = redis.call('get', blackKey)
if blackValue then
    return -1;
end

-- 当前值
local kValue = redis.call('get', key)

-- 第一次
if not kValue then
    -- 当前消耗#刷新时间#容量
    kValue = table.concat({'0', tostring(now + duration), tostring(maxCapacity)}, '#')
end

-- 解析
local parts = {}
for part in string.gmatch(kValue, "[^#]+") do
    table.insert(parts, part)
end

local value = tonumber(parts[1])
local refresh = tonumber(parts[2])
local capacity = tonumber(parts[3])
local fillTimes = math.floor((now - refresh) / duration) + 1

-- 刷新桶令牌
if now >= refresh then
    value = 0
    -- 补充
    capacity = capacity + fill * fillTimes
    -- 最多补满
    if capacity > maxCapacity then
        capacity = maxCapacity
    end
    -- 刷新时间
    refresh = now + duration
end

-- 拿完
if value == capacity then
    -- 加入黑名单
    if blackDuration > 0 then
        redis.call('set', blackKey, '_', 'PX', blackDuration)
        redis.call('del', key)
    end
    return -1
end

-- 期间
value = value + 1
kValue = table.concat({tostring(value), tostring(refresh), tostring(capacity)}, '#')
redis.call('set', key, kValue, 'PX', expire)

return capacity - value

二、 漏桶算法—出桶量限制

在客户端请求打过来的时候,会去桶里放令牌(有唯一标识),放成功后尝试把自己放的令牌拿走,拿不走就不断的尝试拿,直到拿走;但如果放失败了,不好意思,服务器拒绝服务。

关键点
  • 桶有多大?您定😊
  • 哪些令牌可以被自己拿走?您定不了了😎,我定,这里有两个方案,公平的方案每个请求只能拿自己放的令牌,先放进来的令牌可以先被拿走不公平的方案是拿谁放的都可以,大家一起抢,谁抢到算谁的
  • 多长时间内最多可以拿走多少个令牌?‘多长时间’您定😊,‘最多可拿走’您定😊

在想好以上关键点后,我想你已经明白了。

主要步骤

请求打过来

  1. 放令牌成功就执行第2步,但桶满了放不了了,不好意思,服务器拒绝服务
  2. 不断尝试拿令牌(两种方案实现) –不断尝试拿!!不断尝试拿!!!不断尝试拿!!!
实现
-- 参数初始化
local key = KEYS[1]
local capacity = tonumber(ARGV[1]) -- <= 桶有多大
local pass = tonumber(ARGV[2]) -- <= 多长时间内最多可以拿走多少个令牌 -- 最多可以拿走多少个令牌
local duration = tonumber(ARGV[3]) -- <= 多长时间内最多可以拿走多少个令牌 -- 多长时间内
local fair = tonumber(ARGV[4]) -- <= 公平/不公平方案
local queueId = tonumber(ARGV[5]) -- <= 哪些令牌可以被自己拿走
local exceedQueueId = -(capacity + 1)
local legacyPassKey = key..'-legacyPass'
local queueKey = key..'-queue'

-- redis初始化
local qMembers = redis.call('lRange', queueKey, 0, -1)
local lpValue = redis.call('get', legacyPassKey)
local lpExpire = redis.call('pTtl', legacyPassKey)

local function min(q)
    if #q == 0 then
        return 0
    end
    local mv = tonumber(q[1])
    local tmp = mv
    for i = 2, #q do
        tmp = tonumber(q[i])
        if tmp < mv then
            mv = tmp
        end
    end
    return mv
end

local function addQueue(qId)
    if qId ~= exceedQueueId then
        return qId
    end
    if #qMembers == capacity then
        return exceedQueueId
    end
    qId = min(qMembers) - 1
    redis.call('rPush', queueKey, qId)
    table.insert(qMembers, tostring(qId))
    return qId
end

-- 1 删除成功,0删除失败
local function delQueue()
    -- >= 队列不为空,队列有元素
    if #qMembers > 0 then
        -- 公平
        if fair > 0 then
            -- 第一个不是当前请求
            if tonumber(qMembers[1]) ~= queueId then
                return 0
            else
                redis.call('lPop', queueKey)
                table.remove(qMembers, 1)
            end
        else -- 不公平
            redis.call('lRem', queueKey, 1, queueId)
            for i = #qMembers, 1, -1 do
                if tostring(qMembers[i]) == queueId then
                    table.remove(qMembers, i)
                end
            end
        end
    end
    -- 队列无元素,或队列第一个是
    return 1
end

-- 第一次/到刷新时间-刷新pass
if lpExpire <= 0 then
    local update = delQueue()
    if update == 1 then
        pass = pass - 1
    else
        queueId = addQueue(queueId)
    end
    redis.call('set', legacyPassKey, pass, 'PX', duration)

    if update == 1 then
        return pass
    end
    return queueId
end

lpValue = tonumber(lpValue)
-- 如果lpValue >= 0 表示当前不需要等待
if lpValue - 1 >= 0 then
    if delQueue() == 0 then
        return queueId
    end
    lpValue = lpValue - 1
    redis.call('set', legacyPassKey, lpValue, 'PX', lpExpire)
    return lpValue
else
    return addQueue(queueId)
end

三、 固定窗口算法

时间窗口,随时间变化,比如1秒10个,即时间窗口大小为1秒,窗口内最多允许10个请求,随时间变化,如0 ~ 1秒有一个时间窗口,1 ~ 2秒内又是一个时间窗口 … 且每个窗口内最多有10个请求,超过的就放弃。

关键点
  • 时间窗口内允许最多多少个请求?您定😊
  • 时间窗口大小是多大?您定😊

在想好以上关键点后,我想你已经明白了。

主要步骤

请求打过来

  1. 看一下还剩下多少个位置,不剩就服务器拒绝服务,否则成功
Lua实现
local key = KEYS[1]
local limit = tonumber(ARGV[1]) -- <= 时间窗口内允许最多多少个请求
local window = tonumber(ARGV[2]) -- <= 时间窗口大小是多大

local expire = redis.call('pTtl', key)
local kValue = redis.call('get', key)

if expire <= 0 then
    redis.call('set', key, 1, 'PX', window)
    return 1 -- 通过
end

kValue = tonumber(kValue)
if kValue == limit then
    return 0 -- 限流
else
    redis.call('set', key, kValue + 1, 'PX', expire)
    return 1 -- 通过
end

四、滑动窗口

和固定窗口类似,只是这个窗口是动的,怎么动?每次请求过来的时候,就以当前请求为窗口的右端点,而不是固定窗口那样固定。

Lua实现
local key = KEYS[1]
local limit = tonumber(ARGV[1]) -- <= 时间窗口内允许最多多少个请求
local window = tonumber(ARGV[2]) -- <= 时间窗口大小是多大
local now = tonumber(ARGV[3]) -- <= 就以当前请求为窗口的右端点

local start = now - window
redis.call('zRemRangeByScore', key, 0, start) -- 移除上一个窗口期之前的数据
local size = redis.call('zCard', key)

if size == limit then
    return 0 -- 限制
else
    redis.call('zAdd', key, now, now)
    redis.call('pExpire', key, window + 1000) -- 窗口期 + 1秒后过期
    return 1 -- 通过
end

讨论

  • 固定窗口问题:比如窗口是1秒最多10个,当流量在第一个窗口最后100毫秒满10个,且在第二个窗口前100毫秒满10个时,这200毫秒可以看做1秒内,也就是一个窗口,这就有问题(流量激增)…
  • 令牌桶问题:不能平稳的处理请求…
  • 滑动窗口:请求打满后,必须要延迟等到下一个窗口…
  • 漏桶:突发流量会有一堆被抛弃的…

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

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

相关文章

PVDF-SiO₂复合纳米纤维膜

PVDF-SiO₂复合纳米纤维膜是一种结合了聚偏氟乙烯&#xff08;PVDF&#xff09;和二氧化硅&#xff08;SiO₂&#xff09;纳米粒子的新型复合材料。这种材料通常通过静电纺丝技术或其他纤维制备技术制备而成&#xff0c;具有许多良好的性能和广泛的应用前景。 PVDF是一种热塑性…

final、finally、finalize有什么区别?

引言 在Java编程语言中&#xff0c;final、finally和finalize是三个具有不同用途和语义的关键字或方法。它们在编程和面试中经常被提及&#xff0c;因此理解它们之间的区别是非常重要的。 题目 final、finally、 finalize有什么区别&#xff1f; 典型回答 final&#xff1…

ZooKeeper 搭建详细步骤之二(伪集群模式)

ZooKeeper 搭建详细步骤之三&#xff08;真集群&#xff09; ZooKeeper 搭建详细步骤之二&#xff08;伪集群模式&#xff09; ZooKeeper 搭建详细步骤之一&#xff08;单机模式&#xff09; ZooKeeper 及相关概念简介 伪集群搭建 ZooKeeper 伪集群是指在一个单一的物理或虚拟…

活动回顾 | 春起潮涌——硬件驱动的量化交易与AI

4月20日&#xff0c;华锐技术ACLUB联合AMD在上海举办了“春起潮涌——硬件驱动的量化交易与AI”沙龙活动&#xff0c;会议围绕FPGA硬件加速、CPU&网卡调优、AI技术应用等展开&#xff0c;近50位量化IT与分享嘉宾一起探讨硬件技术在量化交易和AI领域的应用和创新。 FPGA在交…

云服务器把端口添加到安全组后无法访问

直接 sudo iptables -I INPUT 5 -p tcp --dport 8085 -j ACCEPT 8085就是端口号 然后再运行服务器 就成功了

YOLOv5入门(二)处理自己数据集(标签统计、数据集划分、数据增强)

上一节中我们讲到如何使用Labelimg工具标注自己的数据集&#xff0c;链接&#xff1a;YOLOv5利用Labelimg标注自己数据集&#xff0c;完成1658张数据集的预处理&#xff0c;接下来将进一步处理这批数据&#xff0c;通常是先划分再做数据增强。 目录 一、统计txt文件各标签类型…

【C语言】——数据在内存中的存储

【C语言】——数据在内存中的存储 一、整数在内存中的存储1.1、整数的存储方式1.2、大小端字节序&#xff08;1&#xff09;大小端字节序的定义&#xff08;2&#xff09;判断大小端 1.3、整型练习 二、浮点数在内存中的存储2.1、引言2.2、浮点数的存储规则2.3、浮点数的存储过…

OI Wiki—递归 分治

//新生训练&#xff0c;搬运整理 递归 定义 递归&#xff08;英语&#xff1a;Recursion&#xff09;&#xff0c;在数学和计算机科学中是指在函数的定义中使用函数自身的方法&#xff0c;在计算机科学中还额外指一种通过重复将问题分解为同类的子问题而解决问题的方法。 引入…

完美解决AttributeError: module ‘backend_interagg‘ has no attribute ‘FigureCanvas‘

遇到这种错误通常是因为matplotlib的后端配置问题。在某些环境中&#xff0c;尤其是在某些特定的IDE或Jupyter Notebook环境中&#xff0c;可能会因为后端配置不正确而导致错误。错误信息提示 module backend_interagg has no attribute FigureCanvas 意味着当前matplotlib的后…

基于STC12C5A60S2系列1T 8051单片机的Proteus中的单片机发送一帧或一串数据给串口调试助手软件接收区显示出来的串口通信应用

基于STC12C5A60S2系列1T 8051单片机的Proteus中的单片机发送一帧或一串数据给串口调试助手软件接收区显示出来的串口通信应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机串口通信介绍STC12C5A60S2系列1T 8051单片机串口通信的结构基于STC12C5A60S2系列…

【MyBatis】 MyBatis框架下的高效数据操作:深入理解增删查改(CRUD)

&#x1f493; 博客主页&#xff1a;从零开始的-CodeNinja之路 ⏩ 收录文章&#xff1a;【MyBatis】 MyBatis框架下的高效数据操作&#xff1a;深入理解增删查改&#xff08;CRUD&#xff09; &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 My …

工具分享:免费一键生成像素风格头像神器

目录 引言神器介绍使用方法上传照⽚选择像素大小保存or分享图片生后图像处理功能娱乐功能 结语最后 引言 五一前一天和群友聊到换微信头像的事情&#xff0c;我就心想自己制作一些头像来用吧&#xff0c;起初是用的无界AI通过咒语来生成头像&#xff0c;但总不尽如人意。如下&…

TFLOPS和TOPS介绍

TFLOPS和TOPS都是衡量计算设备性能的单位&#xff0c;常用于评估处理器或加速器在科学计算、图形处理以及人工智能领域的运算能力。它们分别代表不同的运算类型&#xff1a; TFLOPS (Tera Floating Point Operations Per Second) TFLOPS用于衡量每秒执行的万亿次浮点运算数。…

「 网络安全常用术语解读 」软件物料清单SBOM详解

1. 概览 软件物料清单&#xff08;Software Bill of Materials&#xff0c;SBOM&#xff09;是软件成分信息的集合&#xff0c;SBOM文件中记录了软件产品或服务所使用组件、库、框架的清单&#xff0c;用于描述软件构建过程中使用的所有组件及其关系&#xff0c;以实现软件供应…

spring的高阶使用技巧1——ApplicationListener注册监听器的使用

Spring中的监听器&#xff0c;高阶开发工作者应该都耳熟能详。在 Spring 框架中&#xff0c;这个接口允许开发者注册监听器来监听应用程序中发布的事件。Spring的事件处理机制提供了一种观察者模式的实现&#xff0c;允许应用程序组件之间进行松耦合的通信。 更详细的介绍和使…

22 重构系统升级-实现不停服的数据迁移和用户切量

专栏的前 21 讲&#xff0c;从读、写以及扣减的角度介绍了三种特点各异的微服务的构建技巧&#xff0c;最后从微服务的共性问题出发&#xff0c;介绍了这些共性问题的应对技巧。 在实际工作中&#xff0c;你就可以参考本专栏介绍的技巧构建新的微服务&#xff0c;架构一个具备…

【Schrödinger薛定谔软件使用实战】- 4lyw蛋白实战

文章目录 软件选择1 pretein preparation1.1 import and process注意1.1.1 preprocess可能遇到的问题 1.2 review and modify1.3 refine1.3.1 optimize优化氢键网络1.3.2 minimize 氢原子会进行能量最小化 2 ligand prepare3 生成对接盒子-receptor grid generation3.1 recepto…

Q1营收稳健增长,云从科技如何在“百模大战”的险中求稳?

自从迈入大模型时代&#xff0c;AI行业可谓“一天一个样”。越来越多的企业涌现&#xff0c;舆论热议从未断绝。 但就像所有技术必须经历的那些考验&#xff0c;在现实尺度下&#xff0c;AI顺利走进商业化世界&#xff0c;仍然是少部分玩家掌握的稀缺能力。个中原因不尽相同&a…

第49期|GPTSecurity周报

GPTSecurity是一个涵盖了前沿学术研究和实践经验分享的社区&#xff0c;集成了生成预训练Transformer&#xff08;GPT&#xff09;、人工智能生成内容&#xff08;AIGC&#xff09;以及大语言模型&#xff08;LLM&#xff09;等安全领域应用的知识。在这里&#xff0c;您可以找…

javase学习01-GUI设计中的菜单条,菜单及菜单项(简单的实现)

目录 一&#xff0c;效果及代码 二&#xff0c;相关内容 1&#xff0c;创建图片资源文件夹 2&#xff0c;菜单初识 3&#xff0c;图标大小设置 4&#xff0c;菜单高度设置 5&#xff0c;设置窗口的图标 ☀ 今天学习了Java的GUI&#xff08;graphics user interface&…
最新文章