Go语言高并发限流器实现:令牌桶与滑动窗口的生产级方案

限流算法选型:令牌桶 vs 滑动窗口 vs 固定窗口

高并发后端服务的限流需要根据业务特征选择算法:

  • 固定窗口:实现简单,但窗口边界有突发流量翻倍问题
  • 滑动窗口:精度高,内存开销大(需存储每个请求时间戳)
  • 令牌桶:允许突发流量,匀速补充令牌,适合API限流
  • 漏桶:强制匀速输出,不适合有突发特征的HTTP请求

实际生产中,令牌桶是最通用的方案,本文给出完整实现。

单机令牌桶限流器实现

package ratelimit

import (
    "sync"
    "time"
)

type TokenBucket struct {
    mu         sync.Mutex
    rate       float64   // 每秒补充令牌数
    burst      int       // 桶容量(最大突发量)
    tokens     float64   // 当前令牌数
    lastRefill time.Time // 上次补充时间
}

func NewTokenBucket(rate float64, burst int) *TokenBucket {
    return &TokenBucket{
        rate:       rate,
        burst:      burst,
        tokens:     float64(burst),
        lastRefill: time.Now(),
    }
}

func (tb *TokenBucket) Allow() bool {
    tb.mu.Lock()
    defer tb.mu.Unlock()

    now := time.Now()
    elapsed := now.Sub(tb.lastRefill).Seconds()
    tb.tokens += elapsed * tb.rate
    
    if tb.tokens > float64(tb.burst) {
        tb.tokens = float64(tb.burst)
    }
    tb.lastRefill = now

    if tb.tokens >= 1 {
        tb.tokens--
        return true
    }
    return false
}

rate控制稳态吞吐,burst允许瞬间通过的最大请求数。例如rate=100、burst=20:稳态100 QPS,瞬时最多20个请求并发通过。

分布式限流:Redis + Lua原子操作

多实例部署时,单机限流无法保证全局限流精度。用Redis实现分布式令牌桶:

-- Lua脚本:原子化令牌桶
local key = KEYS[1]
local rate = tonumber(ARGV[1])
local burst = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

local bucket = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(bucket[1]) or burst
local last_refill = tonumber(bucket[2]) or now

-- 补充令牌
local elapsed = now - last_refill
tokens = math.min(burst, tokens + elapsed * rate)
last_refill = now

-- 尝试消费
local allowed = 0
if tokens >= 1 then
    tokens = tokens - 1
    allowed = 1
end

redis.call('HMSET', key, 'tokens', tokens, 'last_refill', last_refill)
redis.call('EXPIRE', key, math.ceil(burst / rate) + 10)

return allowed

Go端调用:

func (r *RedisLimiter) Allow(ctx context.Context, key string) (bool, error) {
    now := time.Now().UnixMilli() / 1000.0
    result, err := r.luaScript.Run(
        ctx, r.client,
        []string{fmt.Sprintf("ratelimit:%s", key)},
        r.rate, r.burst, now,
    ).Int64()
    return result == 1, err
}

Lua脚本在Redis单线程内原子执行,避免并发竞态。key设TTL自动清理过期限流器。

滑动窗口限流:精确控制每分钟请求量

对需要严格限制请求总量的场景(如第三方API调用配额),滑动窗口比令牌桶更直观:

-- Redis滑动窗口Lua脚本
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])  -- 窗口秒数
local now = tonumber(ARGV[3])

redis.call('ZADD', key, now, now)  -- member和score都用时间戳
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)
local count = redis.call('ZCARD', key)
redis.call('EXPIRE', key, window + 1)

if count <= limit then
    return 1
else
    redis.call('ZREM', key, now)  -- 回滚
    return 0
end

用Sorted Set存储请求时间戳,ZREMRANGEBYSCORE清除窗口外的记录。内存开销:每个请求占8字节(时间戳),1万QPS的分钟窗口约480KB,可控。

限流中间件集成与降级策略

限流器需集成到HTTP中间件层,对被拒绝的请求返回合理的响应:

func RateLimitMiddleware(limiter *TokenBucket) gin.HandlerFunc {
    return func(c *gin.Context) {
        if !limiter.Allow() {
            c.JSON(http.StatusTooManyRequests, gin.H{
                "code":    429,
                "message": "请求过于频繁,请稍后重试",
                "retry_after": 1,  // 秒
            })
            c.Abort()
            return
        }
        c.Next()
    }
}

返回429状态码和Retry-After头,客户端可据此实现退避重试。降级策略:限流触发时不直接丢弃请求,而是排入延迟队列异步处理,适用于写操作。读操作则走缓存降级,返回过期数据而非空结果。

监控维度:限流触发率、被限流请求的P99延迟、各key维度的QPS分布。接入Prometheus后用rate(limiter_rejected_total[5m])告警,超过5%触发扩容或降级。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-gao-bing-fa-xian-liu-qi-shi-xian-ling-pai-tong-yu/

(0)
小编小编
上一篇 20小时前
下一篇 20小时前

相关推荐