Valkey连接池必须启用读写分离与连接复用,使用ClusterClient、合理配置连接数、禁用网络超时、TLS需显式配置;令牌桶操作须用Lua脚本保证原子性;Key设计需含租户ID与API路径哈希;降级策略应熔断+本地兜底并上报Metrics。

Valkey连接池配置必须启用读写分离与连接复用
Valkey(原Redis)在分布式限流场景下,高频的INCR、EXPIRE、GET操作对连接稳定性极其敏感。默认的redis-go客户端(如github.com/redis/go-redis/v9)不自动复用连接,短连接会迅速耗尽服务端连接数或触发TIME_WAIT堆积。
- 务必使用
redis.NewClusterClient()而非redis.NewClient()——即使单节点部署也应走Cluster模式,它内置连接池管理且兼容Valkey 7+的RESP3协议 -
MaxActiveConns设为0(不限制)或至少50,MinIdleConns不低于10,避免冷启动时频繁建连 - 禁用
ReadTimeout和WriteTimeout设为0(即永不超时),限流逻辑本身不能因网络抖动失败;改用业务层兜底超时(如context.WithTimeout包裹单次令牌桶操作) - 若Valkey启用了TLS,需显式设置
TLSConfig并验证证书链,否则redis: connection closed错误频发
令牌桶原子操作必须用Lua脚本封装
单纯靠INCR+EXPIRE两步无法保证原子性:若INCR成功但EXPIRE失败,会导致桶永久存在;并发请求还可能绕过限流。Valkey原生支持Lua,必须将桶初始化、令牌消耗、时间窗口续期全部压进一个脚本。
示例脚本(保存为rate_limit.lua):
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local window = tonumber(ARGV[4])
<p>local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or capacity
local last_refill = tonumber(bucket[2]) or now</p><p>local elapsed = now - last_refill
local refill = math.floor(elapsed * rate)
tokens = math.min(capacity, tokens + refill)</p><p>local allowed = tokens >= 1
if allowed then
tokens = tokens - 1
end</p><p>redis.call('HMSET', key, 'tokens', tokens, 'last_refill', allowed and now or last_refill)
redis.call('EXPIRE', key, window + 1)
return { allowed and 1 or 0, tokens }Go中调用方式:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
立即学习“go语言免费学习笔记(深入)”;
- 用
script.Load(ctx, rdb).Run(ctx, rdb, []string{key}, args...)执行,args按顺序传入capacity、rate、time.Now().UnixMilli()、windowSeconds - 返回值是
[]interface{},需手动转int64判断是否允许通过 - 脚本中
EXPIRE时间设为window + 1秒,防止因系统时钟漂移导致桶提前失效
Key设计要包含租户ID与API路径哈希
分布式环境下,同一接口被不同租户调用必须隔离计数,否则A租户的请求会挤占B租户配额。直接拼接字符串易导致Key过长或含非法字符,必须标准化。
- Key格式:
rl:{tenant_id}:{sha256(api_path)},其中tenant_id来自JWT或Header,api_path取r.URL.Path(不含Query参数) - 用
fmt.Sprintf("rl:%s:%x", tenantID, sha256.Sum256([]byte(path)).[8:])生成8字节哈希,兼顾唯一性与长度控制(Valkey Key建议≤1KB) - 禁止将用户ID、IP等高频变动字段放入Key——会导致缓存击穿,桶数量爆炸
- 若需多级限流(如租户级+接口级+IP级),各层级Key独立,不嵌套;用不同TTL区分优先级(租户级TTL长,IP级TTL短)
降级策略要绕过Valkey直接放行或拒绝
Valkey不可用时,硬限流会阻塞所有请求。必须实现熔断+本地内存兜底,但注意本地计数无法跨进程同步,仅适用于“宁可误放行,不可误拦截”的场景。
- 用
github.com/sony/gobreaker包装Valkey调用,cb.Execute失败时进入降级分支 - 降级逻辑分两种:1)返回
http.StatusTooManyRequests强制拒绝(适合支付类强管控接口);2)用sync.Map记录tenant_id+path最近1分钟请求数,超阈值才拒绝(适合查询类弱管控接口) - 本地计数器必须带TTL清理,否则内存泄漏;可用
time.AfterFunc定时清理过期项,或改用github.com/bluele/gcache自动驱逐 - 切记:降级状态需上报Metrics(如Prometheus
rate_limit_fallback_total计数器),否则故障时无法感知
Valkey集群拓扑变更、Lua脚本版本不一致、租户ID解析为空这三类问题最常导致限流失效,上线前务必用真实流量压测验证Key生成逻辑与脚本返回值解析路径。

















