1. 项目概述为什么需要精准Token限流上周团队里有个新上线的Go网关服务差点酿成事故——由于未做请求限流某合作方突然发起大量高频请求15分钟内产生了近万元的云服务账单。这件事让我意识到在微服务架构中网关作为流量入口必须实现精准的请求控制。传统的IP限流在面对Token认证体系时会显得力不从心比如同一个IP可能对应多个用户企业NAT出口不同Token可能有差异化的QPS权限VIP用户 vs 普通用户突发流量可能导致雪崩效应如秒杀场景经过多轮方案对比我们最终采用RedisLua实现了一套基于Token的分布式限流系统。实测在百万级QPS压力下平均延迟仅增加1.2ms且完美避免了天价账单的再次出现。2. 核心设计思路2.1 限流算法选型常见的四种限流算法对比如下算法类型实现复杂度平滑度内存消耗适用场景计数器固定窗口低差低简单粗暴的防刷场景滑动日志窗口高优秀高金融级精准控制漏桶算法中优秀中流量整形令牌桶算法中优秀中突发流量处理我们选择令牌桶算法因其独特的优势允许突发流量短时间内消耗积攒的令牌支持动态调整速率通过修改Redis中存储的配置实现相对简单Redis原子操作即可支持2.2 数据结构设计在Redis中为每个Token维护一个Hash结构HSET rate_limit:token123 burst 100 # 桶容量 rate 50 # 每秒生成令牌数 last_time 1630000000 # 最后更新时间戳 tokens 80 # 当前令牌数这种设计相比简单的KV存储有以下好处避免为每个字段单独建立Key可通过HMGET一次性获取所有参数方便后续扩展新字段3. 关键实现细节3.1 Lua脚本实现使用Lua保证原子性是本方案的核心要点。以下是核心逻辑local key KEYS[1] local now tonumber(ARGV[1]) local requested tonumber(ARGV[2]) local burst tonumber(ARGV[3]) local rate tonumber(ARGV[4]) local data redis.call(HMGET, key, tokens, last_time) local tokens tonumber(data[1]) or burst local last_time tonumber(data[2]) or now -- 计算时间差并生成新令牌 local delta math.max(0, now - last_time) local new_tokens math.min(burst, tokens delta * rate) -- 判断是否允许请求 if new_tokens requested then redis.call(HMSET, key, tokens, new_tokens - requested, last_time, now) return 1 else redis.call(HSET, key, last_time, now) return 0 end重要提示必须使用tonumber()显式转换类型否则Redis返回的字符串会导致计算错误3.2 Go集成方案在Go中通过redis.Eval调用Lua脚本func AllowRequest(token string, n int) (bool, error) { keys : []string{fmt.Sprintf(rate_limit:%s, token)} now : time.Now().Unix() args : []interface{}{now, n, config.Burst, config.Rate} res, err : redisClient.Eval( context.Background(), luaScript, keys, args..., ).Result() if err ! nil { return false, err } return res.(int64) 1, nil }实测性能数据单次调用平均耗时1.3ms本地Redis100并发QPS约7500次/秒内存占用每个Token约150字节4. 生产环境优化技巧4.1 热点Key处理当某个Token突然变成热点时可以采用本地缓存在Go服务内存中缓存热点Token的限流结果设置合理的过期时间分片存储对高频Token使用多个Key如token123_1, token123_2提前加载在网关启动时预热已知的高频Token配置4.2 动态配置更新通过Redis Pub/Sub实现实时配置更新func watchConfigChanges() { pubsub : redisClient.Subscribe(context.Background(), rate_limit_updates) ch : pubsub.Channel() for msg : range ch { var update ConfigUpdate if err : json.Unmarshal([]byte(msg.Payload), update); err nil { updateLocalConfig(update) } } }4.3 监控与告警建议采集以下指标限流触发次数按Token分类Redis操作延迟内存使用量增长趋势使用Prometheus的示例配置metrics: key: gateway_rate_limit labels: - token_type - status buckets: [10, 50, 100, 500]5. 常见问题排查5.1 Redis连接池调优我们曾遇到连接泄漏导致的问题解决方案redis.NewClient(redis.Options{ PoolSize: 1000, // 根据QPS调整 MinIdleConns: 100, // 保持最小空闲连接 IdleTimeout: 5 * time.Minute, })5.2 Lua脚本超时当Redis负载高时可能出现脚本执行超时建议优化脚本复杂度避免循环操作设置适当的脚本超时时间redisClient.ConfigSet(context.Background(), lua-time-limit, 5000)5.3 突发流量处理对于秒杀类场景可以采用分级限流策略第一层全局QPS限制简单计数器第二层用户级令牌桶第三层业务特定规则如库存校验6. 扩展思考这套方案稍作改造就能支持更多场景API计费系统将Token替换为AppKey爬虫控制结合User-Agent和IP多维度限流灰度发布为不同用户组设置不同速率我在实际使用中发现配合HTTP头部的Retry-After返回能显著提升用户体验if !allowed { w.Header().Set(Retry-After, 1) // 1秒后重试 w.WriteHeader(http.StatusTooManyRequests) }
Redis+Lua实现高并发Token限流系统设计与实践
1. 项目概述为什么需要精准Token限流上周团队里有个新上线的Go网关服务差点酿成事故——由于未做请求限流某合作方突然发起大量高频请求15分钟内产生了近万元的云服务账单。这件事让我意识到在微服务架构中网关作为流量入口必须实现精准的请求控制。传统的IP限流在面对Token认证体系时会显得力不从心比如同一个IP可能对应多个用户企业NAT出口不同Token可能有差异化的QPS权限VIP用户 vs 普通用户突发流量可能导致雪崩效应如秒杀场景经过多轮方案对比我们最终采用RedisLua实现了一套基于Token的分布式限流系统。实测在百万级QPS压力下平均延迟仅增加1.2ms且完美避免了天价账单的再次出现。2. 核心设计思路2.1 限流算法选型常见的四种限流算法对比如下算法类型实现复杂度平滑度内存消耗适用场景计数器固定窗口低差低简单粗暴的防刷场景滑动日志窗口高优秀高金融级精准控制漏桶算法中优秀中流量整形令牌桶算法中优秀中突发流量处理我们选择令牌桶算法因其独特的优势允许突发流量短时间内消耗积攒的令牌支持动态调整速率通过修改Redis中存储的配置实现相对简单Redis原子操作即可支持2.2 数据结构设计在Redis中为每个Token维护一个Hash结构HSET rate_limit:token123 burst 100 # 桶容量 rate 50 # 每秒生成令牌数 last_time 1630000000 # 最后更新时间戳 tokens 80 # 当前令牌数这种设计相比简单的KV存储有以下好处避免为每个字段单独建立Key可通过HMGET一次性获取所有参数方便后续扩展新字段3. 关键实现细节3.1 Lua脚本实现使用Lua保证原子性是本方案的核心要点。以下是核心逻辑local key KEYS[1] local now tonumber(ARGV[1]) local requested tonumber(ARGV[2]) local burst tonumber(ARGV[3]) local rate tonumber(ARGV[4]) local data redis.call(HMGET, key, tokens, last_time) local tokens tonumber(data[1]) or burst local last_time tonumber(data[2]) or now -- 计算时间差并生成新令牌 local delta math.max(0, now - last_time) local new_tokens math.min(burst, tokens delta * rate) -- 判断是否允许请求 if new_tokens requested then redis.call(HMSET, key, tokens, new_tokens - requested, last_time, now) return 1 else redis.call(HSET, key, last_time, now) return 0 end重要提示必须使用tonumber()显式转换类型否则Redis返回的字符串会导致计算错误3.2 Go集成方案在Go中通过redis.Eval调用Lua脚本func AllowRequest(token string, n int) (bool, error) { keys : []string{fmt.Sprintf(rate_limit:%s, token)} now : time.Now().Unix() args : []interface{}{now, n, config.Burst, config.Rate} res, err : redisClient.Eval( context.Background(), luaScript, keys, args..., ).Result() if err ! nil { return false, err } return res.(int64) 1, nil }实测性能数据单次调用平均耗时1.3ms本地Redis100并发QPS约7500次/秒内存占用每个Token约150字节4. 生产环境优化技巧4.1 热点Key处理当某个Token突然变成热点时可以采用本地缓存在Go服务内存中缓存热点Token的限流结果设置合理的过期时间分片存储对高频Token使用多个Key如token123_1, token123_2提前加载在网关启动时预热已知的高频Token配置4.2 动态配置更新通过Redis Pub/Sub实现实时配置更新func watchConfigChanges() { pubsub : redisClient.Subscribe(context.Background(), rate_limit_updates) ch : pubsub.Channel() for msg : range ch { var update ConfigUpdate if err : json.Unmarshal([]byte(msg.Payload), update); err nil { updateLocalConfig(update) } } }4.3 监控与告警建议采集以下指标限流触发次数按Token分类Redis操作延迟内存使用量增长趋势使用Prometheus的示例配置metrics: key: gateway_rate_limit labels: - token_type - status buckets: [10, 50, 100, 500]5. 常见问题排查5.1 Redis连接池调优我们曾遇到连接泄漏导致的问题解决方案redis.NewClient(redis.Options{ PoolSize: 1000, // 根据QPS调整 MinIdleConns: 100, // 保持最小空闲连接 IdleTimeout: 5 * time.Minute, })5.2 Lua脚本超时当Redis负载高时可能出现脚本执行超时建议优化脚本复杂度避免循环操作设置适当的脚本超时时间redisClient.ConfigSet(context.Background(), lua-time-limit, 5000)5.3 突发流量处理对于秒杀类场景可以采用分级限流策略第一层全局QPS限制简单计数器第二层用户级令牌桶第三层业务特定规则如库存校验6. 扩展思考这套方案稍作改造就能支持更多场景API计费系统将Token替换为AppKey爬虫控制结合User-Agent和IP多维度限流灰度发布为不同用户组设置不同速率我在实际使用中发现配合HTTP头部的Retry-After返回能显著提升用户体验if !allowed { w.Header().Set(Retry-After, 1) // 1秒后重试 w.WriteHeader(http.StatusTooManyRequests) }