引言:为什么限流是高并发系统的守门人
在分布式系统中,流量控制是第一道防线。当突发流量涌入时,如果没有有效的限流机制,服务可能因资源耗尽而雪崩。限流不仅仅是"拒绝请求"这么简单——它涉及到算法选择、工程权衡以及在吞吐量与公平性之间寻找最佳平衡点。
本文将从理论到实践,深入分析四种主流限流算法的原理,并给出完整的 Go 实现方案,最终构建一个生产可用的分布式限流中间件。
一、限流算法全景:从计数器到滑动窗口
1.1 固定窗口计数器(Fixed Window Counter)
这是最直觉的限流方式:在固定时间窗口内统计请求数,超过阈值则拒绝。
type FixedWindow struct {
limit int // 窗口内最大请求数
window time.Duration // 窗口大小
count int // 当前计数
mu sync.Mutex
resetAt time.Time // 窗口重置时间
}
func (fw *FixedWindow) Allow() bool {
fw.mu.Lock()
defer fw.mu.Unlock()
now := time.Now()
if now.After(fw.resetAt) {
fw.count = 0
fw.resetAt = now.Add(fw.window)
}
if fw.count < fw.limit {
fw.count++
return true
}
return false
}
缺陷:边界突发问题——在窗口切换的瞬间,如果前半窗口和后半窗口都打满流量,实际在一个窗口周期内可能承受 2 倍流量。
1.2 滑动窗口(Sliding Window)
滑动窗口将时间轴细分为多个小格子,每次请求到来时淘汰过期格子,统计当前窗口内活跃格子的总和。
type SlidingWindow struct {
limit int
window time.Duration
interval time.Duration // 每个小格子的时间跨度
buckets []int // 环形缓冲区
times []time.Time // 每个格子的到期时间
head int
sum int
mu sync.Mutex
}
func (sw *SlidingWindow) Allow() bool {
sw.mu.Lock()
defer sw.mu.Unlock()
sw.evictExpired()
if sw.sum < sw.limit {
sw.buckets[sw.head]++
sw.sum++
return true
}
return false
}
func (sw *SlidingWindow) evictExpired() {
now := time.Now()
for sw.sum > 0 && now.Sub(sw.times[sw.head]) >= sw.window {
sw.sum -= sw.buckets[sw.head]
sw.buckets[sw.head] = 0
sw.head = (sw.head + 1) % len(sw.buckets)
}
}
滑动窗口消除了边界突发问题,但需要更多内存来存储格子数据。
1.3 令牌桶(Token Bucket)
令牌桶算法以一个固定速率向桶中添加令牌,请求到来时消耗令牌。它允许突发流量(桶满时可以瞬间消耗所有令牌),同时保持长期平均速率稳定。
type TokenBucket struct {
capacity int // 桶容量(允许的最大突发量)
rate float64 // 每秒生成的令牌数
tokens float64 // 当前令牌数
lastRefill time.Time // 上次补充时间
mu sync.Mutex
}
func NewTokenBucket(capacity int, rate float64) *TokenBucket {
return &TokenBucket{
capacity: capacity,
rate: rate,
tokens: float64(capacity),
lastRefill: time.Now(),
}
}
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
tb.refill()
if tb.tokens >= 1 {
tb.tokens--
return true
}
return false
}
func (tb *TokenBucket) refill() {
now := time.Now()
elapsed := now.Sub(tb.lastRefill).Seconds()
tb.tokens = math.Min(tb.capacity, tb.tokens + elapsed * tb.rate)
tb.lastRefill = now
}
令牌桶适合既需要保护下游、又允许合理突发的场景,如 API 网关中的用户级限流。
1.4 漏桶(Leaky Bucket)
漏桶以固定速率"漏水",请求以任意速率流入桶中。当桶满时,新请求被拒绝。它的输出速率恒定,非常适合保护处理能力固定的下游系统。
type LeakyBucket struct {
capacity int
leakRate float64 // 每秒处理请求数
water float64 // 当前桶中水量
lastLeak time.Time
mu sync.Mutex
}
func (lb *LeakyBucket) Allow() bool {
lb.mu.Lock()
defer lb.mu.Unlock()
lb.leak()
if lb.water < float64(lb.capacity) {
lb.water++
return true
}
return false
}
func (lb *LeakyBucket) leak() {
now := time.Now()
elapsed := now.Sub(lb.lastLeak).Seconds()
lb.water = math.Max(0, lb.water - elapsed * lb.leakRate)
lb.lastLeak = now
}
漏桶的输出速率恒定,适合对接固定吞吐量的老旧系统或第三方 API。
二、算法对比与选型决策树
| 算法 | 突发支持 | 精度 | 内存开销 | 适用场景 |
|---|---|---|---|---|
| 固定窗口 | 有边界突发风险 | 最低 | O(1) | 粗略限流、内部工具 |
| 滑动窗口 | 平滑无边界突发 | 高 | O(n) | API 限流、用户级配额 |
| 令牌桶 | 允许合理突发 | 中 | O(1) | API 网关、全局吞吐控制 |
| 漏桶 | 完全无突发 | 中 | O(1) | 固定速率输出、消息队列 |
选型决策树:
- 需要保护固定速率的下游? → 漏桶
- 需要允许突发但控制长期平均? → 令牌桶
- 需要精确的秒级配额? → 滑动窗口
- 对精度要求不高,追求极致性能? → 固定窗口
三、分布式限流:Redis + Lua 原子实现
单机限流在微服务架构下存在两个问题:多实例的配额分配不均、无法跨服务统一管控。生产环境通常使用 Redis 实现分布式限流。
3.1 基于 Redis 的滑动窗口限流
-- KEYS[1]: 限流键名
-- ARGV[1]: 窗口大小(毫秒)
-- ARGV[2]: 最大请求数
-- ARGV[3]: 当前时间戳(毫秒)
local key = KEYS[1]
local window = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
-- 移除窗口外的记录
redis.call('ZREMRANGEBYSCORE', key, '-inf', now - window)
-- 统计当前窗口内的请求数
local count = redis.call('ZCARD', key)
if count < limit then
-- 允许请求,记录当前时间戳(成员用时间戳+随机数保证唯一)
local member = now .. ':' .. math.random(100000)
redis.call('ZADD', key, now, member)
redis.call('PEXPIRE', key, window)
return 1
else
return 0
end
3.2 Go 封装的分布式限流器
func (rl *RedisRateLimiter) Allow(ctx context.Context, key string) (bool, error) {
script := redis.NewScript(`
-- 上述 Lua 脚本
`)
now := time.Now().UnixMilli()
result, err := script.Run(ctx, rl.client, []string{key},
rl.window.Milliseconds(), rl.limit, now).Int()
return result == 1, err
}
使用 Redis Sorted Set 作为滑动窗口,Lua 脚本保证原子性,过期时间自动清理。
四、生产级中间件:Gin 框架集成示例
func RateLimiterMiddleware(limiter *RedisRateLimiter, limit int, window time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
// 按 clientIP + 路径 维度限流
key := fmt.Sprintf("ratelimit:%s:%s", c.ClientIP(), c.FullPath())
allowed, err := limiter.Allow(c.Request.Context(), key)
if err != nil {
// 降级:Redis 故障时放行,避免服务不可用
log.Printf("rate limiter error: %v", err)
c.Next()
return
}
if !allowed {
c.Header("X-RateLimit-Limit", strconv.Itoa(limit))
c.Header("X-RateLimit-Remaining", "0")
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "rate limit exceeded",
"retry_after": window.Seconds(),
})
c.Abort()
return
}
c.Next()
}
}
降级策略:Redis 故障时采用 fail-open(直接放行)而非 fail-close(拒绝所有请求),保证可用性优先。监控告警介入人工处理。
五、进阶:自适应限流与排队理论
固定阈值限流在复杂场景下显得力不从心——请求的处理成本随着系统负载变化。Netflix 的 Concurrency Limits 库提供了基于 AIMD(Additive Increase Multiplicative Decrease)的自适应限流方案:
// AIMD 策略:成功时加性增加限制,失败时乘性减少限制
func (l *AIMDLimiter) OnSuccess() {
l.limit = math.Min(l.maxLimit, l.limit + l.increaseStep)
}
func (l *AIMDLimiter) OnError() {
l.limit = math.Min(l.minLimit, l.limit * l.decreaseFactor) // decreaseFactor < 1
}
这种方式让系统能根据自身处理能力动态调整限流阈值,无需人工反复调参。
六、总结与最佳实践
- 多层限流:入口网关做全局限流(令牌桶)→ 服务级限流(滑动窗口)→ 用户级限流(固定窗口或令牌桶)
- 优雅降级:限流触发时返回 429 + Retry-After 头,而非直接断连
- 监控可视化:将限流事件上报到 Prometheus + Grafana,实时观察限流命中率
- 压测验证:使用 wrk/k6 模拟真实流量模式,验证限流参数合理性
限流是分布式系统稳定性的基石。选择合适的算法、配合 Redis 分布式协调,再加上智能降级策略,才能在高并发洪峰中守住服务的底线。

发表评论 取消回复