惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
P
Palo Alto Networks Blog
T
ThreatConnect
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
T
True Tiger Recordings
P
Privacy & Cybersecurity Law Blog
B
Blog
IT之家
IT之家
Last Week in AI
Last Week in AI
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
C
Comments on: Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
N
News and Events Feed by Topic
NISL@THU
NISL@THU
腾讯CDC
雷峰网
雷峰网
Security Latest
Security Latest
李成银的技术随笔
M
Microsoft Research Blog - Microsoft Research
L
LangChain Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
博客园 - Franky
N
News | PayPal Newsroom
V
V2EX
A
About on SuperTechFans
The Register - Security
The Register - Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
爱范儿
爱范儿
A
Arctic Wolf
L
LINUX DO - 最新话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

博客园 - 码甲哥不卷

GLM模型这么火,咱们用vllm也咧一个呗! - 码甲哥不卷 同样都是九年义务教育,他知道的AI算力科普好像比我多耶 higress 这个中登才是AI时代的心头好 MetalLB才是给Ingress这个老登做负重前行的那个男人 超性感的轻量级openclaw平替,我给nanobot打call 我不允许谁还不清楚function call在AI-Agent领域中打手的地位 还有比ollama更傻瓜式的大模型本地部署方式吗 ? 🔎我不允许谁还分不清这三种watch机制的区别 云原生AI算力平台的架构解读 🚀糟糕,我实现的k8s informer好像是依托答辩 🎉在k8s调度的花园里面挖呀挖 - 码甲哥不卷 🎉卷不过AI算法, AI工程化或许是一个出路 - 码甲哥不卷 我是新来的,我需要知道这些吗?网关上的限流器 新来的外包,限流算法用的这么6 面试总被追问k8s调度器工作原理, 收藏 == 学废 kong网关反向代理grpc请求 幂等的双倍快乐,你值得拥有 JWT 这点小秘密,你们肯定知道! Go动态感知资源变更的技术实践,你指定用过!
新来的外包,在大群分享了它的限流算法的实现
码甲哥不卷 · 2025-11-19 · via 博客园 - 码甲哥不卷

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周期清理内存)。

var userLimiters = cache.New(time.Minute*30, 10) // 10 items per minute
func limiterForUser(userID string) *rate.Limiter {
	if v, found := userLimiters.Get(userID); found {
		return v.(*rate.Limiter)
	}

	l := rate.NewLimiter(rate.Every(time.Minute/60), 10)
	userLimiters.Set(userID, l, cache.DefaultExpiration)
	return l
}

// 更细化的限流: 针对同一用户的请求次数限速, 增加了细粒度的用户维度,需要维护 用户与对应限速器的映射关系
func userRatelimitMiddleware(c *gin.Context) {
	userID := c.GetString("userID")  //  从每个请求context的key中取得信息, 这个key对于req context是排他性的
	if userID == "" {
		c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
		return
	}
	if userID == "" {
		userID = c.GetString("x-api-key")
	}

	if userID == "" {
		userID = c.ClientIP()
	}
	limiter := limiterForUser(userID) // 通过userid维度找到对应的限速器
	if !limiter.Allow() {
		c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "Too many requests"})
		return
	}
	c.Next()
}

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

image

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

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

// 读取- 计算 - 重新赋值都在一个 lua 脚本里面
var redisScript = `
	local key = KEYS[1]
	local capacity = tonumber(ARGV[1])
	local rate = tonumber(ARGV[2])
	local now = tonumber(ARGV[3])
	local tokens =  tonumber(redis.call('hget', key, 'tokens') or '-1')
	local last_time = tonumber(redis.call('hget', key, 'last_time') or  '-1')

	if tokens  == -1 or last_time == -1 then
		tokens = capacity
		last_time = now
	else
		local elapsed = now - last_time
        if elapsed < 0 
			then elapsed = 0
		end
		local delta  = elapsed * rate / 1000
		tokens = tokens + delta
		if tokens > capacity then
			tokens = capacity
		end
        last_time = now
	end
	local allow = 0
	if tokens >= 1 then
		allow = 1
		tokens= tokens - 1
	else	
		allow = 0
	end

	redis.call('hset', key, 'tokens', tokens)
	redis.call('hset', key, 'last_time', last_time)
    redis.call('PEXPIRE', key,  math.max(1000, 2 * math.ceil((capacity / rate) * 5000)))
	return allow
`

注意

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

golang应用层的写法如下:

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

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

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

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