用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++会出错;必须用atomic或sync.Mutex -
time.Ticker不保证严格准时,尤其在 GC 或系统负载高时会有漂移 - 窗口切换瞬间的竞态无法避免,真实流量下会漏放行
用 golang.org/x/time/rate 实现平滑漏桶
标准库扩展包 rate.Limiter 是 Go 官方推荐方案,底层是“漏桶”模型:以恒定速率向桶中“漏水”,每次请求需先“取水”。它支持突发(burst)和平均速率(rps),且线程安全、无锁路径优化好。
关键参数含义:
立即学习“go语言免费学习笔记(深入)”;
-
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),可动态传入。
Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。 Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。Lucene提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在Java开发环境里Lucene是一个成熟的免
中间件中集成限流器,避免每个 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-Limit", "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 延迟翻倍。
真正需要分布式限流的,往往是网关层(如基于 gin 或 echo 的 API 网关),而不是每个微服务内部自己搞一套。









