# 令牌桶限流设计

## 概述

CamTalk 采用令牌桶（Token Bucket）算法实现按用户维度的速率限制，核心目标是**控制 AI 调用成本**，同时为 REST API 提供防暴力破解保护。

**设计原则**：
- **成本优先**：主要限流对象是 WebSocket `query` 消息（每次触发 STT + LLM + TTS 完整调用链）
- **用户隔离**：Per-user 维度限流，单用户超限不影响其他用户
- **弹性突发**：令牌桶允许合理的突发请求，优于固定窗口的滑动限流
- **存储适配**：内存 + Redis 双实现，单实例零依赖，多实例分布式一致

## 整体架构

```mermaid
graph TB
    subgraph Entry["入口层"]
        WS["WebSocket Handler<br/>query 消息"]
        REST["REST API<br/>login / register"]
    end

    subgraph LimiterModule["Rate Limiter 模块"]
        Interface["Limiter 接口<br/>Allow(userID) → (bool, retryAfter)"]
        MemBucket["TokenBucket<br/>内存令牌桶"]
        RedisBucket["RedisTokenBucket<br/>Redis 令牌桶（Lua 脚本）"]
        Middleware["RateLimitMiddleware<br/>Gin 中间件"]
    end

    subgraph Storage["存储层"]
        MemSync["sync.RWMutex<br/>进程内 map"]
        Redis["Redis<br/>分布式计数"]
    end

    WS -->|"限流检查"| Interface
    REST -->|"中间件"| Middleware
    Middleware --> Interface
    Interface --> MemBucket
    Interface --> RedisBucket
    MemBucket --> MemSync
    RedisBucket --> Redis
```

## 令牌桶算法

### 原理

令牌桶以固定速率向桶中添加令牌，桶有最大容量上限。每次请求消耗一个令牌，桶空时拒绝请求。

```
桶容量(capacity) = 允许的突发请求数上限
填充速率(rate)   = 每秒补充的令牌数

时间线示例（capacity=5, rate=0.2）：
  t=0s   桶满 5 令牌 → 用户连续发 5 个 query 全部通过
  t=0s   桶空 → 第 6 个 query 被拒绝，retryAfter=5s
  t=5s   桶补充 1 令牌 → 可再发 1 个 query
  t=10s  桶补充 1 令牌 → 可再发 1 个 query
```

### 算法公式

```
elapsed = now - lastRefill
newTokens = elapsed * rate
currentTokens = min(capacity, lastTokens + newTokens)

if currentTokens >= 1:
    currentTokens -= 1
    allowed = true
else:
    allowed = false
    retryAfter = (1 - currentTokens) / rate
```

## 核心组件

### 1. Limiter 接口

**文件位置**：`backend/internal/ratelimit/limiter.go`

```go
// Limiter 速率限制器接口。
type Limiter interface {
    // Allow 判断 key 是否允许执行一次操作。
    // key 通常为 "userID:action" 格式。
    // 返回 (allowed, retryAfter)。retryAfter 表示需要等待的时间。
    Allow(ctx context.Context, key string) (bool, time.Duration)
}
```

**设计要点**：
- key 为字符串，不限定格式，由调用方决定维度（用户 ID、IP 地址等）
- 返回 `retryAfter` 供客户端/服务端设置 `Retry-After` header
- 接受 `context.Context` 支持超时和取消（Redis 实现需要）

### 2. 内存令牌桶（TokenBucket）

**文件位置**：`backend/internal/ratelimit/bucket.go`

```go
// TokenBucket 内存令牌桶，适用于单实例部署。
type TokenBucket struct {
    capacity   int           // 桶容量
    rate       float64       // 每秒填充令牌数
    tokens     float64       // 当前令牌数
    lastRefill time.Time     // 上次填充时间
    mu         sync.Mutex
}

// Limiter 管理多个用户的令牌桶。
type Limiter struct {
    buckets  map[string]*TokenBucket
    config   Config
    mu       sync.RWMutex
    stopOnce sync.Once
    done     chan struct{}
}
```

**并发安全**：
- 每个桶内部用 `sync.Mutex` 保护读写
- 桶 map 用 `sync.RWMutex` 保护（读多写少场景）
- 用户首次请求时惰性创建桶

**内存回收**：
- 后台 goroutine 定期扫描，清理超过 10 分钟无活动的桶
- 避免长期运行后内存泄漏

### 3. Redis 令牌桶（RedisTokenBucket）

**文件位置**：`backend/internal/ratelimit/redis_bucket.go`

使用 Redis Lua 脚本保证原子性，避免竞态条件：

```lua
-- KEYS[1] = 限流 key
-- ARGV[1] = capacity（桶容量）
-- ARGV[2] = rate（每秒填充数）
-- ARGV[3] = now（当前时间戳，秒，浮点）
-- ARGV[4] = ttl（key 过期时间，秒）

local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local ttl = tonumber(ARGV[4])

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

-- 计算新令牌
local elapsed = math.max(0, now - last_refill)
tokens = math.min(capacity, tokens + elapsed * rate)

local allowed = 0
local retry_after = 0

if tokens >= 1 then
    tokens = tokens - 1
    allowed = 1
else
    retry_after = (1 - tokens) / rate
end

-- 回写状态
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', key, ttl)

return {allowed, tostring(retry_after)}
```

**设计要点**：
- 每个用户的限流状态存储为一个 Redis Hash（`tokens` + `last_refill`）
- TTL 自动过期，无需手动清理
- Lua 脚本保证"读取-计算-回写"原子执行

### 4. Gin 中间件

**文件位置**：`backend/internal/ratelimit/middleware.go`

```go
// RateLimitMiddleware 返回 Gin 中间件，按 key 维度限流。
// keyFunc 从请求中提取限流 key（如 IP、用户 ID）。
func RateLimitMiddleware(limiter Limiter, keyFunc func(*gin.Context) string) gin.HandlerFunc
```

**使用方式**：

```go
// 按 IP 限流（登录/注册，未登录用户无 userID）
loginGroup.POST("/login",
    ratelimit.Middleware(limiter, func(c *gin.Context) string {
        return c.ClientIP() + ":login"
    }),
    authHandler.Login,
)

// 按用户 ID 限流（已认证的 API）
authorized.POST("/conversations",
    ratelimit.Middleware(limiter, func(c *gin.Context) string {
        return c.GetString("user_id") + ":conversation"
    }),
    convHandler.Create,
)
```

**错误响应**：

REST API 返回 HTTP 429：

```json
{
    "code": "RATE_LIMITED",
    "message": "too many requests, retry after 5s"
}
```

同时设置 `Retry-After` header：

```
HTTP/1.1 429 Too Many Requests
Retry-After: 5
```

## 限流接入点

### WebSocket query 消息（核心）

在 `ws/handler.go` 的 `case "query"` 分支中，orchestrator 调用前检查：

```go
case "query":
    // ... 解析消息 ...

    // 限流检查
    if limiter != nil {
        allowed, retryAfter := limiter.Allow(ctx, userID+":query")
        if !allowed {
            errors.SendWSError(client, errors.CodeRateLimited, msg.RequestID,
                fmt.Errorf("rate limited, retry after %s", retryAfter))
            continue
        }
    }

    // ... 继续处理 query ...
```

### REST API 登录/注册

在 `api/auth.go` 的路由注册中添加中间件：

```go
func (h *AuthHandler) RegisterRoutes(rg *gin.RouterGroup, limiter ratelimit.Limiter) {
    auth := rg.Group("/auth")
    if limiter != nil {
        auth.POST("/register",
            ratelimit.Middleware(limiter, ipKeyFunc("register")),
            h.Register,
        )
        auth.POST("/login",
            ratelimit.Middleware(limiter, ipKeyFunc("login")),
            h.Login,
        )
    } else {
        auth.POST("/register", h.Register)
        auth.POST("/login", h.Login)
    }
    auth.POST("/refresh", h.Refresh)
    auth.POST("/logout", h.Logout)
}
```

### 不限流的端点

| 端点 | 原因 |
|------|------|
| `ping` / `pong` | 心跳保活，无 AI 调用成本 |
| `config` | 配置更新，无 AI 调用成本 |
| `interrupt` | 中断请求，取消操作不应被限流 |
| `GET /api/health` | 健康检查，运维必需 |
| `POST /api/auth/refresh` | Token 刷新，限流会导致用户被迫重新登录 |
| `POST /api/auth/logout` | 登出，限流会导致用户无法正常退出 |
| `GET /api/conversations` | 查询列表，无 AI 调用成本 |

## 配置设计

### 配置文件

```yaml
# backend/config/config.yaml 新增
ratelimit:
  enabled: true
  # WebSocket query 消息限流（核心，控制 AI 成本）
  query:
    capacity: 10        # 突发容量：允许连续发 10 个 query
    rate: 0.2           # 填充速率：每 5 秒补充 1 个令牌
  # REST API 登录限流（防暴力破解）
  login:
    capacity: 5         # 突发容量：允许连续 5 次登录尝试
    rate: 0.1           # 填充速率：每 10 秒补充 1 次
  # REST API 注册限流
  register:
    capacity: 3         # 突发容量：允许连续 3 次注册
    rate: 0.05          # 填充速率：每 20 秒补充 1 次
```

### 配置结构体

```go
// config/config.go 新增

type RateLimitConfig struct {
    Enabled  bool          `mapstructure:"enabled"`
    Query    BucketConfig  `mapstructure:"query"`
    Login    BucketConfig  `mapstructure:"login"`
    Register BucketConfig  `mapstructure:"register"`
}

type BucketConfig struct {
    Capacity int     `mapstructure:"capacity"` // 桶容量（突发上限）
    Rate     float64 `mapstructure:"rate"`     // 每秒填充令牌数
}
```

### 默认值

```go
// setDefaults 新增
v.SetDefault("ratelimit.enabled", false)
v.SetDefault("ratelimit.query.capacity", 10)
v.SetDefault("ratelimit.query.rate", 0.2)
v.SetDefault("ratelimit.login.capacity", 5)
v.SetDefault("ratelimit.login.rate", 0.1)
v.SetDefault("ratelimit.register.capacity", 3)
v.SetDefault("ratelimit.register.rate", 0.05)
```

### 参数选择建议

| 场景 | capacity | rate | 含义 |
|------|----------|------|------|
| WebSocket query | 10 | 0.2 | 突发 10 个，之后每 5 秒 1 个 |
| 登录 | 5 | 0.1 | 突发 5 次，之后每 10 秒 1 次 |
| 注册 | 3 | 0.05 | 突发 3 次，之后每 20 秒 1 次 |

> **调参原则**：capacity 决定"能忍多少次突发"，rate 决定"稳态下多久能再请求一次"。query 的 rate 建议根据 AI 调用成本和目标月预算反推。

## 依赖注入

### main.go 初始化

```go
// 初始化限流器
var limiter ratelimit.Limiter
if cfg.RateLimit.Enabled {
    if rdb != nil {
        // 多实例：使用 Redis 令牌桶
        limiter = ratelimit.NewRedisLimiter(rdb, cfg.RateLimit)
        logger.Log.Info("rate limiter initialized with Redis backend")
    } else {
        // 单实例：使用内存令牌桶
        limiter = ratelimit.NewMemoryLimiter(cfg.RateLimit)
        logger.Log.Info("rate limiter initialized with in-memory backend")
    }
    defer limiter.Stop()
}
```

### 注入到各模块

```go
// WebSocket Handler —— 新增 limiter 参数
r.GET("/ws", ws.ServeWS(sessionMgr, orch, cfg, tokenMgr, limiter))

// Auth REST —— 新增 limiter 参数
authHandler := api.NewAuthHandler(authService, tokenMgr)
authHandler.RegisterRoutes(apiGroup, limiter)
```

## 错误码

复用已有错误码 `RATE_LIMITED`（`backend/internal/errors/codes.go`）：

| 传输层 | HTTP 状态码 | 错误格式 |
|--------|-----------|---------|
| REST API | 429 Too Many Requests | `{code: "RATE_LIMITED", message: "too many requests, retry after Xs"}` |
| WebSocket | — | `{type: "error", code: "RATE_LIMITED", request_id: "...", message: "..."}` |

## 文件结构

```
backend/internal/ratelimit/
├── limiter.go          # Limiter 接口 + Config 类型定义
├── bucket.go           # 内存令牌桶实现
├── bucket_test.go      # 内存令牌桶单元测试
├── redis_bucket.go     # Redis 令牌桶实现（Lua 脚本）
├── redis_bucket_test.go# Redis 令牌桶单元测试
└── middleware.go        # Gin 中间件
```

## 测试用例

### 单元测试

**内存令牌桶**（`bucket_test.go`）：
- 首次请求通过
- 连续消耗至桶空
- 桶空后拒绝，返回正确 retryAfter
- 等待后令牌补充，请求通过
- 并发安全性（多个 goroutine 同时 Allow）
- 桶容量边界（capacity=0, capacity=1）
- 填充速率边界（rate=0, rate 极大值）
- 不活跃桶的内存回收

**Redis 令牌桶**（`redis_bucket_test.go`）：
- 与内存实现行为一致性
- Lua 脚本原子性
- key TTL 自动过期
- 并发安全性（多个客户端同时请求）

### 集成测试

- 限流关闭时不拦截请求
- 限流开启后，REST API 登录超限返回 429
- 限流开启后，WebSocket query 超限返回 `RATE_LIMITED` 错误
- 单实例内存限流 vs 多实例 Redis 限流行为一致
- 重启后内存限流重置，Redis 限流保持

## 扩展点

### 1. 多级限流

可扩展为多级限流策略：

```
全局限流（全用户共享） → 用户级限流（当前实现） → 端点级限流（不同 API 不同限制）
```

### 2. 动态调参

通过配置热更新或管理 API 动态调整限流参数，无需重启：

```go
// 预留接口
type DynamicLimiter interface {
    Limiter
    UpdateConfig(action string, cfg BucketConfig) error
}
```

### 3. 按用户等级差异化

不同用户等级使用不同的限流参数：

```yaml
ratelimit:
  query:
    capacity: 10    # 免费用户
    rate: 0.2
  query_premium:
    capacity: 30    # 付费用户
    rate: 1.0
```

### 4. 滑动窗口限流

令牌桶适合允许突发的场景。如果需要更平滑的限流，可增加滑动窗口实现：

```go
type SlidingWindowLimiter struct {
    windowSize time.Duration
    maxRequests int
}
```

### 5. 分布式全局限流

当前 Redis 实现是 Per-Instance 独立计数。如需全局精确限流，可改为 Redis 全局计数器（所有实例共享同一个 key）。

## 监控指标

### 关键指标

- **限流触发率**：被拒绝请求数 / 总请求数
- **各端点限流分布**：query / login / register 各自的触发率
- **等待时长分布**：retryAfter 的 P50/P99
- **桶状态**：各用户桶的平均令牌数（反映使用模式）

### 告警规则

- **限流触发率突增**：可能表示异常流量或攻击
- **单用户持续被限流**：可能表示客户端 bug（死循环请求）

## 实际实现要点

### 文件结构

```
backend/internal/ratelimit/
├── limiter.go              # Limiter 接口定义
├── bucket.go               # 内存令牌桶实现 (MemoryLimiter + TokenBucket)
├── bucket_test.go          # 内存令牌桶单元测试（11 个测试用例）
├── redis_bucket.go         # Redis 令牌桶实现（Lua 脚本）
├── redis_bucket_test.go    # Redis 令牌桶单元测试
└── middleware.go           # Gin 中间件实现
```

### TokenBucket 实现细节

**核心数据结构**（`bucket.go:12-18`）：

```go
type TokenBucket struct {
    capacity   int           // 桶容量
    rate       float64       // 每秒填充令牌数
    tokens     float64       // 当前令牌数（浮点数支持小数令牌）
    lastRefill time.Time     // 上次填充时间
    mu         sync.Mutex    // 保护并发访问
}
```

**并发安全**（`bucket.go:31-56`）：
- 每个桶内部使用 `sync.Mutex` 保护 `tokens` 和 `lastRefill` 字段
- `allow()` 方法的"读取-计算-回写"操作原子执行
- 桶 map 使用 `sync.RWMutex` 保护，读多写少优化（`bucket.go:62`）
- 双重检查锁（`bucket.go:106-113`）：先尝试读锁获取桶，不存在时升级写锁创建

**内存回收机制**（`bucket.go:132-161`）：
- 后台 goroutine 每 10 分钟扫描一次（`cleanup()` 方法）
- 删除超过 10 分钟无活动的桶（`lastRefill` 超时判断）
- 通过 `done` channel 和 `sync.Once` 保证优雅停止

**惰性创建**（`bucket.go:95-121`）：
- 用户首次请求时才创建桶，避免预分配内存
- `getOrCreateBucket()` 使用读写锁分离，优化热路径性能

### Gin 中间件实现

**实际代码**（`middleware.go:12-42`）：

```go
func Middleware(limiter Limiter, keyFunc func(*gin.Context) string) gin.HandlerFunc {
    return func(c *gin.Context) {
        if limiter == nil {
            c.Next()
            return
        }

        key := keyFunc(c)
        if key == "" {
            // key 为空时跳过限流
            c.Next()
            return
        }

        allowed, retryAfter := limiter.Allow(c.Request.Context(), key)

        if !allowed {
            // 设置 Retry-After header（秒）
            c.Header("Retry-After", fmt.Sprintf("%d", int(retryAfter.Seconds()+0.5)))

            c.JSON(http.StatusTooManyRequests, gin.H{
                "code":    "RATE_LIMITED",
                "message": fmt.Sprintf("too many requests, retry after %s", retryAfter.Round(1)),
            })
            c.Abort()
            return
        }

        c.Next()
    }
}
```

**设计要点**：
- `nil` limiter 自动跳过限流（支持配置关闭）
- 空 key 跳过限流（支持匿名端点）
- `retryAfter` 向上取整到秒（符合 HTTP 标准）
- `c.Abort()` 阻止后续 handler 执行

### WebSocket 限流接入

**实际接入点**（`internal/ws/handler.go:230-240`）：

```go
case "query":
    // ... 解析消息 ...

    // 限流检查
    if limiter != nil {
        key := fmt.Sprintf("%s:query", userID)
        allowed, retryAfter := limiter.Allow(ctx, key)
        if !allowed {
            // 限流触发时自动记录 Warn 日志（在 limiter 内部使用 trace.FromContext）
            errors.SendWSError(client, errors.CodeRateLimited, msg.RequestID,
                fmt.Errorf("rate limited, retry after %s", retryAfter.Round(time.Second)))
            continue
        }
    }

    // ... 继续处理 query ...
```

**设计要点**：
- key 格式：`userID:query`（用户级限流）
- 拒绝时发送 `RATE_LIMITED` 错误到客户端
- 限流触发时 `RedisLimiter.Allow` 内部自动记录 Warn 日志（带 trace_id，详见 `docs/13-日志追踪.md`）
- 不阻塞其他消息类型（`ping`/`config`/`interrupt` 不限流）

### 配置加载与依赖注入

**配置文件路径**：
- 基础配置：`backend/config/config.yaml`
- 开发环境：`backend/config/config.dev.yaml`
- 生产环境：`backend/config/config.prod.yaml`

**实际配置示例**（`config.yaml:63-76`）：

```yaml
ratelimit:
  enabled: false              # 是否启用限流
  # WebSocket query 消息限流（核心，控制 AI 成本）
  query:
    capacity: 10              # 突发容量：允许连续发 10 个 query
    rate: 0.2                 # 填充速率：每 5 秒补充 1 个令牌
  # REST API 登录限流（防暴力破解）
  login:
    capacity: 5               # 突发容量：允许连续 5 次登录尝试
    rate: 0.1                 # 填充速率：每 10 秒补充 1 次
  # REST API 注册限流
  register:
    capacity: 3               # 突发容量：允许连续 3 次注册
    rate: 0.05                # 填充速率：每 20 秒补充 1 次
```

**依赖注入实现**（`cmd/server/main.go:200-214`）：

```go
// 初始化限流器
var limiter ratelimit.Limiter
if cfg.RateLimit.Enabled {
    if rdb != nil {
        // 多实例：使用 Redis 令牌桶
        limiter = ratelimit.NewRedisLimiter(rdb, cfg.RateLimit)
        logger.Log.Info("rate limiter initialized with Redis backend")
    } else {
        // 单实例：使用内存令牌桶
        limiter = ratelimit.NewMemoryLimiter(cfg.RateLimit)
        logger.Log.Info("rate limiter initialized with in-memory backend")
    }
    defer limiter.Stop()
} else {
    logger.Log.Info("rate limiter disabled")
}
```

**自动选择策略**：
1. 配置关闭（`enabled: false`）→ `limiter = nil`（完全跳过限流）
2. Redis 可用 → `NewRedisLimiter`（分布式一致）
3. Redis 不可用 → `NewMemoryLimiter`（单实例零依赖）

**注入到模块**：

```go
// WebSocket Handler
r.GET("/ws", ws.ServeWS(sessionMgr, orch, cfg, tokenMgr, limiter))

// REST API
authHandler.RegisterRoutes(apiGroup, limiter)
```

### 实际测试用例

**内存令牌桶测试**（`bucket_test.go`，11 个用例）：

| 测试用例 | 验证内容 |
|---------|---------|
| `TestTokenBucket_Allow_FirstRequest` | 首次请求通过 |
| `TestTokenBucket_Allow_ConsumeUntilEmpty` | 连续消耗至桶空 |
| `TestTokenBucket_Allow_RetryAfterCorrect` | `retryAfter` 计算准确性 |
| `TestTokenBucket_Allow_RefillAfterWait` | 等待后令牌补充 |
| `TestTokenBucket_Allow_CapacityLimit` | 桶容量上限限制 |
| `TestTokenBucket_Allow_ConcurrentSafe` | 100 并发请求正确性 |
| `TestTokenBucket_Allow_ZeroCapacity` | 边界：`capacity=0` |
| `TestTokenBucket_Allow_ZeroRate` | 边界：`rate=0` |
| `TestMemoryLimiter_Allow_DifferentKeys` | 不同用户隔离 |
| `TestMemoryLimiter_Cleanup` | 不活跃桶自动清理 |
| `TestMemoryLimiter_Stop` | 多次 `Stop()` 不 panic |

**中间件测试**（`middleware_test.go`，7 个用例）：

| 测试用例 | 验证内容 |
|---------|---------|
| `TestMiddleware_Allow` | 允许时正常响应 |
| `TestMiddleware_Deny` | 拒绝时返回 429 + `Retry-After` header |
| `TestMiddleware_NilLimiter` | `nil` limiter 放行 |
| `TestMiddleware_EmptyKey` | 空 key 放行 |
| `TestMiddleware_KeyFunc` | `keyFunc` 正确提取 key |
| `TestMiddleware_RetryAfterRounding` | `retryAfter` 向上取整 |

**并发安全性验证**（`bucket_test.go:83-107`）：

```go
func TestTokenBucket_Allow_ConcurrentSafe(t *testing.T) {
    bucket := newTokenBucket(100, 10.0)
    var wg sync.WaitGroup
    successCount := 0
    var mu sync.Mutex

    // 100 个并发请求
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            allowed, _ := bucket.allow()
            if allowed {
                mu.Lock()
                successCount++
                mu.Unlock()
            }
        }()
    }

    wg.Wait()

    // 应该正好 100 个成功（桶容量为 100）
    assert.Equal(t, 100, successCount)
}
```

### Redis Lua 脚本实现

**实际脚本**（`redis_bucket.go:15-53`）：

```lua
-- KEYS[1] = 限流 key
-- ARGV[1] = capacity（桶容量）
-- ARGV[2] = rate（每秒填充数）
-- ARGV[3] = now（当前时间戳，秒，浮点）
-- ARGV[4] = ttl（key 过期时间，秒）

local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local ttl = tonumber(ARGV[4])

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

-- 计算新令牌
local elapsed = math.max(0, now - last_refill)
tokens = math.min(capacity, tokens + elapsed * rate)

local allowed = 0
local retry_after = 0

if tokens >= 1 then
    tokens = tokens - 1
    allowed = 1
else
    if rate == 0 then
        retry_after = 86400  -- 24小时
    else
        retry_after = (1 - tokens) / rate
    end
end

-- 回写状态
redis.call('HMSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', key, ttl)

return {allowed, tostring(retry_after)}
```

**设计要点**：
- 使用 Hash 存储两个字段：`tokens`（当前令牌数）+ `last_refill`（上次填充时间）
- 原子性：整个脚本在 Redis 单线程中执行，无竞态条件
- 自动过期：每次操作设置 TTL（默认 10 分钟），无需手动清理
- 与内存实现算法一致（便于单元测试验证行为等价性）

### 编译期接口检查

**接口契约**（`bucket.go:172`，`middleware_test.go:31`）：

```go
// 确保 MemoryLimiter 实现了 Limiter 接口
var _ Limiter = (*MemoryLimiter)(nil)

// 确保 mockLimiter 实现了 Limiter 接口
var _ Limiter = (*mockLimiter)(nil)
```

编译器会在类型不匹配时报错，避免运行时接口错误。

### 环境变量覆盖

配置文件中的 `ratelimit` 配置可通过环境变量覆盖：

```bash
export CAMTALK_RATELIMIT_ENABLED=true
export CAMTALK_RATELIMIT_QUERY_CAPACITY=20
export CAMTALK_RATELIMIT_QUERY_RATE=0.5
```

环境变量优先级高于配置文件（Viper 配置绑定）。

## 参考资料

- [Token Bucket 算法](https://en.wikipedia.org/wiki/Token_bucket)
- [Redis Rate Limiting](https://redis.io/glossaries/rate-limiting/)
- [Cloudflare - How we built rate limiting capable of scaling to millions of domains](https://blog.cloudflare.com/counting-things-a-lot-of-different-things/)
