为什么需要限流
限流(Rate Limiting)控制单位时间内的请求数量,目标:防止资源耗尽、避免服务雪崩、保证服务质量、抵御恶意攻击。核心是选对算法——四种主流算法各有取舍。
1. 计数器(固定窗口)
最简单:一个窗口周期内计数,超过阈值拒绝。临界问题:窗口边界前后各放行一批,可能出现”两倍阈值”的瞬时流量。
type CounterLimiter struct {
rate int
begin time.Time
cycle time.Duration
count int
mu sync.Mutex
}
func (l *CounterLimiter) Allow() bool {
l.mu.Lock(); defer l.mu.Unlock()
if time.Now().After(l.begin.Add(l.cycle)) {
l.begin = time.Now()
l.count = 0
}
if l.count >= l.rate { return false }
l.count++
return true
}
2. 滑动窗口
把窗口切成多个小段(segment),随时间滑动,统计”当前窗口内”所有段的计数——消除临界问题,更精确。
type SlidingWindowLimiter struct {
rate int
segmentSize time.Duration
segments []int // 环形/滑动数组,每段一个计数
mu sync.Mutex
lastTime time.Time
}
func (l *SlidingWindowLimiter) Allow() bool {
l.mu.Lock(); defer l.mu.Unlock()
now := time.Now()
// 推进窗口:把过期的段清零
steps := int(now.Sub(l.lastTime) / l.segmentSize)
l.lastTime = now
for i := 0; i < steps && i < len(l.segments); i++ {
l.segments = append(l.segments[1:], 0)
}
total := 0
for _, c := range l.segments { total += c }
if total >= l.rate { return false }
l.segments[len(l.segments)-1]++
return true
}
代价:实现更复杂、内存开销与段数成正比。
3. 漏桶
请求任意速率进桶,固定速率流出;桶满拒绝。输出平滑,但不能应对突发。
type LeakyBucketLimiter struct {
rate float64 // 流出速率(请求/秒)
capacity float64
water float64
lastMs int64
mu sync.Mutex
}
func (l *LeakyBucketLimiter) Allow() bool {
l.mu.Lock(); defer l.mu.Unlock()
nowMs := time.Now().UnixNano() / 1e6
elapsed := nowMs - l.lastMs
l.lastMs = nowMs
l.water -= float64(elapsed) * l.rate / 1000 // 按时间漏水
if l.water < 0 { l.water = 0 }
if l.water >= l.capacity { return false }
l.water++
return true
}
4. 令牌桶(业界主流)

以固定速率生成令牌存进桶(有容量上限),请求消耗令牌。桶里有囤积的令牌 → 允许突发流量,同时长期速率被平均速率约束——兼顾平滑与弹性,是 Nginx/guava/rate 库的默认选择。
type TokenBucketLimiter struct {
rate float64 // 令牌生成速率(个/秒)
capacity float64 // 桶容量(最大突发)
tokens float64
lastMs int64
mu sync.Mutex
}
func (l *TokenBucketLimiter) Allow() bool {
l.mu.Lock(); defer l.mu.Unlock()
nowMs := time.Now().UnixNano() / 1e6
elapsed := nowMs - l.lastMs
l.lastMs = nowMs
l.tokens += float64(elapsed) * l.rate / 1000 // 生成令牌
if l.tokens > l.capacity { l.tokens = l.capacity }
if l.tokens < 1 { return false }
l.tokens--
return true
}
算法对比
| 算法 | 实现难度 | 平滑度 | 突发流量 | 适用 |
|---|---|---|---|---|
| 计数器 | 简单 | 低 | 不支持 | 简单限流 |
| 滑动窗口 | 中等 | 中 | 部分支持 | 需要更精确 |
| 漏桶 | 中等 | 高 | 不支持 | 要求输出平滑 |
| 令牌桶 | 中等 | 高 | 支持 | 主流方案 |
生产首选:golang.org/x/time/rate
标准生态里直接用官方限流器(令牌桶实现),别自己造轮子:
limiter := rate.NewLimiter(rate.Limit(10), 10) // 每秒 10 个,桶容量 10
if limiter.Allow() {
// 放行
} else {
// 限流
}
分布式限流
单机限流在多实例下各自计数,总量会超。常见方案:
- Redis + Lua:脚本原子执行”计数+过期”,如固定窗口的 INCR + EXPIRE;
- Redis 令牌桶:令牌存 Redis,分布式共享;
- 网关层:Nginx(limit_req)、Envoy 内置限流。
// Redis + Lua 固定窗口(原子,防并发超发)
script := `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('GET', key)
if not current then
redis.call('SET', key, 1, 'EX', window)
return 1
end
if tonumber(current) >= limit then return 0 end
redis.call('INCR', key)
return 1`
// rdb.Eval(ctx, script, []string{key}, limit, window.Seconds())
最佳实践
- 按业务选算法:普通接口计数器/滑动窗口够用;突发容忍场景上令牌桶;
- 多级限流:网关层 + 服务层 + 方法层叠加;
- 区分用户:VIP 与普通用户不同阈值;
- 被限流时优雅降级:返回明确的 429/排队提示,而不是让客户端超时重试打爆;
- 监控限流命中率,动态调整阈值。
