Ai网关:基于 Redis + MySQL 的 LLM 应用层 RPM / TPM 限流方案
1. 背景
本文描述一个面向 B 端固定 AI 服务的应用层 RPM / TPM 限流方案。
业务具有以下特点:
- 以
service_id作为固定服务标识和限流主体。 - 每个服务具有相对固定的 Prompt,并动态拼接业务条件。
- 每个服务配置固定的 RPM(Requests Per Minute)和 TPM(Tokens Per
Minute)额度。 - RPM / TPM 配置由用户在管理后台维护。
- MySQL 是限流配置的权威数据源(Source of Truth)。
- Redis 同时承担:
- RPM / TPM 配置缓存;
- 当前分钟 RPM / TPM 实时计数;
- 最近 10 分钟 Output Token 统计。
- 管理后台修改配置后,先提交 MySQL,再删除对应 Redis 配置缓存。
- 在线请求发现 Redis 配置不存在时,通过
singleflight合并同一个
service_id的并发回源请求,避免缓存击穿。 - 请求进入模型前无法准确知道
output_tokens,使用最近 10 分钟历史
output_tokens估算。 - 请求进入模型前进行 Token 预扣,模型返回后根据实际 Usage 补扣或退款。
- RPM + TPM 判断及预扣通过 Redis Lua 原子执行。
第一版保持分钟固定窗口模型,不额外引入滑动窗口、Token Bucket
等复杂机制。
2. 核心定义
2.1 service_id
service_id 是一个固定 AI
业务服务的唯一标识,同时也是本方案中的主要限流维度。
例如:
service_id = svc_product_audit_shoes
所有配置缓存、RPM、TPM 和 Output 历史统计均围绕 service_id 组织。
2.2 RPM
RPM 表示一个服务一分钟内允许进入的请求数量。
例如:
service_id = svc_product_audit_shoes
RPM = 1000
表示该服务每分钟最多允许 1000 个请求进入模型调用链路。
2.3 TPM
TPM 表示一个服务一分钟内允许消耗的 Token 数量。
例如:
TPM = 500000
一次请求的实际 Token 消耗:
actual_tokens =
actual_input_tokens
+ actual_output_tokens
请求进入模型前 actual_output_tokens 尚不可知,因此需要预估。
2.4 Token 预估
本次预扣 Token:
reserve_tokens =
input_tokens
+ avg_output_tokens_10m
其中:
avg_output_tokens_10m
=
最近10分钟 output_tokens 总和
/
最近10分钟成功请求数
不能采用"每分钟平均值再求 10
分钟平均值",因为不同分钟请求量不同,会导致低流量分钟和高流量分钟拥有相同权重。
3. 总体架构
==============================================================================
配置管理链路
==============================================================================
┌──────────────────────────────────────────────────────────────────────────────┐
│ 管理后台 / B端控制台 │
└──────────────────────────────────────┬───────────────────────────────────────┘
│
│ 修改 service RPM / TPM
▼
┌──────────────────────────────┐
│ Config API │
│ UpdateServiceLimit() │
└──────────────┬───────────────┘
│
▼
UPDATE MySQL 配置
│
▼
COMMIT 成功
│
▼
DEL config:{service_id}
│
▼
配置修改完成
==============================================================================
在线请求链路
==============================================================================
用户请求
│
│ service_id
▼
┌─────────────────────────┐
│ API / Gateway │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ ConfigManager │
│ GetLimit(service_id) │
└────────────┬────────────┘
│
▼
┌─────────────────────────┐
│ Redis 配置缓存 │
│ config:{service_id} │
└────────────┬────────────┘
│
┌────────┴────────┐
│ │
HIT MISS
│ │
│ ▼
│ ┌─────────────────────┐
│ │ singleflight │
│ │ key = service_id │
│ └──────────┬──────────┘
│ │
│ ┌─────────┴──────────┐
│ │ │
│ Leader Followers
│ │ │
│ │ 等待 Leader
│ ▼ │
│ Redis Double Check │
│ │ │
│ 仍然 MISS │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │ MySQL │ │
│ │ SELECT rpm,tpm│ │
│ └───────┬───────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ 回填 Redis Cache │ │
│ │ HSET config:{id} │ │
│ │ EXPIRE ... │ │
│ └─────────┬──────────┘ │
│ │ │
└─────────────┴─────────────────┘
│
▼
得到 RPM / TPM
│
▼
计算 input_tokens
│
▼
Redis Pipeline 读取近10分钟
output token 统计
│
▼
avg_output = SUM(output_tokens)
------------------
SUM(count)
│
▼
reserve_tokens =
input + avg_output
│
▼
┌────────────────────────────────────────┐
│ Redis Lua 原子限流 │
│ │
│ RPM + 1 <= rpm_limit ? │
│ TPM + reserve <= tpm_limit ? │
└──────────────────┬─────────────────────┘
│
┌─────────┴─────────┐
│ │
超限 通过
│ │
▼ ▼
HTTP 429 RPM + 1 / TPM预扣
│
▼
┌────────────────┐
│ 调用 LLM │
└───────┬────────┘
│
▼
actual usage
│
▼
diff = actual - reserve
│
┌──────────────┴──────────────┐
│ │
diff > 0 diff < 0
│ │
补扣 退款
│ │
└──────────────┬──────────────┘
│
▼
更新当前分钟 TPM
│
▼
写入本分钟 output 统计
4. 配置存储设计
4.1 MySQL:权威配置存储
RPM / TPM 的最终权威值存储在 MySQL。
示例表:
CREATE TABLE service_limit (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
service_id VARCHAR(128) NOT NULL,
rpm INT NOT NULL,
tpm BIGINT NOT NULL,
status TINYINT NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE KEY uk_service_id(service_id)
);
示例:
service_id rpm tpm
------------------------------------------------
svc_product_audit_shoes 1000 500000
职责:
MySQL
│
└── RPM / TPM 配置事实源
Redis 中的配置只允许作为缓存使用。
5. Redis 数据模型
Redis 数据分为两大类:
Redis
│
├── 配置缓存
│ └── config:{service_id}
│
└── 运行时数据
├── rate:rpm:{service_id}:{minute}
├── rate:tpm:{service_id}:{minute}
└── usage:output:{service_id}:{minute}
5.1 RPM / TPM 配置缓存
Key:
config:{service_id}
类型:
HASH
示例:
HSET config:svc_product_audit_shoes rpm 1000 tpm 500000
读取:
HMGET config:svc_product_audit_shoes rpm tpm
设置 TTL:
EXPIRE config:svc_product_audit_shoes 600
示例数据:
config:svc_product_audit_shoes
rpm = 1000
tpm = 500000
5.2 当前分钟 RPM
Key:
rate:rpm:{service_id}:{yyyyMMddHHmm}
类型:
STRING
示例:
rate:rpm:svc_product_audit_shoes:202609042310
Value:
523
Redis:
GET rate:rpm:svc_product_audit_shoes:202609042310
INCR rate:rpm:svc_product_audit_shoes:202609042310
EXPIRE rate:rpm:svc_product_audit_shoes:202609042310 120
5.3 当前分钟 TPM
Key:
rate:tpm:{service_id}:{yyyyMMddHHmm}
类型:
STRING
示例:
rate:tpm:svc_product_audit_shoes:202609042310
Value:
312580
Redis:
GET rate:tpm:svc_product_audit_shoes:202609042310
INCRBY rate:tpm:svc_product_audit_shoes:202609042310 4000
INCRBY rate:tpm:svc_product_audit_shoes:202609042310 400
INCRBY rate:tpm:svc_product_audit_shoes:202609042310 -500
EXPIRE rate:tpm:svc_product_audit_shoes:202609042310 120
5.4 每分钟 Output Token 统计
Key:
usage:output:{service_id}:{yyyyMMddHHmm}
类型:
HASH
字段:
count
output_tokens
示例:
usage:output:svc_product_audit_shoes:202609042310
count = 85
output_tokens = 126500
更新:
HINCRBY usage:output:svc_product_audit_shoes:202609042310 count 1
HINCRBY usage:output:svc_product_audit_shoes:202609042310 output_tokens 1800
EXPIRE usage:output:svc_product_audit_shoes:202609042310 900
6. Redis Key 总览
┌──────────────────────────────────────────────────────────────┐
│ 配置缓存 │
│ │
│ config:{service_id} │
│ HASH │
│ ├── rpm │
│ └── tpm │
│ TTL = 600s │
├──────────────────────────────────────────────────────────────┤
│ 当前分钟 RPM │
│ │
│ rate:rpm:{service_id}:{minute} │
│ STRING │
│ value = 当前分钟请求数 │
│ TTL = 120s │
├──────────────────────────────────────────────────────────────┤
│ 当前分钟 TPM │
│ │
│ rate:tpm:{service_id}:{minute} │
│ STRING │
│ value = 当前分钟 Token 账本 │
│ TTL = 120s │
├──────────────────────────────────────────────────────────────┤
│ Output 历史统计 │
│ │
│ usage:output:{service_id}:{minute} │
│ HASH │
│ ├── count │
│ └── output_tokens │
│ TTL = 900s │
└──────────────────────────────────────────────────────────────┘
7. 后台修改 RPM / TPM
用户在后台修改:
RPM: 1000 → 1500
TPM: 500000 → 800000
处理顺序:
Admin
│
▼
Config API
│
▼
UPDATE MySQL
│
▼
COMMIT
│
▼
DEL Redis Config Cache
│
▼
完成
SQL:
UPDATE service_limit
SET
rpm = 1500,
tpm = 800000,
updated_at = NOW()
WHERE service_id = 'svc_product_audit_shoes';
MySQL 提交成功后:
DEL config:svc_product_audit_shoes
为什么必须先更新 MySQL,再删除 Redis?
正确顺序:
UPDATE MySQL
│
▼
COMMIT
│
▼
DEL Redis
如果反过来:
DEL Redis
│
▼
并发请求发现 MISS
│
▼
查询 MySQL
│
▼
读取旧配置
│
▼
旧配置重新写入 Redis
│
▼
MySQL 新配置才提交
这样会出现旧配置重新进入缓存的问题。
因此:
MySQL 更新并提交成功后,再删除 Redis 配置缓存。
8. ConfigManager
在线请求不直接操作 MySQL,而统一通过 ConfigManager 获取配置。
业务请求
│
▼
ConfigManager.GetLimit(serviceID)
│
▼
Redis
│
├── HIT ────────────────> 返回
│
└── MISS
│
▼
singleflight
│
▼
Redis Double Check
│
├── HIT ──────────> 返回
│
└── MISS
│
▼
MySQL
│
▼
回填 Redis
│
▼
返回
9. singleflight 防缓存击穿
假设后台刚执行:
DEL config:svc_product_audit_shoes
此时 1000 个请求同时到达。
如果没有 singleflight:
1000 requests
│
▼
1000 Redis MISS
│
▼
1000 MySQL SELECT
这会造成缓存击穿。
使用 singleflight:
Request A ─┐
Request B ─┤
Request C ─┤
Request D ─┤
... ├──── service_id
Request N ─┘
│
▼
singleflight.Do()
│
┌──────┴───────┐
│ │
Leader Followers
│ │
│ 等待 Leader
▼ │
MySQL │
│ │
▼ │
回填 Redis │
│ │
└──────┬───────┘
▼
所有请求返回
同一个 service_id 同一时间只有一个 Leader 回源 MySQL。
10. ConfigManager 伪代码
func GetLimit(serviceID string) (*LimitConfig, error) {
key := "config:" + serviceID
// 1. Redis First
cfg, err := GetConfigFromRedis(key)
if err == nil && cfg != nil {
return cfg, nil
}
// 2. Redis MISS
// 同 serviceID 请求合并
value, err, _ :=
singleflightGroup.Do(
serviceID,
func() (interface{}, error) {
// 3. Double Check
cfg, err :=
GetConfigFromRedis(key)
if err == nil && cfg != nil {
return cfg, nil
}
// 4. 回源 MySQL
cfg, err :=
GetConfigFromMySQL(serviceID)
if err != nil {
return nil, err
}
// 5. 回填 Redis
err = Redis.HSET(
key,
"rpm", cfg.RPM,
"tpm", cfg.TPM,
)
if err != nil {
// Redis 缓存失败不改变 MySQL 权威数据
// 根据业务策略记录日志/监控
}
Redis.EXPIRE(
key,
600,
)
return cfg, nil
},
)
if err != nil {
return nil, err
}
return value.(*LimitConfig), nil
}
Redis 读取:
HMGET config:svc_product_audit_shoes rpm tpm
Redis 回填:
HSET config:svc_product_audit_shoes rpm 1000 tpm 500000
EXPIRE config:svc_product_audit_shoes 600
11. 为什么 singleflight 内需要 Double Check?
请求过程可能是:
Request A
│
▼
Redis MISS
│
▼
准备进入 singleflight
│
│
│ 其他实例 / 请求已经完成缓存加载
│ │
│ ▼
│ HSET config:{service}
│
▼
singleflight Leader
如果 Leader 直接查询 MySQL,会产生一次不必要的数据库访问。
因此 Leader 真正回源前再次:
HMGET config:svc_product_audit_shoes rpm tpm
如果已经存在,直接返回。
12. 计算 Input Token
inputTokens :=
CountTokens(
fixedPrompt +
dynamicCondition +
userInput,
)
例如:
input_tokens = 2500
13. 获取最近 10 分钟 Output Token
假设当前时间:
23:10
读取:
23:00
23:01
...
23:09
Redis:
HMGET usage:output:svc_product_audit_shoes:202609042300 count output_tokens
HMGET usage:output:svc_product_audit_shoes:202609042301 count output_tokens
HMGET usage:output:svc_product_audit_shoes:202609042302 count output_tokens
HMGET usage:output:svc_product_audit_shoes:202609042303 count output_tokens
HMGET usage:output:svc_product_audit_shoes:202609042304 count output_tokens
HMGET usage:output:svc_product_audit_shoes:202609042305 count output_tokens
HMGET usage:output:svc_product_audit_shoes:202609042306 count output_tokens
HMGET usage:output:svc_product_audit_shoes:202609042307 count output_tokens
HMGET usage:output:svc_product_audit_shoes:202609042308 count output_tokens
HMGET usage:output:svc_product_audit_shoes:202609042309 count output_tokens
实际代码使用 Redis Pipeline。
func GetAvgOutputTokens(
serviceID string,
now time.Time,
) int {
totalCount := 0
totalOutputTokens := 0
pipeline := Redis.Pipeline()
for i := 1; i <= 10; i++ {
minute :=
FormatMinute(
now.Add(
-time.Duration(i) * time.Minute,
),
)
key :=
"usage:output:" +
serviceID +
":" +
minute
pipeline.HMGET(
key,
"count",
"output_tokens",
)
}
results := pipeline.Exec()
for _, result := range results {
count :=
ToInt(result[0])
outputTokens :=
ToInt(result[1])
totalCount += count
totalOutputTokens += outputTokens
}
if totalCount == 0 {
return DEFAULT_OUTPUT_TOKENS
}
return totalOutputTokens / totalCount
}
公式:
avg_output_tokens
=
SUM(output_tokens)
/
SUM(count)
14. Token 预扣
例如:
input_tokens = 2500
avg_output_tokens = 1500
则:
reserve_tokens
=
2500 + 1500
=
4000
15. RPM + TPM 原子限流
不能:
GET RPM
GET TPM
应用层判断
INCR RPM
INCRBY TPM
因为并发请求可能同时读取旧值,导致限流穿透。
必须:
Redis Lua
│
├── GET current RPM
├── GET current TPM
│
├── RPM + 1 > limit ?
│ └── YES → Reject
│
├── TPM + reserve > limit ?
│ └── YES → Reject
│
├── INCR RPM
└── INCRBY TPM reserve
Lua:
-- KEYS[1] RPM key
-- KEYS[2] TPM key
--
-- ARGV[1] RPM limit
-- ARGV[2] TPM limit
-- ARGV[3] reserve tokens
-- ARGV[4] TTL
local rpmKey = KEYS[1]
local tpmKey = KEYS[2]
local rpmLimit = tonumber(ARGV[1])
local tpmLimit = tonumber(ARGV[2])
local reserveTokens = tonumber(ARGV[3])
local ttl = tonumber(ARGV[4])
local currentRPM =
tonumber(redis.call("GET", rpmKey) or "0")
local currentTPM =
tonumber(redis.call("GET", tpmKey) or "0")
if currentRPM + 1 > rpmLimit then
return {
0,
"RPM_LIMIT",
currentRPM,
currentTPM
}
end
if currentTPM + reserveTokens > tpmLimit then
return {
0,
"TPM_LIMIT",
currentRPM,
currentTPM
}
end
local newRPM =
redis.call(
"INCR",
rpmKey
)
local newTPM =
redis.call(
"INCRBY",
tpmKey,
reserveTokens
)
if newRPM == 1 then
redis.call(
"EXPIRE",
rpmKey,
ttl
)
end
if newTPM == reserveTokens then
redis.call(
"EXPIRE",
tpmKey,
ttl
)
end
return {
1,
"OK",
newRPM,
newTPM
}
调用:
EVAL "<lua-script>" 2 \
rate:rpm:svc_product_audit_shoes:202609042310 \
rate:tpm:svc_product_audit_shoes:202609042310 \
1000 \
500000 \
4000 \
120
16. 调用 LLM
限流通过:
RPM + 1
TPM + reserve_tokens
然后调用:
resp, err := CallLLM(req)
假设:
{
"input_tokens": 2600,
"output_tokens": 1800
}
则:
actual_tokens
=
2600 + 1800
=
4400
17. 实际 Token 结算
reserve = 4000
actual = 4400
diff = +400
补扣:
INCRBY rate:tpm:svc_product_audit_shoes:202609042310 400
如果:
reserve = 4000
actual = 3500
diff = -500
退款:
INCRBY rate:tpm:svc_product_audit_shoes:202609042310 -500
伪代码:
func SettleTPM(
serviceID string,
minute string,
diff int,
) {
if diff == 0 {
return
}
key :=
"rate:tpm:" +
serviceID +
":" +
minute
Redis.INCRBY(
key,
diff,
)
}
预扣负责:
是否允许请求进入模型
结算负责:
修正 TPM 实际账本
模型已经返回后,即使实际 Token
高于预估,也不能再拒绝本次请求;补扣会减少当前分钟后续请求的可用额度。
18. Output Token 历史更新
模型成功后:
actual_output_tokens = 1800
更新:
HINCRBY usage:output:svc_product_audit_shoes:202609042310 count 1
HINCRBY usage:output:svc_product_audit_shoes:202609042310 output_tokens 1800
EXPIRE usage:output:svc_product_audit_shoes:202609042310 900
Lua:
local key = KEYS[1]
local outputTokens = tonumber(ARGV[1])
local ttl = tonumber(ARGV[2])
redis.call(
"HINCRBY",
key,
"count",
1
)
redis.call(
"HINCRBY",
key,
"output_tokens",
outputTokens
)
redis.call(
"EXPIRE",
key,
ttl
)
return 1
19. LLM 调用失败
如果已经:
RPM + 1
TPM + reserve
但 LLM 调用失败,并确认没有产生实际 Token Usage:
RPM:不回滚
TPM:回滚预扣
例如:
INCRBY rate:tpm:svc_product_audit_shoes:202609042310 -4000
状态:
RPM
500
│
▼
501
│
└── LLM失败,保持501
TPM
300000
│
│ reserve 4000
▼
304000
│
│ LLM失败
▼
300000
20. 完整请求伪代码
func HandleRequest(req Request) Response {
serviceID := req.ServiceID
now :=
Now()
minute :=
FormatMinute(now)
// --------------------------------
// 1. ConfigManager 获取限流配置
//
// Redis HIT -> 直接返回
// Redis MISS -> singleflight
// -> double check
// -> MySQL
// -> Redis 回填
// --------------------------------
cfg, err :=
ConfigManager.GetLimit(
serviceID,
)
if err != nil {
return Error(err)
}
rpmLimit :=
cfg.RPM
tpmLimit :=
cfg.TPM
// --------------------------------
// 2. 计算 input tokens
// --------------------------------
inputTokens :=
CountTokens(
req.FixedPrompt +
req.DynamicCondition +
req.UserInput,
)
// --------------------------------
// 3. 最近10分钟 output
// --------------------------------
avgOutputTokens :=
GetAvgOutputTokens(
serviceID,
now,
)
// --------------------------------
// 4. 计算预扣
// --------------------------------
reserveTokens :=
inputTokens +
avgOutputTokens
// --------------------------------
// 5. Redis Lua 原子限流
// --------------------------------
result :=
ReserveRateLimit(
serviceID,
minute,
rpmLimit,
tpmLimit,
reserveTokens,
)
if !result.Allowed {
return HTTP429
}
// --------------------------------
// 6. 调用 LLM
// --------------------------------
resp, err :=
CallLLM(req)
if err != nil {
// RPM 不回滚
// TPM 回滚
RollbackTPM(
serviceID,
minute,
reserveTokens,
)
return Error(err)
}
// --------------------------------
// 7. 实际 usage
// --------------------------------
actualInput :=
resp.Usage.InputTokens
actualOutput :=
resp.Usage.OutputTokens
actualTokens :=
actualInput +
actualOutput
// --------------------------------
// 8. TPM 实际结算
// --------------------------------
diff :=
actualTokens -
reserveTokens
SettleTPM(
serviceID,
minute,
diff,
)
// --------------------------------
// 9. 更新 output 历史
// --------------------------------
RecordOutputUsage(
serviceID,
minute,
actualOutput,
)
return resp
}
21. 完整 Redis / MySQL 调用链
用户请求
│
▼
ConfigManager.GetLimit()
│
▼
HMGET config:{service_id} rpm tpm
│
┌────────┴────────┐
│ │
HIT MISS
│ │
│ ▼
│ singleflight
│ │
│ Redis Double Check
│ │
│ MISS
│ │
│ ▼
│ SELECT rpm,tpm
│ FROM MySQL
│ │
│ ▼
│ HSET config:{service}
│ EXPIRE config:{service}
│ │
└────────┬────────┘
│
▼
RPM / TPM
│
▼
Count input tokens
│
▼
Redis Pipeline
│
┌──────────────┼──────────────┐
│ │ │
minute-1 minute-2 ... minute-10
│ │ │
└──────────────┼──────────────┘
▼
avg_output_tokens
│
▼
reserve_tokens
│
▼
EVAL Reserve Lua
│
┌──────────┴──────────┐
│ │
Reject Allow
│ │
▼ ▼
HTTP 429 Call LLM
│
▼
actual usage
│
▼
INCRBY TPM diff
│
▼
Record Output Usage
│
▼
Response
22. 后台配置更新完整链路
管理后台
│
│ 修改 RPM / TPM
▼
Config API
│
▼
BEGIN TRANSACTION
│
▼
UPDATE service_limit
│
▼
COMMIT
│
┌──────┴──────┐
│ │
SUCCESS FAILED
│ │
▼ ▼
DEL config:{service_id} 返回失败
│
▼
完成
下一次请求:
Request
│
▼
Redis Config MISS
│
▼
singleflight
│
▼
Redis Double Check
│
▼
MySQL
│
▼
最新 RPM/TPM
│
▼
HSET Redis
│
▼
后续请求 HIT
23. Redis 数据结构汇总
数据 Redis 类型 Key TTL
RPM / TPM 配置缓存 HASH config:{service_id} 600s
RPM 分钟桶 STRING rate:rpm:{service_id}:{minute} 120s
TPM 分钟桶 STRING rate:tpm:{service_id}:{minute} 120s
Output 分钟统计 HASH usage:output:{service_id}:{minute} 900s
MySQL:
数据 存储
service_id MySQL
RPM MySQL
TPM MySQL
status MySQL
created_at / updated_at MySQL
24. 模块职责
┌───────────────────────────────────────────────────────────┐
│ Config API │
│ │
│ 用户修改 RPM / TPM │
│ MySQL 更新 │
│ Redis 配置缓存失效 │
└──────────────────────────┬────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────┐
│ ConfigManager │
│ │
│ Redis Config Cache │
│ Cache Miss │
│ singleflight │
│ Double Check │
│ MySQL 回源 │
│ Redis 回填 │
└──────────────────────────┬────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────┐
│ RateLimiter │
│ │
│ RPM 判断 │
│ TPM 判断 │
│ Token 预扣 │
│ Redis Lua 原子操作 │
└──────────────────────────┬────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────┐
│ LLM Client │
│ │
│ 模型请求 │
│ 获取 actual usage │
└──────────────────────────┬────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────┐
│ Usage / Settlement │
│ │
│ TPM 补扣 / 退款 │
│ Output Token 分钟统计 │
│ 最近10分钟历史预估 │
└───────────────────────────────────────────────────────────┘
25. 方案总结
完整方案可以概括为:
┌──────────────┐
│ 管理后台 │
└──────┬───────┘
│
▼
Config API
│
▼
MySQL
配置事实源
│
更新后 DEL Cache
│
▼
Redis config:{service}
配置缓存
▲
│
MISS + singleflight
│
▼
ConfigManager
│
RPM/TPM
│
▼
RateLimiter
│
┌──────────┴──────────┐
▼ ▼
RPM Minute Bucket TPM Minute Bucket
│ │
└──────────┬──────────┘
│
▼
Redis Lua 预扣
│
▼
LLM
│
▼
Actual Usage
│
▼
TPM 实际结算
│
▼
Output Minute Usage
│
▼
最近10分钟统计
│
└────> 下一次 Token 预估
第一版核心原则:
- MySQL 是 RPM / TPM 配置的唯一权威数据源。
- Redis 的
config:{service_id}只作为配置缓存。 - 后台修改配置时,先更新并提交 MySQL,再
DEL config:{service_id}。 - 在线请求优先从 Redis 获取配置。
- 配置 Cache Miss 时,使用
singleflight(service_id)
合并并发回源请求。 - singleflight Leader 回源 MySQL 前再次 Double Check Redis。
service_id + minute作为 RPM / TPM 运行时计数维度。- RPM 按请求数计数。
- TPM 按 Token 数计数。
- Output Token 使用最近 10 分钟
SUM(output_tokens) / SUM(count)
进行预估。 - 请求进入模型前执行 Token 预扣。
- RPM + TPM 判断及预扣通过 Redis Lua 原子完成。
- 模型返回后使用 Actual Usage 对 TPM 进行补扣或退款。
- 实际 Output Token 写入分钟级 Redis Hash,为后续请求提供预估数据。
- LLM 调用失败且确认未产生 Usage 时,RPM 不回滚,TPM 预扣回滚。

浙公网安备 33010602011771号