gin-contrib/limiter不支持运行时动态读取用户等级,因其限流key在初始化阶段即固定,无法获取auth后设置的c.GetString("user_level");必须将限流中间件置于auth之后,按等级动态创建rate.Limiter并用sync.Map缓存,多实例需Redis+Lua实现分布式限流。

为什么不能直接用 gin-contrib/limiter 做用户等级限流
它不支持运行时动态读取用户身份和等级,所有限流 key 都在中间件初始化阶段就固定了——比如 c.ClientIP() 或静态字符串,压根拿不到 c.GetString("user_level") 这种 auth 后才有的值。更关键的是,它的限流器实例是全局单例或按路径预设的,没法为 VIP 用户开 100 QPS、普通用户只给 10 QPS 这种差异化策略。
必须把限流逻辑放在 auth 中间件之后
用户等级(如 "level")通常由鉴权中间件写入 c.Set("user_level", "vip"),后续中间件才能通过 c.GetString("user_level") 获取。如果限流中间件注册顺序在 auth 之前,c.GetString("user_level") 返回空字符串,就会退化成 IP 级限流,甚至因 key 为空导致 panic。
- 注册顺序必须是:
router.Use(authMiddleware)→router.Use(rateLimitByLevel()) - 在限流中间件里,先调
level := c.GetString("user_level"),再 fallback 到c.ClientIP()(防未登录场景) - 别用
c.Request.Header.Get("X-User-Level")替代 —— 容易被伪造,且绕过 auth 校验逻辑
用 sync.Map + rate.Limiter 实现 per-level 动态桶
每个用户等级对应一个独立的 *rate.Limiter 实例,避免 VIP 和普通用户互相挤占令牌。用 sync.Map 缓存,key 是等级字符串(如 "vip"),value 是 *rate.Limiter。注意:rate.Limiter 本身并发安全,不用额外加锁。
- VIP 用户:用
rate.NewLimiter(rate.Every(time.Millisecond*10), 100)(100 QPS,突发 100) - 普通用户:用
rate.NewLimiter(rate.Every(time.Second/10), 10)(10 QPS,突发 10) - 务必加过期清理:对 5 分钟内没访问过的 level key,调
sync.Map.Delete(),否则内存持续增长 - 不要在每次请求都 new Limiter —— 初始化开销虽小,但高频创建+GC 会拖慢吞吐
多实例部署时必须切到 Redis+Lua
单机 sync.Map 只能管本进程流量,负载均衡后多个 Gin 实例之间无法同步令牌余量。此时用户等级限流会失效:VIP 请求可能被不同实例重复放行,总 QPS 超出预期。
- Redis key 设计示例:
rate:level:<code>vip:api/v1/submit,其中vip来自c.GetString("user_level"),路径用c.FullPath()而非原始 URL - Lua 脚本要原子完成:incr + expire + 判断是否超限,三步不能拆
- 别漏掉连接池配置:
redis.NewClient(&redis.Options{PoolSize: 50}),否则高并发下 Redis 连接耗尽 - 降级策略要明确:Redis 不可用时,是 fallback 到本地限流(可能不准),还是直接放行(风险高)?建议 fallback 到保守的全局限流
真正难的不是写个限流函数,而是确保用户等级字段在链路中稳定传递、各实例间限流状态一致、以及降级路径不破坏业务 SLA。这些地方一松动,动态 QPS 就变成“动态失效”。


















