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 预估

第一版核心原则:

  1. MySQL 是 RPM / TPM 配置的唯一权威数据源。
  2. Redis 的 config:{service_id} 只作为配置缓存。
  3. 后台修改配置时,先更新并提交 MySQL,再 DEL config:{service_id}。
  4. 在线请求优先从 Redis 获取配置。
  5. 配置 Cache Miss 时,使用 singleflight(service_id)
    合并并发回源请求。
  6. singleflight Leader 回源 MySQL 前再次 Double Check Redis。
  7. service_id + minute 作为 RPM / TPM 运行时计数维度。
  8. RPM 按请求数计数。
  9. TPM 按 Token 数计数。
  10. Output Token 使用最近 10 分钟 SUM(output_tokens) / SUM(count)
    进行预估。
  11. 请求进入模型前执行 Token 预扣。
  12. RPM + TPM 判断及预扣通过 Redis Lua 原子完成。
  13. 模型返回后使用 Actual Usage 对 TPM 进行补扣或退款。
  14. 实际 Output Token 写入分钟级 Redis Hash,为后续请求提供预估数据。
  15. LLM 调用失败且确认未产生 Usage 时,RPM 不回滚,TPM 预扣回滚。
posted @ 2026-09-04 23:58  alin_qu  阅读(38)  评论(0)    收藏  举报