基于Redis的滑动窗口限流-Golang实现

news/2025/11/18 20:22:22/文章来源:https://www.cnblogs.com/zmxhello/p/19239194

常用限流算法

包括固定窗口、滑动窗口、令牌桶、漏桶
固定窗口:
将时间划分为固定长度的窗口(如 1 秒),窗口内维护请求计数,当请求数超过阈值时拒绝新请求。实现简单,但存在窗口临界值问题;就是大量请求聚集在第一个窗口的最后和第二个窗口的最前
优点: 实现简单,开销小。
缺点: 存在典型的“窗口边界问题”:若大量请求集中在窗口末尾 + 下一个窗口开始,会出现瞬时突刺(burst),导致实际请求量明显超过设定值。
滑动窗口:
将固定窗口拆分为多个小时间片,通过滑动窗口覆盖的时间片总和作为最大请求量,这样就解决了窗口临界峰值问题,但精度越高(时间片越多),计算和存储开销越大
优点: 精度高,限流更平滑。
缺点: 越高精度(时间片越多),存储和计算成本越高。

漏桶算法:
请求进来后直接进入漏桶排序,漏桶以固定的顺序处理请求,如果新增请求的速率大于漏桶漏出的速率,多余的请求会溢出
特征: 输出速率固定,可平滑流量,但不允许突发。

令牌桶算法:
以固定的速率产生令牌,直到桶内的令牌满了。新请求进来后取出一个令牌才会放行,如果没有取到令牌则进入一个队列等待,如果等待队列满了,新请求会被直接抛弃。
特征: 允许突发(桶内累积的令牌),更灵活。

基于Redis的滑动窗口限流实现

需求分析

  • 滑动窗口算法的基本要点是回溯一段时间,得到这段时间内请求数量;
  • 将不在窗口内的时间戳删除,以节省内存

比如:假设窗口大小为1分钟,限制10个请求,那么也就是回溯一分钟看看过去1分钟是不是已经有了10个请求;
思路:记录每次请求的时间戳,插入队列中,查询时判断队列中数据量即可

选择数据结构

什么样的结构方便回溯一段窗口,并计算数量呢?

  • list
  • zset

更加方便删除, 有个命令ZCount轻松获取范围内的值;ZRemRangeByScore删除范围外的值,因此大多数都会选择zset

定义限流器对象

type MiddlewareBuilder struct {client redis.CmdablekeyGenFunc func(ctx *gin.Context) string	// 限流对象-基于字符串,可以是IP、UUID、ServerName等windowSize time.Duration						// 窗口大小limit int64												// 限流阈值
}

这里封装为gin框架中间件的形式:

func NewMiddlewareBuilder(client redis.Cmdable, windowSize time.Duration, limit int64) *MiddlewareBuilder {builder :=  &MiddlewareBuilder{client: client,keyGenFunc: func(ctx *gin.Context) string {return ctx.ClientIP()},windowSize: windowSize,limit: limit,}return builder
}

V1版本

很简单,共有5个步骤:

  • 统计最近窗口内的请求数
  • 判断是否超限,超限直接返回
  • 记录当前请求,插入zset
  • 删掉没用的窗口外的数据,就是当前时刻减去窗口大小之前的数据
  • 更新过期时间,因为限流往往是短时间集中的
func (b *MiddlewareBuilder) LimitMiddlewareV1() gin.HandlerFunc {return func(ctx *gin.Context) {now := time.Now().UnixNano()windowStart := now - b.windowSize.Nanoseconds()key := b.keyGenFunc(ctx)// 1. 统计窗口内的请求数(同时获取结果和错误)count, err := b.client.ZCount(ctx, key, fmt.Sprintf("%d", windowStart), fmt.Sprintf("%d", now)).Result()if err != nil {ctx.AbortWithStatus(500)return}// 2. 处理限流请求if count >= b.limit {ctx.AbortWithStatusJSON(429, gin.H{"error": "rate limit exceeded",})return}// 3. 记录当前请求_, err = b.client.ZAddNX(ctx, key, redis.Z{Score:  float64(now),Member: fmt.Sprintf("%d", now),}).Result()if err != nil {ctx.AbortWithStatus(500)return}// 4. 过期请求清理 (清理窗口外的请求)_, err = b.client.ZRemRangeByScore(ctx, key, "0", fmt.Sprintf("%d", windowStart)).Result()if err != nil {ctx.AbortWithStatus(500)return}// 5. 设置过期时间b.client.Expire(ctx, key, b.windowSize * 3)ctx.Next()}
}

但是这样会有并发问题:

  1. 请求 A 调用 ZCOUNT 判断未超限
  2. 请求 B 也判断未超限
  3. 两个请求一起通过,导致实际请求数超过限制

解决方式:
考虑事务?

但是redis的事务机制并不是真正原子性,比如事务中某条命令执行错误,其他命令照样可以执行成功;另外不支持回滚操作

因此更好方式基于lua脚本实现,实现很简单,以字符串方式即可,当然如果命令较多,可以单独保存到一个文件中:

const luaScript = `
local key = KEYS[1]
local windowStart = tonumber(ARGV[1])
local now = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local random = ARGV[4]  -- 新增随机数参数-- 生成唯一 member:毫秒时间 + 随机数
local member = now .. ":" .. random-- 统计窗口内的请求数
local count = redis.call('zcount', key, windowStart, now)
if count >= limit thenreturn 'false'
end-- 记录当前请求(member 唯一,不会覆盖)
redis.call('zadd', key, now, member)-- 清理窗口外的请求
redis.call('zremrangebyscore', key, '-inf', windowStart)-- 设置过期时间(窗口大小的 3 倍,单位秒)
redis.call('expire', key, math.ceil((now - windowStart) / 1000 * 3))return 'true'
`

然后执行方式是使用eval:

func (b *MiddlewareBuilder) LimitMiddlewareV2() gin.HandlerFunc {return func(ctx *gin.Context) {key := b.keyGenFunc(ctx)now := time.Now().UnixMilli()windowStart := now - b.windowSize.Milliseconds()random := rand.Intn(1000000)// 1. 执行 Lua 脚本allowed, err := b.client.Eval(ctx, luaScript, []string{key}, windowStart, now, b.limit, random).Bool()if err != nil {ctx.AbortWithStatus(500)fmt.Println(err)return}if !allowed {ctx.AbortWithStatusJSON(429, gin.H{"error": "rate limit exceeded",})return}}
}

运行& shell测试

func f6() {redisClient := redis.NewClient(&redis.Options{Addr:     "localhost:6379",Password: "",DB:       0,})limiter := ratelimit.NewMiddlewareBuilder(redisClient, time.Second * 10, 5)r := gin.Default()r.GET("/test-v1", limiter.LimitMiddlewareV1(), func(ctx *gin.Context) {ctx.JSON(200, gin.H{"msg": "success-v1"})})r.GET("/test-v2", limiter.LimitMiddlewareV2(), func(ctx *gin.Context) {ctx.JSON(200, gin.H{"msg": "success-v2"})})r.Run(":8080")
}

测试

上述代码调用了限流中间件,并且窗口大小设置为10s,阈值设置为5,执行shell脚本测试V2:

for i in {1..20}; docurl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8080/test-v2 &
done
wait

结果:

(base) xing@xing-2 ttt % sh ./test.sh
200
200
200
200
429
200
429
429
429
429
429
429
429
429
429
429
429
429
429
429

只有5个请求得到200状态码,其余被限流返回了429。

修改shell脚本测试v1接口,结果如下:

(base) xing@xing-2 ttt % sh ./test.sh
(base) xing@xing-2 ttt % sh ./test.sh
429
429
429
429
429
429
429
429
429
429
200
200
200
200
200
200
200
200
200
200

每次200的请求不相同,但是大都超过5个,并发问题明显!

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

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

相关文章

查看laya已经加载的资源

查看laya已经加载的资源Laya.Loader.loadedMap 不是Laya.loader, loadedMap是static的, 在Loader类中定义的.

ESP32 + LVGL 开发笔记(一):点亮屏幕

折腾了几天 lvgl,打算写一个简单的教程来记录一下。教程的最后会实现一下使用 lvgl 显示 lottie 动画的效果,见下图:环境介绍 硬件:esp32s3 n16r8核心板 1.47寸 ips lcd彩色屏幕(st7789)分辨率320 x 172,没有触屏…

聊聊deepseek对latex的辅助

聊聊deepseek对latex的辅助最近两天,用deepseek辅助latex写了一篇20页的反串讲文档,感觉结构清晰,且格式工整,一目了然。 很感慨的一点是,最开始的时候,是大学毕业论文时使用latex,当时就想着自己不用考虑work的…

【LVGL】图片部件

引言 图片部件(lv_img)图片部件相关 api 函数图片转换 LVGL 中要显示图片则需要 C 语言,或者 bin 的方式, 恰好 LVGL 官网提供了一个很好的在线工具:图像转换器 不过要注意的是选择自己当前的 LVGL 版本,下面是我…

linux c makefile

当然可以!在 Linux 系统中,makefile 是用于编译和构建项目的常用工具。它定义了项目的构建规则,帮助开发者自动化编译过程。一、什么是 Makefile? Makefile 是一个文本文件,它定义了如何编译和链接源代码。它告诉…

基于自适应遗传算法风光场景生成的电动汽车并网优化调度【IEEE33节点】(Matlab代码建立)

基于自适应遗传算法风光场景生成的电动汽车并网优化调度【IEEE33节点】(Matlab代码建立)pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !importa…

10大 spring源码设计模式 (图解+秒懂+史上最全)

本文 的 原文 地址 原始的内容,请参考 本文 的 原文 地址 本文 的 原文 地址 尼恩说在前面 在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、…

实用指南:《中国电力产业数字化》深度解析与前沿展望(下)——中国电力数字化转型路线图:SPARK 融合平台的设计与落地方案

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

High Frequency Active Auroral Research Program(HAARP)部分摘取

High Frequency Active Auroral Research Program(HAARP)部分摘取原网站:https://haarp.gi.alaska.edu/ 部分摘取:利用最近研发的强大且灵活的电离层加热器(如EISCAT加热器,以及最近建成的HAARP加热器)对高频无…

CF813E Army Creation

考虑套用 HH 的项链做法,如果一个数前面第 \(k\) 个数小于 \(l\) 则可以选,那么用主席树维护值域线段树即可。

Mac 怎么安装 PyCharm 2020.1.dmg?超简单教程(附安装包)

Mac 怎么安装 PyCharm 2020.1.dmg?超简单教程(附安装包)​ 一、下载文件 安装包下载:https://pan.quark.cn/s/c35137bf43ce , PyCharm 2020.1.dmg文件,一般是在浏览器下载后,放在了「下载」文件夹里。如果没有,…

C# 蓝牙远程控制应用:从零达成移动设备与硬件的无线交互

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

铭记旧友

命运,总是一个恶劣地笑着,将人间搅得天翻地覆的死神。这次祂将手伸向她的父母,以名为“期许”的毒药杀死了她。 也曾挣扎过,将呼救之声传入爱情之耳,却不知会陷入更深的泥泞。 缺爱吗?或许吧,但命运所施舍给她的…

标题:鸿蒙Next音频开发新篇章:深入解析Audio Kit(音频服务) - 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

Spring AI Alibaba 项目源码学习(十二)-完结:Tool

Tool 系统分析 请关注微信公众号:阿呆-bot 概述 本文档分析 Spring AI Alibaba Agent Framework 中的 Tool(工具)系统,包括工具的定义、注册、调用流程、扩展机制以及 AgentTool 的实现。 入口类说明 ToolCallback…

ftp,sftp,scp,tftp几种简单对比,以及python实现ftp功能

ftp,sftp,scp,tftp几种简单对比,以及python实现ftp功能对比如下:特性维度FTPSFTPSCPTFTP安全性 明文传输 基于SSH加密 基于SSH加密 无加密默认端口 21 22 22 69协议基础 TCP SSH SSH UDP认证方式 用户名/密码 多种(…

实用指南:深入解析音频编解码器(Audio CODEC):硬件、接口与驱动开发

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

福利MegaLLM–175刀免费额度建教程

0.简介 MegaLLM 是一个 API 中转服务,支持主流模型 OpenAI、Anthropic、Google、Meta 等,以及包括国产千问、DeepSeek、GLM、K2 等。可以在 Claude Code、 Codex、OpenCode、Kilocode、RooCode... 1. 注册就送 75 刀…

C# 常用控件(学习笔记8)

1. TreeView 树形控件/// <summary> /// 添加 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void BtnTreeAdd_…

模拟赛记录 11/18

显然不应该把别人的模拟赛指认成自己的。