Skip to content

分布式限流

Vistack 内置两级限流:单机令牌桶 + 分布式滑动窗口,通过配置一键切换,登录后接口按用户 ID 限流。

设计目标

text
用户请求 ──▶ 限流中间件(按 user_id 生成 key)
              ├─ 令牌桶(单机:进程内内存,低延迟)
              └─ 滑动窗口(分布式:Redis Lua,跨实例共享)

        放行 / 拒绝(429 + Retry-After + X-RateLimit-*)

算法一:令牌桶(单机)

适用场景:单实例部署或进程内限流,零网络开销。

go
// internal/middlewares/ratelimit/token_bucket.go
type TokenBucket struct {
    mu       sync.Mutex
    rate     float64  // 每秒补充令牌数
    burst    float64  // 桶容量(突发上限)
    tokens   float64
    lastTime time.Time
}
  • 经典漏桶/令牌桶实现:按 rate 补充令牌,桶容量 burst 允许突发;
  • 基于 time 差值补充,不依赖定时器;
  • 线程安全(sync.Mutex)。

算法二:滑动窗口(分布式)

适用场景:多实例部署,需要跨实例共享窗口。基于 Redis ZSet + Lua 脚本原子执行。

lua
-- 原子:清理窗口外旧记录 → 计数 → 未超限则写入
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])

redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, now - window)
local count = redis.call('ZCARD', KEYS[1])

if count < limit then
    redis.call('ZADD', KEYS[1], now, ARGV[4])   -- member 用 UUID 防同毫秒去重
    redis.call('PEXPIRE', KEYS[1], window)
    return {1, limit - count - 1, now + window}  -- {allowed, remaining, reset_at}
end

local oldest = redis.call('ZRANGE', KEYS[1], 0, 0, 'WITHSCORES')
local resetAt = now + window
if oldest[2] then
    resetAt = tonumber(oldest[2]) + window       -- 最早记录过期时间 = 可再放行时间
end
return {0, 0, resetAt}

实现要点:

  • ZSet 记录每次请求的时间戳,member 用 uuid.NewString() 保证全局唯一(防止同毫秒多请求被 ZSet 去重);
  • ZREMRANGEBYSCORE 清理窗口外旧记录,窗口内计数即 ZCARD
  • 整个判断 + 写入在一个 Lua 脚本内原子完成,无竞态;
  • 返回 {allowed, remaining, reset_at},支撑标准限流响应头。

中间件行为

go
// internal/middlewares/ratelimit.go
type Result struct {
    Allowed   bool
    Limit     int
    Remaining int
    ResetAt   time.Time
}

放行/拒绝时返回标准响应头:

响应说明
200放行,带 X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset
429 Too Many Requests拒绝,带 Retry-After(秒)+ X-RateLimit-*

fail-open 策略:Redis 不可用时,分布式限流降级为直接放行(记录错误日志),保证业务可用性优先。

限流 key 设计

text
vistack:ratelimit:{user_id}      # 滑动窗口 key 前缀

登录后的接口按 user_id 限流,未登录接口可配置全局 key。

配置

toml
[ratelimit]
enabled = true
algorithm = "sliding_window"   # token_bucket | sliding_window
token_rate = 10                # 令牌桶:每秒补充令牌数
token_burst = 20               # 令牌桶:桶容量
window = 60                    # 滑动窗口:窗口时长(秒)
limit = 100                    # 滑动窗口:窗口内最大请求数

单测覆盖

internal/middlewares/ratelimit/ratelimit_test.go 覆盖:

  • 令牌桶按速率补充、突发上限、并发安全;
  • 滑动窗口窗口滑动、超限拒绝、ResetAt 计算;
  • 中间件 429 响应与响应头。

演进方向

  • 播放上报 IP 限流(防匿名刷量);
  • 分布式限流阈值动态配置(etcd 配置中心热更新)。

基于 MIT License 发布