Go语言实现简单限流功能_Golang中间件实战项目

用time.Ticker实现固定窗口限流简单但易超限,因窗口切换存在竞态和时钟漂移;推荐使用golang.org/x/time/rate的漏桶模型,支持突发、线程安全且性能优;分布式场景需Redis等外部存储协调。

time.Ticker 做固定窗口限流,简单但容易超限

固定窗口限流最直观:每秒最多处理 N 次请求,到整秒重置计数器。但问题在于边界——比如 0.9s 到 1.1s 这 200ms 内,可能触发两个窗口的计数(0s 窗口剩 1 次 + 1s 窗口刚清零),实际通过 2×N 次请求。

time.Ticker 配合原子计数器能快速验证逻辑,但不适合生产环境的精度要求:

var (
    limit = 10
    count int64
    mu    sync.RWMutex
)

ticker := time.NewTicker(time.Second) go func() { for range ticker.C { mu.Lock() count = 0 mu.Unlock() } }()

// 在 handler 中: mu.RLock() c := atomic.LoadInt64(&count) mu.RUnlock() if c >= int64(limit) { http.Error(w, "rate limited", http.StatusTooManyRequests) return } atomic.AddInt64(&count, 1)

  • 不考虑并发安全时,count++ 会出错;必须用 atomicsync.Mutex
  • time.Ticker 不保证严格准时,尤其在 GC 或系统负载高时会有漂移
  • 窗口切换瞬间的竞态无法避免,真实流量下会漏放行

golang.org/x/time/rate 实现平滑漏桶

标准库扩展包 rate.Limiter 是 Go 官方推荐方案,底层是“漏桶”模型:以恒定速率向桶中“漏水”,每次请求需先“取水”。它支持突发(burst)和平均速率(rps),且线程安全、无锁路径优化好。

关键参数含义:

  • rate.Every(100 * time.Millisecond) 表示每 100ms 放行 1 次 → 等价于 10 rps
  • burst = 5 表示桶容量为 5,允许短时突发 5 次请求
  • 首次调用 Wait() 会阻塞,TryConsume() 则立即返回布尔值
import "golang.org/x/time/rate"

var limiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 5)

func handler(w http.ResponseWriter, r *http.Request) { if !limiter.TryConsume(1) { http.Error(w, "too many requests", http.StatusTooManyRequests) return } // 处理业务逻辑 }

注意:TryConsume(1) 中的 1 是“令牌数”,一般单请求消耗 1;若接口权重不同(如上传消耗 3),可动态传入。

中间件中集成限流器,避免每个 handler 重复写判断

把限流逻辑抽成 HTTP 中间件,复用性更高,也便于统一响应头(如 X-RateLimit-Remaining)。

常见错误是把 rate.Limiter 实例定义在函数内,导致每次请求新建一个 limiter,完全失效:

  • ✅ 正确:全局变量或依赖注入方式传递同一个 *rate.Limiter
  • ❌ 错误:limiter := rate.NewLimiter(...) 写在 handler 函数里
  • ⚠️ 注意:不要在中间件里对每个请求都调用 SetLimit()SetBurst(),有锁开销且非线程安全
func RateLimitMiddleware(limiter *rate.Limiter) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if !limiter.TryConsume(1) {
                w.Header().Set("X-RateLimit-Lim

it", "10") w.Header().Set("X-RateLimit-Remaining", "0") http.Error(w, "rate limited", http.StatusTooManyRequests) return } // 更新响应头(剩余令牌数) remaining := limiter.Burst() - int(limiter.ReserveN(time.Now(), 1).TokensFromBucket()) w.Header().Set("X-RateLimit-Remaining", strconv.Itoa(remaining)) next.ServeHTTP(w, r) }) } }

// 使用: http.Handle("/api/", RateLimitMiddleware( rate.NewLimiter(rate.Limit(10), 10), )(http.HandlerFunc(apiHandler)))

分布式场景下 rate.Limiter 失效,得换方案

rate.Limiter 是纯内存实现,多实例部署时各自维护独立桶,总通过量变成 N × 单机 limit。此时必须引入外部存储做协调:

  • Redis + Lua 脚本(如 INCR + EXPIRE 组合)是最常用解法,能保证原子性
  • 如果已用 etcd,可用其 lease + key TTL 实现分布式令牌桶
  • 避免用 MySQL 计数器:高并发下行锁争抢严重,延迟不可控

别低估网络开销——一次 Redis 请求约 0.2~1ms,而本地 TryConsume 是纳秒级。高频小接口加 Redis 限流,可能让 P99 延迟翻倍。

真正需要分布式限流的,往往是网关层(如基于 ginecho 的 API 网关),而不是每个微服务内部自己搞一套。