# 09 — 限流 > **一句话概括**:Redis Lua 原子令牌桶 + 双层限流 + Fail-Open 降级,保护系统免受过载。 --- ```mermaid flowchart LR A["🌐 Request"] --> B["🌍 Global
Limiter"] B -->|pass| C["👤 User
Limiter"] B -->|deny| F["❌ 429"] B -->|redis-fail| C C -->|pass| D["✅ Handler"] C -->|deny| F C -->|redis-fail| D style A fill:#e3f2fd,stroke:#1976d2 style B fill:#fff3e0,stroke:#f57c00 style C fill:#e8f5e9,stroke:#388e3c style D fill:#c8e6c9,stroke:#2e7d32 style F fill:#ffcdd2,stroke:#c62828 ``` --- ## ⚙️ 令牌桶算法 Gen2D 使用 **Redis + Lua 脚本** 实现分布式令牌桶限流,保证原子性和一致性。 ### Lua 脚本核心逻辑 ```lua -- KEYS[1] = 限流 key -- ARGV[1] = rate(每秒令牌数) -- ARGV[2] = burst(桶容量) -- ARGV[3] = now(当前时间戳,毫秒) -- ARGV[4] = expiration(key 过期时间) -- 1. 获取当前桶状态 local data = redis.call('HMGET', key, 'tokens', 'ts') -- 2. 首次访问,初始化满桶 if tokens == nil then tokens = burst last_ts = now end -- 3. 计算时间差,补充令牌 local delta = now - last_ts if delta > 0 and rate > 0 then local refill = (delta * rate) / 1000 tokens = math.min(burst, tokens + refill) end -- 4. 判断是否允许 if tokens >= 1 then tokens = tokens - 1 allowed = 1 else retry_after = math.ceil((deficit * 1000) / rate) end -- 5. 更新 Redis redis.call('HSET', key, 'tokens', tokens, 'ts', last_ts) redis.call('PEXPIRE', key, expiration) return {allowed, tokens, retry_after} ``` ### 固定窗口变体 当 `Rate = 0` 时,令牌桶退化为固定窗口模式: - 桶初始化为满(Burst 个令牌) - 用完后不补充(`rate = 0` 时跳过 refill) - 等待 key 过期后重置(`Expiration` 控制窗口大小) > 💡 **适用场景**:24 小时维度的配额控制,如"每天 30 次提示词优化"。 --- ## 🔀 双层限流配置 Gen2D 对核心接口实施**全局限流 + 用户限流**双重保护: | 接口 | 全局限流 | 用户限流 | 窗口 | |------|---------|---------|------| | `/api/v1/prompt/optimize` | 1000 次/24h | 30 次/24h | 25h 过期 | | `/api/v1/generate` | 500 次/24h | 15 次/24h | 25h 过期 | ### 执行顺序 ```mermaid flowchart TB A["请求进入"] --> B["全局限流检查"] B -->|通过| C["用户限流检查"] B -->|拒绝| D["429 Too Many Requests"] C -->|通过| E["执行 Handler"] C -->|拒绝| D style B fill:#fff3e0,stroke:#f57c00 style C fill:#e8f5e9,stroke:#388e3c style D fill:#ffcdd2,stroke:#c62828 style E fill:#c8e6c9,stroke:#2e7d32 ``` **全局限流在前**:先检查系统总体配额,避免单个用户耗尽全局配额。 ### 配置参数 ```go type Config struct { Rate int // 每秒令牌数;0 = 固定窗口 Burst int // 桶容量(窗口内总量上限) KeyPrefix string // Redis key 前缀 Expiration time.Duration // key 过期时间 } ``` | 配置项 | Prompt User | Prompt Global | Generate User | Generate Global | |--------|:-----------:|:-------------:|:-------------:|:---------------:| | Rate | 0 | 0 | 0 | 0 | | Burst | 30 | 1000 | 15 | 500 | | KeyPrefix | `ratelimit:prompt:user:` | `ratelimit:prompt:global:` | `ratelimit:generate:user:` | `ratelimit:generate:global:` | | Expiration | 25h | 25h | 25h | 25h | --- ## 🛡️ Fail-Open 降级 当 Redis 不可用时限流器自动降级为 **Fail-Open** 模式: ```go func (l *TokenBucketLimiter) Allow(ctx context.Context, key string) (bool, int, time.Duration) { result, err := l.script.Run(ctx, l.client, ...).Int64Slice() if err != nil { // Redis 不可用时 fail-open,放行请求 return true, l.config.Burst, 0 } // ... } ``` **设计权衡**: | 策略 | 优点 | 缺点 | |------|------|------| | **Fail-Open** ✅ | 保证可用性,用户体验不受影响 | 可能短暂失去限流保护 | | Fail-Close | 严格限流保护 | Redis 故障导致全站不可用 | > 🛡️ **选择 Fail-Open**:在"偶尔超限"和"完全不可用"之间,优先保证服务可用性。 --- ## 📡 中间件响应 限流中间件返回标准化的 HTTP 响应: ### 允许通过 ```http HTTP/1.1 200 OK X-RateLimit-Remaining: 12 ``` ### 被限流 ```http HTTP/1.1 429 Too Many Requests Retry-After: 3600 X-RateLimit-Remaining: 0 { "code": 429, "message": "请求过于频繁,请稍后再试" } ``` | Header | 说明 | |--------|------| | `X-RateLimit-Remaining` | 剩余令牌数 | | `Retry-After` | 建议重试等待秒数 | --- ## 📊 指标采集 限流中间件自动采集 Prometheus 指标: ```go metrics.RateLimitRequestsTotal.WithLabelValues(scope, endpoint, result).Inc() metrics.RateLimitRemainingTokens.WithLabelValues(scope, endpoint).Set(float64(remaining)) ``` - `scope`:`user` / `global` - `endpoint`:`prompt` / `generate` - `result`:`allowed` / `denied` 配合告警规则 `RateLimitHighDenialRate`(拒绝率 > 5%),及时发现异常流量。 --- ## 🔗 关联文档 - [← 返回索引](00-index.md) - [10 — 中间件链](10-middleware-chain.md) — 限流中间件在链中的位置 - [07 — 可观测性](07-observability.md) — 限流指标和告警规则