rate.limiter不能直接用于分布式http限流,因其是单机内存态、不共享状态;需用redis+lua实现原子化令牌桶,按路径或用户id维度限流,并辅以本地缓存降压。

为什么 rate.Limiter 不能直接套在 HTTP handler 里做分布式限流
因为 rate.Limiter 是内存态、单实例的,节点间不共享状态。你在一个服务实例上限制了每秒 100 次读请求,另一个实例照样能扛住另外 100 次——总量就翻倍了,根本不是“分布式”流量整形。真实场景下,文件读写请求往往带路径(如 /files/abc123)或用户 ID(如 uid=789),需要按维度聚合限流,而不是粗暴地全局限速。
用 Redis + Lua 实现原子化令牌桶(以文件路径为 key)
核心思路:把桶状态存在 Redis,每次读/写请求到来时,用 Lua 脚本一次性完成「取令牌→更新剩余数→设置过期」三步,避免竞态。注意别用 INCR + EXPIRE 分两步,那是非原子的,高并发下会超发。
示例 Lua 脚本(存为 bucket.lua):
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local fillRate = tonumber(ARGV[2]) -- tokens per second
local now = tonumber(ARGV[3])
local lastTime = tonumber(redis.call('HGET', key, 'last_time') or '0')
local tokens = tonumber(redis.call('HGET', key, 'tokens') or tostring(capacity))
<p>local delta = math.min((now - lastTime) * fillRate, capacity)
tokens = math.min(capacity, tokens + delta)
local allowed = (tokens >= 1)
if allowed then
tokens = tokens - 1
end
redis.call('HMSET', key, 'tokens', tokens, 'last_time', now)
redis.call('EXPIRE', key, 3600)
return allowed and 1 or 0</p>
Go 中调用它:
- 用
redis.Client.Eval执行脚本,KEYS[1]设为"bucket:read:" + filepath.Base(req.URL.Path) -
ARGV传入容量、填充速率、当前 Unix 时间戳(用time.Now().UnixMilli(),Lua 里除以 1000) - 返回 1 表示放行,0 表示拒绝,HTTP 状态码返回
429 Too Many Requests - 注意 Redis 连接池要配够,否则限流逻辑本身成瓶颈
写请求比读请求更需严格整形:避免小文件高频刷盘
读请求失败可重试或降级,但写请求一旦成功就必须落盘。如果不限制写频次,用户可能用脚本每毫秒 POST 一个 1KB 文件,迅速打满磁盘 I/O 或触发 inode 耗尽。这时不能只看 QPS,得结合「单位时间总字节数」和「单次最大尺寸」双重控制。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
建议策略:
- 对每个
uid单独建桶,key 为"bucket:write:uid_" + uid - 桶容量按字节设(比如 10MB/小时),填充速率也用字节/秒(如 3KB/s)
- 在 handler 开头解析
Content-Length,若为 -1(分块传输)则先收前几 KB 判断是否恶意小包 - 拒绝时返回
Retry-After: 60,明确告知冷却时间,方便客户端退避
本地缓存 + 分布式兜底:降低 Redis 压力但不牺牲一致性
纯靠 Redis 会有延迟和网络抖动风险。可在 Go 服务内加一层 LRU 缓存(如 groupcache 或 bigcache),缓存「已通过限流的 key → 剩余令牌数」,但必须设短 TTL(比如 10 秒),且每次 Lua 执行后主动失效对应缓存项。关键点在于:缓存只是加速,不是权威;所有决策最终以 Redis Lua 脚本返回为准。
容易被忽略的细节:
- Redis 时间要用
TIME命令或传入服务端time.Now(),别用 Lua 的os.time()(不同节点时间不同步) - 桶 key 命名必须包含租户/用户维度,否则多租户场景下互相干扰
- 文件系统实际写入失败(如磁盘满)不能被流量整形捕获,得单独监控
write syscall错误率
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










