2026-05-30 22:42:23 +08:00
|
|
|
|
# 令牌桶 — Redis Lua 脚本
|
|
|
|
|
|
|
|
|
|
|
|
原子操作,单次 Redis 往返
|
|
|
|
|
|
|
|
|
|
|
|
<div class="mt-2">
|
|
|
|
|
|
|
|
|
|
|
|
```lua {1-2|4-8|10-14|all}
|
|
|
|
|
|
local data = redis.call('HMGET', key, 'tokens', 'ts')
|
|
|
|
|
|
local tokens = tonumber(data[1])
|
|
|
|
|
|
local last_ts = tonumber(data[2])
|
|
|
|
|
|
-- 首次访问,初始化满桶
|
|
|
|
|
|
if tokens == nil then
|
|
|
|
|
|
tokens = burst
|
|
|
|
|
|
last_ts = now
|
|
|
|
|
|
end
|
|
|
|
|
|
-- 计算时间差,补充令牌
|
|
|
|
|
|
local delta = now - last_ts
|
|
|
|
|
|
if delta > 0 then
|
|
|
|
|
|
local refill = (delta * rate) / 1000
|
|
|
|
|
|
tokens = math.min(burst, tokens + refill)
|
|
|
|
|
|
end
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<v-clicks>
|
|
|
|
|
|
|
|
|
|
|
|
<Item title="为什么用 Lua 脚本?">
|
|
|
|
|
|
整个令牌桶逻辑(读取 → 计算 → 扣减 → 写回)在 Redis 服务端原子执行,避免竞态条件。单次网络往返,性能最优。
|
|
|
|
|
|
</Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Item title="关键逻辑">
|
|
|
|
|
|
**初始化**:首次访问设满桶令牌。**补充**:按时间差 × 速率补充,上限为 burst。**扣减**:请求到达时 tokens - 1,不足则拒绝。
|
|
|
|
|
|
</Item>
|
|
|
|
|
|
|
|
|
|
|
|
</v-clicks>
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# 限流中间件集成
|
|
|
|
|
|
|
|
|
|
|
|
Gin 中间件 + 统一错误格式
|
|
|
|
|
|
|
2026-05-30 23:01:46 +08:00
|
|
|
|
<v-clicks>
|
|
|
|
|
|
|
2026-05-30 22:42:23 +08:00
|
|
|
|
<Item title="中间件签名">
|
|
|
|
|
|
`func RateLimit(limiter Limiter, keyFunc func(c *gin.Context) string) gin.HandlerFunc` — 可配置的 key 提取函数,支持按用户ID、IP、端点等维度限流。
|
|
|
|
|
|
</Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Item title="注册方式">
|
|
|
|
|
|
|
|
|
|
|
|
- **用户级**:auth 路由组,key = userID,保护个人配额
|
|
|
|
|
|
- **全局级**:`/generate` 端点,key = endpoint,保护总容量
|
|
|
|
|
|
|
|
|
|
|
|
</Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Item title="响应规范">
|
|
|
|
|
|
|
|
|
|
|
|
- 通过:设置 `X-RateLimit-Remaining` 头
|
|
|
|
|
|
- 拒绝:HTTP 429 + `Retry-After` 头 + 统一 JSON 错误体
|
|
|
|
|
|
|
|
|
|
|
|
</Item>
|
2026-05-30 23:01:46 +08:00
|
|
|
|
|
|
|
|
|
|
</v-clicks>
|