## 主要变更 ### 文档重构(减少 1199 行,-23%) - 01-架构设计.md: 503→369 行 (-27%),删除 DDL/配置示例,精简鉴权/存储描述 - 02-接口文档.md: 1313→570 行 (-57%),删除 Go 接口/Orchestrator 实现/配置管理 - 07-成本控制.md: 65→59 行 (-9%),代码块替换为文件引用 ### 文档编号规范化 - 08-功能创意.md → 删除(内容整合到 README.md "功能扩展方向") - 10-Eino框架与编排设计.md → 08-Eino框架与编排设计.md - 情景切换.md → 09-情景切换.md - 12-鉴权体系.md → 10-鉴权体系.md - 13-令牌桶限流.md → 11-令牌桶限流.md ### 交叉引用更新 - 01-架构设计.md: 更新对鉴权体系/令牌桶限流的引用为新编号 - README.md: 更新文档索引表、推荐阅读顺序、新增功能扩展方向 ### 删除过时文档 - 09-技术名词解释.md(内容已整合到 03-技术选型.md) - 10-Eino重构方案.md(历史记录,已完成) - 11-Eino框架技术文档.md(已合并到 08) - 情景切换功能完整文档.md(已规范化为 09) ## 重构原则 - 架构文档聚焦系统结构,移除实现细节 - 接口文档保留纯契约,删除内部实现 - 编号连续(01-11),语义清晰 - 通过交叉引用连接相关文档,避免重复
24 KiB
令牌桶限流设计
概述
CamTalk 采用令牌桶(Token Bucket)算法实现按用户维度的速率限制,核心目标是控制 AI 调用成本,同时为 REST API 提供防暴力破解保护。
设计原则:
- 成本优先:主要限流对象是 WebSocket
query消息(每次触发 STT + LLM + TTS 完整调用链) - 用户隔离:Per-user 维度限流,单用户超限不影响其他用户
- 弹性突发:令牌桶允许合理的突发请求,优于固定窗口的滑动限流
- 存储适配:内存 + Redis 双实现,单实例零依赖,多实例分布式一致
整体架构
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
// 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-Afterheader - 接受
context.Context支持超时和取消(Redis 实现需要)
2. 内存令牌桶(TokenBucket)
文件位置:backend/internal/ratelimit/bucket.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 脚本保证原子性,避免竞态条件:
-- 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
// RateLimitMiddleware 返回 Gin 中间件,按 key 维度限流。
// keyFunc 从请求中提取限流 key(如 IP、用户 ID)。
func RateLimitMiddleware(limiter Limiter, keyFunc func(*gin.Context) string) gin.HandlerFunc
使用方式:
// 按 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:
{
"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 调用前检查:
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 的路由注册中添加中间件:
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 调用成本 |
配置设计
配置文件
# 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 次
配置结构体
// 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"` // 每秒填充令牌数
}
默认值
// 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 初始化
// 初始化限流器
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.NewLimiter(cfg.RateLimit)
logger.Log.Info("rate limiter initialized with in-memory backend")
}
defer limiter.Stop()
}
注入到各模块
// 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 动态调整限流参数,无需重启:
// 预留接口
type DynamicLimiter interface {
Limiter
UpdateConfig(action string, cfg BucketConfig) error
}
3. 按用户等级差异化
不同用户等级使用不同的限流参数:
ratelimit:
query:
capacity: 10 # 免费用户
rate: 0.2
query_premium:
capacity: 30 # 付费用户
rate: 1.0
4. 滑动窗口限流
令牌桶适合允许突发的场景。如果需要更平滑的限流,可增加滑动窗口实现:
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):
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超时判断) - 通过
donechannel 和sync.Once保证优雅停止
惰性创建(bucket.go:95-121):
- 用户首次请求时才创建桶,避免预分配内存
getOrCreateBucket()使用读写锁分离,优化热路径性能
Gin 中间件实现
实际代码(middleware.go:12-42):
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()
}
}
设计要点:
nillimiter 自动跳过限流(支持配置关闭)- 空 key 跳过限流(支持匿名端点)
retryAfter向上取整到秒(符合 HTTP 标准)c.Abort()阻止后续 handler 执行
WebSocket 限流接入
实际接入点(internal/ws/handler.go:230-240):
case "query":
// ... 解析消息 ...
// 限流检查
if limiter != nil {
key := fmt.Sprintf("%s:query", userID)
allowed, retryAfter := limiter.Allow(context.Background(), key)
if !allowed {
logger.Log.Warnw("rate limited", "user_id", userID, "retry_after", retryAfter)
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错误到客户端 - 记录警告日志(便于监控告警)
- 不阻塞其他消息类型(
ping/config/interrupt不限流)
配置加载与依赖注入
配置文件路径:
- 基础配置:
backend/config/config.yaml - 开发环境:
backend/config/config.dev.yaml - 生产环境:
backend/config/config.prod.yaml
实际配置示例(config.yaml:63-76):
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):
// 初始化限流器
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")
}
自动选择策略:
- 配置关闭(
enabled: false)→limiter = nil(完全跳过限流) - Redis 可用 →
NewRedisLimiter(分布式一致) - Redis 不可用 →
NewMemoryLimiter(单实例零依赖)
注入到模块:
// 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):
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):
-- 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):
// 确保 MemoryLimiter 实现了 Limiter 接口
var _ Limiter = (*MemoryLimiter)(nil)
// 确保 mockLimiter 实现了 Limiter 接口
var _ Limiter = (*mockLimiter)(nil)
编译器会在类型不匹配时报错,避免运行时接口错误。
环境变量覆盖
配置文件中的 ratelimit 配置可通过环境变量覆盖:
export CAMTALK_RATELIMIT_ENABLED=true
export CAMTALK_RATELIMIT_QUERY_CAPACITY=20
export CAMTALK_RATELIMIT_QUERY_RATE=0.5
环境变量优先级高于配置文件(Viper 配置绑定)。