🎨 新来的外包,在大群分享了它的限流算法的实现

作者:有态度的下等马日期:2025/11/20

1. 令牌桶按用户维度限流

前文golang/x/time/rate演示了基于整体请求速率的令牌桶限流;
那基于用户id、ip、apikey请求速率的限流(更贴近生产的需求), 阁下又该如何应对?

那这个问题就从全局速率变成了按照用户维度(group by userid)来做限流,那么

  • 早先的全局的rateLimiter就要变成人手一个令牌桶,也就是userid:rateLimiter的键值对集合,select count( * ) from table ---> select userid, count(*) from table group by userid
  • 使用缓存组件来存储维度键值对: 缓存的剔除机制来清理不再访问的键值对 (30min过期,10min周期清理内存)。
1var userLimiters = cache.New(time.Minute*30, 10) // 10 items per minute
2func limiterForUser(userID string) *rate.Limiter {
3	if v, found := userLimiters.Get(userID); found {
4		return v.(*rate.Limiter)
5	}
6
7	l := rate.NewLimiter(rate.Every(time.Minute/60), 10)
8	userLimiters.Set(userID, l, cache.DefaultExpiration)
9	return l
10}
11
12// 更细化的限流: 针对同一用户的请求次数限速, 增加了细粒度的用户维度,需要维护 用户与对应限速器的映射关系
13func userRatelimitMiddleware(c *gin.Context) {
14	userID := c.GetString("userID")  //  从每个请求context的key中取得信息, 这个key对于req context是排他性的
15	if userID == "" {
16		c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
17		return
18	}
19	if userID == "" {
20		userID = c.GetString("x-api-key")
21	}
22
23	if userID == "" {
24		userID = c.ClientIP()
25	}
26	limiter := limiterForUser(userID) // 通过userid维度找到对应的限速器
27	if !limiter.Allow() {
28		c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "Too many requests"})
29		return
30	}
31	c.Next()
32}
33

2. redis 作为限流器外置存储

这个思路也是极其常见的行为: redis可以成为用户令牌桶的全局中心存储: 当多个负载层需要读写用户限流器时,与redis交互。

本次通过golang的实战,深入理解基于redis的令牌桶限流器的算法实现。

① 请求到达负载层,被负载层识别为userid=junio

② 负载层请求redis获取该用户的token bucket的当前状态:hget userbucket:junio tokens last_time

③ 基于当前时间nowlast_time,计算流逝的时间,再根据rate计算这一阶段下发了多少tokens:delta=(now-last_time) * r/1000,加上redis原始记录的token,就是本次请求时bucket中能用的tokens, 注意:令牌数量最多不能超过cap

④ 如果tokens>=1, 表示桶中有令牌,可放行请求,tokens数量减1

⑤ 最后将本次处理完后的 tokens和last_time=now写入原用户令牌桶hset userbucket:junio tokens 20 last_time 990

使用redis 中的hashmap存储用户的tokenbucket状态,应用存在读取redis- 计算- 回写redis过程,使用redis lua的脚本执行三个动作,以保证线程安全。

为什么lua脚本能保证线程安全呢?
主要得益于 Redis 的单线程架构和原子性执行机制: 加载并执行lua脚本时所有的redis操作作为一个整体完成; 整个脚本执行期间没有其他命令可以插入。

1// 读取- 计算 - 重新赋值都在一个 lua 脚本里面
2var redisScript = `
3	local key = KEYS[1]
4	local capacity = tonumber(ARGV[1])
5	local rate = tonumber(ARGV[2])
6	local now = tonumber(ARGV[3])
7	local tokens =  tonumber(redis.call('hget', key, 'tokens') or '-1')
8	local last_time = tonumber(redis.call('hget', key, 'last_time') or  '-1')
9
10	if tokens  == -1 or last_time == -1 then
11		tokens = capacity
12		last_time = now
13	else
14		local elapsed = now - last_time
15        if elapsed < 0 
16			then elapsed = 0
17		end
18		local delta  = elapsed * rate / 1000
19		tokens = tokens + delta
20		if tokens > capacity then
21			tokens = capacity
22		end
23        last_time = now
24	end
25	local allow = 0
26	if tokens >= 1 then
27		allow = 1
28		tokens= tokens - 1
29	else	
30		allow = 0
31	end
32
33	redis.call('hset', key, 'tokens', tokens)
34	redis.call('hset', key, 'last_time', last_time)
35    redis.call('PEXPIRE', key,  math.max(1000, 2 * math.ceil((capacity / rate) * 5000)))
36	return allow
37`
38

注意

  • 上面还使用的redis expire机制: redis expire不是滑动过期,但是每次被请求触发执行的时候就重新设置TTL, 表现为“滑动过期”。
  • 除了hset/hget ,还有hmget可用,另外这些操作还有配套的TTL指令,eg:hset key EXAT 1740470400 FIELDS 2 field1 "Hello" field2 "World"

golang应用层的写法如下:

1func (r *RedisLimiter) Allow(c *gin.Context, userid string) bool {
2	key := r.keyprefix + userid // 定位这个用户的token bucket
3	now := time.Now().UnixMilli()
4	// Check if the key exists in Redis
5	rCmd := r.redis.Eval(redisScript, []string{key}, r.cap, r.rate, now)
6	res, err := rCmd.Result()
7	if err != nil {
8		log.Printf("get from redis failure. ", err)
9		return false
10	}
11	if allow, ok := res.(int64); ok { // 注意:lua返回的0,1 值对应golang的int64
12		log.Printf("%v %v \n", allow, res)
13		return allow == 1
14	} else {
15		log.Printf("get from redis failure. ", err)
16		return false
17	}
18}
19

3. 总结展望

至此限流第二弹结束了,本文紧接掘金爆文🎨 新来的外包,限流算法用的这么6,进一步讲述了
① 实现根据特定业务维度的限流: 从全局限流器转换成针对业务维度的人手一个限流器;

② redis作为限流计数器的外置存储,令牌桶算法在redis上的算法实现:核心是使用hashmap存储当前请求用户的令牌桶状态(current_tokens, last_time), 落地时注意使用lua脚本避免竞态条件。

后面35+外包er针对限流设计还会再更新几个彩蛋, 期待一键三连,交个朋友, 35+报团不迷路。


🎨 新来的外包,在大群分享了它的限流算法的实现》 是转载文章,点击查看原文


相关推荐


IIoT 数据接口契约化工具JSON、OPC UA和Sparkplug B 优缺点对比分析
RockHopper20252025/11/19

本文以IIoT(Industrial Internet of Things)的核心需求为背景,系统性论述“数据接口契约化”的必要性,并对 JSON、OPC UA、Sparkplug B 三者作为“契约化工具(Contract Enforcement Mechanisms)”的优缺点作对比分析。 一、为什么 IIoT 需要“数据接口契约化” IIoT 的本质是:跨设备、跨系统、跨生命周期的数据互操作。 没有契约,就没有稳定接口;没有稳定接口,就没有可维护的生态。 1. 设备异构性极高 各


【微服务】【Nacos 3】 ② 深度解析:AI模块介绍
小毅&Nora2025/11/17

📖目录 前言1. Nacos AI 模块概述2. 核心组件详解2.1 MCP (Model Control Plane)2.1.1 核心功能2.1.2 关键类分析McpServerOperationService索引机制 2.1.3 控制器层 2.2 A2A (Agent to Agent)2.2.1 核心功能2.2.2 关键类分析A2aServerOperationService请求处理器 3. 关键源码剖析3.1 模型服务注册流程3.2 代理通信处理流程


Python 的内置函数 print
IMPYLH2025/11/16

Python 内建函数列表 > Python 的内置函数 print Python 的内置函数 print() 是编程中最常用的输出函数之一,主要用于将指定的内容输出到标准输出设备(通常是控制台)。它的基本语法如下: print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False) 参数详解: *objects:可接收多个对象参数,会依次打印这些对象。例如: print("Hello", "World") # 输出:H


C语言是什么编译? | 了解C语言编译过程及其重要性
cbarur_2892025/11/15

乐高编程机器人|探索创意与技术结合的无限可能乐高编程机器人结合了乐高积木的创造性和编程的逻辑性,是一种非常适合青少年学习的科技玩具。它不仅能够培养孩子们的动手能力,还能激发他们对编程的兴趣,从而提升解决问题的能力。乐高机器人通常配备了多种传感器和电动机,可以根据编程指令执行各种复杂的任务,例如行走、避障、抓取物体等。随着科技的不断进步,乐高编程机器人也不断更新换代,添加了更多高科技的功能。例如,最新版本的乐高机器人可以通过蓝牙连接到手机或电脑,进行远程控制和编程。通过这种方式,孩子们可以在编程的


Python 的内置函数 len
IMPYLH2025/11/14

Python 内建函数列表 > Python 的内置函数 len Python 的内置函数 len() 是一个常用的内置函数,主要用于返回对象的长度或项目数量。它可以应用于多种数据类型,包括但不限于以下几种: 字符串(str):返回字符串中的字符数量。例如: text = "Hello, World!" print(len(text)) # 输出:13 列表(list):返回列表中元素的数量。例如: numbers = [1, 2, 3, 4, 5] print(len(number


Android 修改项目包名,一键解决.
没有了遇见2025/11/13

需求: 项目需要 需要改变包名和签名. 版本 Windows 10 实现 1.修改设置 2:shift+F6 3: 确认修改


uniapp之WebView容器原理详解
前端一课2025/11/11

UniApp WebView容器原理详解 一、WebView容器概述 UniApp之所以能够实现跨平台开发,其核心原理是"uniapp 主要是运行在webview容器中的"。这句话揭示了UniApp的底层架构:它本质上是一个基于WebView的混合应用框架。 1.1 什么是WebView容器 WebView是一种系统组件,可以嵌入到原生应用中,用于显示网页内容。它本质上是一个迷你浏览器,提供了网页渲染、JavaScript执行环境以及与原生功能的桥接能力。 // WebView的基本结构(概念图


从零搭建:json-server+Bootstrap+OpenAI 全栈 AI 小项目
有意义2025/11/9

在前后端分离与大模型应用日益普及的当下,我搭建了这个集模拟后端、交互前端、AI 响应于一体的实战项目。 本笔记将详细记录从项目结构搭建、依赖安装,到接口模拟、页面开发、大模型对接的完整流程,核心技术涵盖 json-server 后端模拟、Bootstrap 前端布局、JavaScript fetch 数据交互及 OpenAI API 集成。 通过这个轻量化项目,可快速掌握前后端通信逻辑与大模型的实际应用,适合作为入门级全栈 + AI 开发的实践参考。 初步准备 在启动项目开发前,需完成基础环境与


Redis(109)Redis的Pipeline如何使用?
Victor3562025/11/7

Redis 的 Pipeline 机制允许客户端在一次网络请求中发送多个命令,并在一次网络响应中接收所有命令的结果。这种机制可以显著提高批量操作的性能,因为它减少了每个命令的网络往返延迟。 1. Pipeline 的基本使用 在 Jedis 客户端中,使用 Pipeline 可以通过 Jedis.pipelined() 方法实现。 示例代码 import redis.clients.jedis.Jedis; import redis.clients.jedis.Pipeline; public


Python 的内置函数 frozenset
IMPYLH2025/11/2

Python 内建函数列表 > Python 的内置函数 frozenset def frozenset(x): ''' 类型转换为 frozenset :param x: 一个变量 :return: 转换为 frozenset 后的值 ''' Python 的内置函数 frozenset 用于创建一个不可变的集合对象,它继承了普通集合(set)的所有特性,如无序性、元素唯一性等,但关键区别在于 frozenset 一旦创建就不能被修改,因此它是可

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2025 聚合阅读