必须按请求来源分桶且跨实例共享状态才能有效防刷,全局单桶或每次新建限流器均失效;单机用sync.map缓存ip桶,集群须用redis+lua保证原子性。

直接用 rate.Limiter 做防刷中间件是常见做法,但仅靠全局单桶限流等于没防——恶意请求能轻易挤占所有配额,正常用户全被拦。真要防刷,必须按请求来源(IP 或用户标识)分桶,且多实例部署时得靠 Redis 保证原子性。
为什么不能直接 new 一个 rate.Limiter 放在中间件里
每次请求都 new rate.Limiter(...),等于每个请求拿一个全新空桶,Allow() 永远返回 true,完全失效。限流器必须复用,且状态需跨请求共享。
- 全局单例(如
var limiter = rate.NewLimiter(...))只能做服务级兜底,无法区分 IP 或用户 - 桶的生命周期必须与请求来源绑定:同一个 IP 的多次请求,要命中同一个桶
- Go 的
rate.Limiter本身无存储能力,需配合sync.Map(单机)或 Redis(集群)缓存各桶实例
按 IP 分桶限流的最小可行实现
适合单机部署、QPS 不高的管理后台或开放 API。核心是用 sync.Map 缓存每个 IP 对应的 *rate.Limiter 实例,Key 是客户端 IP,Value 是带独立速率的桶。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
- 从
c.Request().RemoteAddr提取真实 IP(注意反向代理场景需解析X-Forwarded-For) - 用
sync.Map.LoadOrStore(ip, newLimiter())获取或创建对应桶,避免重复初始化 - 调用
limiter.Allow()判断是否放行;失败则返回429,不进业务逻辑 - 示例关键片段:
func IPBasedRateLimit(next echo.HandlerFunc) echo.HandlerFunc { limiters := &sync.Map{} return func(c echo.Context) error { ip := getRealIP(c.Request()) limiter, _ := limiters.LoadOrStore(ip, rate.NewLimiter(rate.Every(1*time.Second), 5)) if !limiter.(*rate.Limiter).Allow() { return c.JSON(429, map[string]string{"error": "too many requests"}) } return next(c) } }
生产环境必须用 Redis + Lua 做分布式限流
多实例部署时,sync.Map 失效——同一 IP 的请求可能打到不同机器,各自维护独立桶,限流形同虚设。必须把桶状态提到共享存储层。
- 用 Redis 的
INCR+EXPIRE组合模拟令牌桶,或更稳妥地用 Lua 脚本一次性完成“读+判+写”原子操作 - Key 设计建议:
rate:ip:{ip}(IP 限流)或rate:user:{user_id}(JWT 解析后提取) - 注意 Redis 连接池配置,避免限流中间件自身成为瓶颈;超时设置建议 ≤ 50ms
- 若用
echo-contrib/redis,可复用其封装好的redis.Client,但限流逻辑仍需自行编写 Lua 脚本
真正难的不是写个 Allow() 判断,而是怎么让“同一个用户”的所有请求稳定落到同一个桶里,同时扛住横向扩容。IP 可伪造,user_id 需鉴权后才可信——防刷策略必须和你的认证链路对齐,否则中间件再严,入口处就漏了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










