New API 扣费链路架构分析

1. 文档范围

  • 阶段:方案调研
  • 对象:当前 new-api 源码中的同步 Relay、用户钱包、API Token 和异步 Task 扣费
  • 重点:多 Pod 场景下,Redis Lua 如何执行额度扣减,MySQL 如何持久化余额和统计
  • 证据边界:本文基于源码静态分析,未执行多 Pod 压测、故障注入或线上对账

2. 前置背景与部署假设

2.1 业务计费背景

一次请求可能同时包含输入 Token、输出 Token、缓存 Token、图片、音频和工具调用等计费项。它们的价格规则不同,当前实现先读取 usage 明细,再按模型价格、分组倍率和各类附加倍率,统一折算成内部额度值 Quota。

这里要先区分两类 Token:

输入 Token、输出 Token、缓存 Token
    -> 本次请求的 usage 明细

API Token
    -> 调用网关使用的访问密钥
    -> 拥有 remain_quota 和 used_quota

actualQuota 是本次请求按真实 usage 计算出的实际费用换算值。它不是输入 Token、输出 Token 的数量,也不是货币金额,而是 new-api 用来统一扣减用户钱包和 API Token 额度的内部单位。

flowchart LR A[输入 Token] --> F[价格与倍率计算] B[输出 Token] --> F C[缓存 Token] --> F D[图片 / 音频] --> F E[工具调用附加费] --> F F --> G[actualQuota<br/>本次实际费用] G --> H[用户钱包 quota] G --> I[API Token<br/>remain_quota / used_quota] A --> J[消费日志<br/>保存 usage 明细] B --> J C --> J

请求开始前还没有真实输出 usage,因此系统先根据估算输入和 max_tokens 计算 preConsumedQuota,先扣住一笔可能的最大额度。请求完成后,再用真实 usage 计算 actualQuota,得到:

delta = actualQuota - preConsumedQuota
flowchart LR A[估算输入 Token<br/>+ max_tokens] --> B[preConsumedQuota] B --> C[请求前预扣] D[上游真实 usage] --> E[actualQuota] E --> F[delta = actualQuota - preConsumedQuota] F --> G{delta} G -->|大于 0| H[补扣差额] G -->|等于 0| I[无需调整] G -->|小于 0| J[返还多预扣额度]

普通倍率计费可以抽象为:

baseInput = PromptTokens
          - CachedTokens
          - CacheCreationTokens
          - ImageTokens
          - AudioTokens

promptQuota = baseInput
            + CachedTokens * CacheRatio
            + CacheCreationTokens * CacheCreationRatio
            + ImageTokens * ImageRatio

completionQuota = CompletionTokens * CompletionRatio

actualQuota = (promptQuota + completionQuota)
            * ModelRatio
            * GroupRatio
            + AudioQuota
            + ToolSurchargeQuota

缓存、图片和音频部分从普通输入中拆出,是为了让它们按各自倍率单独计费,避免重复计算。固定价格模型和 tiered_expr 模式也会在完成价格计算后得到统一的 Quota。

2.1.1 计费示例

假设一次请求返回:

PromptTokens     = 1000
CachedTokens     = 200
CompletionTokens = 300

ModelRatio       = 1
GroupRatio       = 1
CacheRatio       = 0.1
CompletionRatio  = 2
flowchart LR A[Prompt 1000] --> B[普通输入 800] C[缓存 200] --> D[缓存费用 200 x 0.1 = 20] E[输出 300] --> F[输出费用 300 x 2 = 600] B --> G[PromptQuota 800] D --> H[PromptQuota 820] G --> H H --> I[加 CompletionQuota 600] F --> I I --> J[ModelRatio 1<br/>GroupRatio 1] J --> K[actualQuota = 1420]

计算过程:

普通输入费用 = (1000 - 200) * 1 = 800
缓存输入费用 = 200 * 0.1 = 20
输出费用     = 300 * 2 = 600

actualQuota  = (800 + 20 + 600) * 1 * 1
              = 1420

请求开始前,如果估算输入为 1000、max_tokens 为 500:

preConsumedQuota = (1000 + 500) * 1 * 1
                 = 1500

请求完成后:

delta = actualQuota - preConsumedQuota
      = 1420 - 1500
      = -80

因此结算阶段返还 80 个 Quota。后面的 Redis Lua 和 MySQL 不再分别处理输入 Token、输出 Token 和缓存 Token,而是处理这个统一的额度差额。

tokens.remain_quota 和 tokens.used_quota 保存的是 Quota 额度汇总;usage 明细和输入、输出、缓存的拆分则保存在消费日志中。

对应源码:

2.2 部署假设

多个 Pod
    -> 一个共享 Redis
    -> 一个共享 MySQL

所有 Pod 使用同一个 Redis 连接配置和同一个 MySQL 数据库连接配置。

组件 用途
Redis 共享额度缓存、Lua 原子预扣、额度增量和热点状态
MySQL 余额、使用统计、消费日志、任务状态和配置数据
BillingSession 当前请求所在 Pod 内的计费状态
批量更新队列 当前 Pod 内存中的延迟落库队列

配置入口:

内容 源码
Redis 客户端 common/redis.go
Redis 连接 .env.example 的 REDIS_CONN_STRING
数据库连接 model/main.go

3. 整体架构

flowchart LR C[客户端] --> LB[负载均衡] subgraph Pods[多个 Pod] PA[Pod A] PB[Pod B] PC[Pod C] end LB --> PA LB --> PB LB --> PC subgraph Request[请求内计费] P[PriceHelper<br/>计算 Quota] B[BillingSession<br/>预扣 / 结算 / 退款] end PA --> P PB --> P PC --> P P --> B subgraph Redis[共享 Redis] LUA[Lua EVAL<br/>原子检查与修改] UH[User Hash<br/>用户钱包余额] TH[Token Hash<br/>API Token 额度] end subgraph MySQL[共享 MySQL] U[users<br/>钱包余额与统计] T[tokens<br/>API Token 额度] R[logs / tasks / channels<br/>明细与状态] end B --> LUA LUA --> UH LUA --> TH LUA -->|成功后返回| B B -->|余额增量落库| U B -->|余额增量落库| T B -.->|Redis 不可用时条件更新| U B -.->|Redis 不可用时条件更新| T B --> R B -->|调用| AI[上游 AI] AI -->|结果 / usage| B B -->|成功或错误响应| C

各组件的作用:

组件 作用
PriceHelper 将输入、输出、缓存等 usage 按价格规则折算为统一 Quota
BillingSession 保存当前请求的预扣、结算和退款状态
Redis Lua 处理多个 Pod 之间的实时额度竞争
MySQL 持久化余额、统计、消费日志和任务状态
MySQL 条件更新 Redis 不可用时提供余额检查和原子扣减

4. 扣费链路与核心技术

4.1 三条核心链路

flowchart LR A[请求] --> B[计算并预扣<br/>preConsumedQuota] B --> C{预扣结果} C -->|失败| D[返回额度错误] C -->|成功| E[调用上游] E --> F{上游结果} F -->|成功| G[计算 actualQuota] G --> H[按 delta<br/>补扣或返还] H --> I[统计与消费日志] I --> J[返回成功结果] F -->|失败| K[退回预扣额度] K --> L[返回上游错误]

三条链路可以概括为:

链路 核心行为
预扣 请求开始前,按估算额度检查并扣减钱包和 API Token 额度
结算 请求成功后,用 actualQuota - preConsumedQuota 补扣或返还
退款 上游失败时,退回本次请求已经预扣的额度

异步 Task 复用相同模型:提交时预扣,成功时结算,失败时退款。

4.2 Redis Lua 与 MySQL 如何完成并发扣费

flowchart LR A[TryReserveQuota] --> B{Redis 缓存可用?} B -->|是| C[Reserve Lua] C --> D{Lua 返回值} D -->|1| E[Redis 原子预扣成功] D -->|0| X[返回额度不足] D -->|-1| F[从 MySQL 读取<br/>补齐 Redis 缓存] F -->|补齐成功| R[重试一次 Reserve Lua] R --> S{重试结果} S -->|1| E S -->|0| X S -->|-1 / error| G F -->|补齐失败| G[MySQL 条件 UPDATE] B -->|Redis 异常| G G -->|RowsAffected = 1| H[MySQL 预扣成功] G -->|RowsAffected = 0| X E --> I{批量落库?} I -->|否| J[MySQL 增量 UPDATE] I -->|是| K[当前 Pod 内存队列] K --> L[定时聚合写 MySQL] J --> M[继续调用上游] L --> M H --> M J -->|写库失败| N[Redis Delta Lua<br/>反向补偿] N --> O[返回预扣失败]

4.2.1 实际 Lua 脚本

Reserve Lua 的核心是把“校验缓存、检查余额、修改余额”放在一次 Redis EVAL 中。

用户钱包使用 userQuotaReserveScript:

-- KEYS[1]:用户缓存 Key,例如 user:123
-- ARGV[1]:本次预扣额度 amount
-- ARGV[2]:用户 ID
-- ARGV[3]:缓存结构版本

-- 第一步:确认当前 Hash 属于目标用户,并且缓存字段完整、版本正确。
if tonumber(redis.call('HGET', KEYS[1], 'Id') or '0') ~= tonumber(ARGV[2])
  or tonumber(redis.call('HGET', KEYS[1], 'CacheSchema') or '0') ~= tonumber(ARGV[3])
  or redis.call('HEXISTS', KEYS[1], 'Quota') == 0 then
  -- 返回 -1:缓存不存在、不完整或版本不匹配,调用方尝试从 MySQL 补缓存。
  return -1
end

-- 第二步:读取当前用户钱包余额。
local quota = tonumber(redis.call('HGET', KEYS[1], 'Quota'))
if quota == nil or quota < tonumber(ARGV[1]) then
  -- 返回 0:余额不足,不修改任何字段。
  return 0
end

-- 第三步:余额充足时原子扣减。
-- HINCRBY 传入负数,相当于 Quota = Quota - amount。
redis.call('HINCRBY', KEYS[1], 'Quota', -tonumber(ARGV[1]))

-- 返回 1:预扣成功。
return 1

参数含义:

参数 值
KEYS[1] user:{userId}
ARGV[1] 本次需要预扣的 Quota
ARGV[2] 用户 ID
ARGV[3] 用户缓存结构版本

API Token 使用 tokenQuotaReserveScript:

-- KEYS[1]:API Token 缓存 Key,例如 token:{HMAC(tokenKey)}
-- ARGV[1]:本次预扣额度 amount
-- ARGV[2]:API Token ID
-- ARGV[3]:当前时间戳

-- 第一步:确认 Hash 属于目标 API Token,并且额度字段完整。
if tonumber(redis.call('HGET', KEYS[1], 'Id') or '0') ~= tonumber(ARGV[2])
  or redis.call('HEXISTS', KEYS[1], 'RemainQuota') == 0
  or redis.call('HEXISTS', KEYS[1], 'UsedQuota') == 0 then
  -- 返回 -1:缓存不存在、字段不完整或 Token 身份不匹配。
  return -1
end

-- 第二步:读取 API Token 当前剩余额度。
local remain = tonumber(redis.call('HGET', KEYS[1], 'RemainQuota'))
if remain == nil or remain < tonumber(ARGV[1]) then
  -- 返回 0:剩余额度不足,下面三个修改都不会执行。
  return 0
end

-- 第三步:在同一次 EVAL 中完成三项修改。
-- 剩余额度减少 amount。
redis.call('HINCRBY', KEYS[1], 'RemainQuota', -tonumber(ARGV[1]))

-- 累计已用额度增加 amount。
redis.call('HINCRBY', KEYS[1], 'UsedQuota', tonumber(ARGV[1]))

-- 记录本次访问时间。
redis.call('HSET', KEYS[1], 'AccessedTime', ARGV[3])

-- 返回 1:预扣成功。
return 1

参数含义:

参数 值
KEYS[1] token:{HMAC(tokenKey)}
ARGV[1] 本次需要预扣的 Quota
ARGV[2] API Token ID
ARGV[3] 当前时间戳

脚本返回值:

返回值 含义 后续处理
1 Redis 预扣成功 将相同额度增量持久化到 MySQL
0 余额不足 返回额度不足,不修改余额
-1 Hash 缺失、字段不完整或身份不匹配 从 MySQL 补齐 Redis 后重试一次
Redis error Redis 调用失败 改走 MySQL 条件更新

结算和退款使用 Delta Lua,不再检查余额:

用户:Quota += delta

API Token:
RemainQuota += delta
UsedQuota   -= delta

例如退款 80 时,delta = 80;补扣 30 时,调用方传入的余额增量为 delta = -30。

4.2.2 MySQL SQL

Redis 不可用时,MySQL 直接承担“检查余额 + 扣减”的并发控制。

用户钱包预扣:

-- 在一条 UPDATE 中同时完成余额判断和扣减。
UPDATE users
SET quota = quota - :amount
WHERE id = :user_id
  -- 条件不成立时不会更新该行,避免并发请求把余额扣成负数。
  AND quota >= :amount;

API Token 预扣:

-- remain_quota 和 used_quota 在同一条 UPDATE 中更新。
UPDATE tokens
-- 剩余额度减少,累计已用额度增加。
SET remain_quota = remain_quota - :amount,
    used_quota = used_quota + :amount,
    accessed_time = :now
WHERE id = :token_id
  -- 余额不足时条件不成立,RowsAffected 为 0。
  AND remain_quota >= :amount;

数据库通过 RowsAffected 判断结果:

RowsAffected = 1 -> 预扣成功
RowsAffected = 0 -> 余额不足或记录不存在
SQL error        -> 数据库操作失败

Redis 预扣成功后,MySQL 不再重复检查余额,而是持久化同一个增量。

用户钱包落库:

-- :delta 表示余额变化量。
-- 预扣时为负数,退款时为正数。
UPDATE users
SET quota = quota + :delta
WHERE id = :user_id;

API Token 落库:

-- :delta 表示 RemainQuota 的变化量。
-- 预扣时 delta < 0,退款时 delta > 0。
UPDATE tokens
SET remain_quota = remain_quota + :delta,
    -- UsedQuota 与 RemainQuota 反向变化。
    used_quota = used_quota - :delta,
    accessed_time = :now
WHERE id = :token_id;

预扣 80 时,:delta = -80:

remain_quota = remain_quota - 80
used_quota   = used_quota + 80

直写 MySQL 失败时,代码用 Delta Lua 把 Redis 中刚预扣的额度加回去。批量模式下,增量只进入当前 Pod 的内存队列,调用时无法立即知道后续定时落库是否成功。

4.2.3 两个 Pod 同时预扣示例

假设 API Token 的初始状态为:

RemainQuota = 100
UsedQuota   = 0

Pod A 请求预扣 80
Pod B 请求预扣 80

Redis 正常时:

sequenceDiagram participant A as Pod A participant R as Shared Redis participant B as Pod B participant DB as Shared MySQL par 两个 Pod 并发执行 A->>R: EVAL Reserve Lua, amount=80 B->>R: EVAL Reserve Lua, amount=80 end R-->>A: 1,预扣成功 Note over R: RemainQuota=20<br/>UsedQuota=80 R-->>B: 0,余额不足 A->>DB: remain_quota - 80<br/>used_quota + 80 DB-->>A: RowsAffected=1 B-->>B: 返回额度不足

Redis 会顺序执行两个 Lua 脚本。本例假设 Pod A 先执行,它将余额从 100 扣到 20;Pod B 随后读取到的是 20,因此不能再次扣除 80。实际运行中也可能由 Pod B 先成功。

Redis 不可用时:

sequenceDiagram participant A as Pod A participant DB as Shared MySQL participant B as Pod B par 两个 Pod 并发执行条件更新 A->>DB: UPDATE ... WHERE remain_quota >= 80 B->>DB: UPDATE ... WHERE remain_quota >= 80 end DB-->>A: RowsAffected=1 Note over DB: RemainQuota=20<br/>UsedQuota=80 DB-->>B: RowsAffected=0 B-->>B: 返回额度不足

MySQL 对同一行的条件更新形成并发边界。本例假设 Pod A 先更新;实际运行中任一 Pod 都可能先成功。第一个更新完成后,另一个更新的 remain_quota >= 80 条件不再成立。

这两条路径都能防止同一个余额被并发超扣,但 Redis 路径还需要后续 MySQL 落库,因此 Redis 与 MySQL 之间仍然不存在一个统一事务。

4.2.4 持久化模式

模式 Redis MySQL
直写模式 Lua 立即修改共享余额 当前请求立即执行增量 UPDATE
批量模式 Lua 立即修改共享余额 增量进入当前 Pod 内存队列,定时聚合写入
Redis fallback 不使用 Redis 余额 MySQL 条件 UPDATE 直接完成预扣

批量模式下,各 Pod 的队列彼此独立;共享 Redis 是实时余额边界,共享 MySQL 是最终持久化存储。

对应源码:

4.3 结算、退款与 BillingSession

flowchart LR A[预扣成功] --> B{上游结果} B -->|成功| C[计算 actualQuota] C --> D[delta = actual - pre] D --> E{delta} E -->|大于 0| F[补扣钱包和 API Token] E -->|等于 0| G[无需调整] E -->|小于 0| H[返还钱包和 API Token] F --> I[settled = true] G --> I H --> I B -->|失败| J{需要退款?} J -->|否| K[不再退款] J -->|是| L[refunded = true] L --> M[异步返还钱包] L --> N[异步返还 API Token]

BillingSession 保存当前请求的核心状态:

状态 作用
preConsumedQuota 本次已经预扣的额度
tokenConsumed API Token 已扣额度
fundingSettled 钱包等资金来源是否已经结算
settled 防止同一会话重复结算
refunded 防止同一会话重复发起退款
mu 保护当前会话状态

结算时先调整资金来源,再调整 API Token。退款通过进程内 gopool 异步执行。

BillingSession 的保护范围是当前 Pod 内的同一个对象:

同一个 BillingSession 被重复 Settle
    -> settled 拦截

同一个 BillingSession 被重复 Refund
    -> refunded 拦截

另一个 Pod 或另一个进程
    -> 不共享这个状态

对应源码:

5. 方案优点与限制

5.1 两个开关

当前源码中影响扣费路径的主要开关:

开关 配置方式 影响
RedisEnabled 配置 REDIS_CONN_STRING;未配置时关闭 是否使用共享 Redis Hash 和 Redis Lua 处理额度
BatchUpdateEnabled 配置 BATCH_UPDATE_ENABLED=true 余额增量是否先进入当前 Pod 的内存队列
BATCH_UPDATE_INTERVAL 配置批量刷新间隔,默认由系统配置读取 批量队列多久尝试写入共享 MySQL

两个开关相互独立,因此形成四种运行模式。RedisEnabled 主要决定实时扣费在哪里完成,BatchUpdateEnabled 主要决定 MySQL 余额增量是直写还是延迟批量写入。

下表描述的是各开关组合下的正常主路径。即使 RedisEnabled=true,如果 Redis 执行失败或缓存无法水合,预扣仍会回退到 MySQL 条件更新。

5.2 四种运行模式

模式 Redis 批量更新 预扣路径 结算 / 退款路径 MySQL 写入压力 大流量下的特点
A 开 关 Redis Reserve Lua 检查并扣减,随后直接写 MySQL Redis Delta Lua 异步调整,随后直接写 MySQL 高 余额持久化及时,链路简单;每次预扣、结算和退款都可能触发 MySQL 写入
B 开 开 Redis Reserve Lua 检查并扣减,额度增量进入当前 Pod 内存队列 Redis Delta Lua 异步调整,额度增量进入当前 Pod 内存队列 较低 适合大流量,减少 MySQL 写入;MySQL 延迟、队列积压和 Pod 退出丢失窗口增大
C 关 关 MySQL 条件 UPDATE 直接检查余额并扣减 MySQL 增量 UPDATE 直接执行 高 不依赖 Redis,余额以 MySQL 为准;所有 Pod 竞争数据库行,数据库压力最大
D 关 开 MySQL 条件 UPDATE 直接检查余额并扣减 增量进入当前 Pod 内存队列,定时写 MySQL 高 预扣仍同步依赖 MySQL,结算和退款可以批量化;Redis 关闭时无法获得 Redis 主路径的高并发能力

四种模式的共同点:

多个 Pod
    -> 共享 Redis Lua 或共享 MySQL 条件 UPDATE 完成预扣竞争
    -> 用户钱包和 API Token 都执行额度变化
    -> BillingSession 只管理当前请求内的状态

5.3 各开关组合的处理细节

模式 A:Redis 开启,批量更新关闭

flowchart LR A[请求] --> B[Redis Reserve Lua] B --> C[预扣成功] C --> D[MySQL 余额增量直写] D --> E[调用上游] E --> F[结算或退款] F --> G[Redis Delta Lua] G --> H[MySQL 增量直写]
项目 行为
并发控制 Redis Lua 负责余额检查和预扣原子性
MySQL 预扣成功后直接保存增量,结算和退款也直接更新
优点 MySQL 状态相对及时,异常窗口较小,链路容易观察
代价 高流量下 MySQL 写入次数多,余额热点行容易出现锁竞争

模式 B:Redis 开启,批量更新开启

flowchart LR A[请求] --> B[Redis Reserve Lua] B --> C[Redis 预扣成功] C --> D[额度增量进入当前 Pod 队列] D --> E[调用上游] E --> F[结算或退款] F --> G[Redis Delta Lua] G --> H[额度增量进入当前 Pod 队列] H --> I[定时聚合写 MySQL]
项目 行为
并发控制 Redis Lua 负责多个 Pod 的实时余额竞争
MySQL 预扣、结算和退款的增量先入当前 Pod 队列,再定时写入共享 MySQL
优点 正常扣费不需要每次同步等待 MySQL 写入,能够显著减少数据库写压力
代价 队列是进程内存;Pod 重启、崩溃或批量写入失败时存在延迟和未落库窗口
额外边界 persistUserQuotaDelta、persistTokenQuotaDelta 在批量模式下入队即返回成功,调用方不能立即知道后续落库结果

模式 C:Redis 关闭,批量更新关闭

flowchart LR A[请求] --> B[MySQL 条件 UPDATE] B --> C{RowsAffected} C -->|1| D[预扣成功] C -->|0| E[余额不足] D --> F[调用上游] F --> G[结算或退款] G --> H[MySQL 增量 UPDATE]
项目 行为
并发控制 依赖 MySQL 的条件更新和数据库行级并发控制
MySQL 预扣、结算和退款全部直接访问 MySQL
优点 没有 Redis/MySQL 双写,余额事实来源单一
代价 所有 Pod 的扣费竞争和写入都集中到 MySQL;高流量下连接池、行锁和磁盘 I/O 更容易成为瓶颈

模式 D:Redis 关闭,批量更新开启

flowchart LR A[请求] --> B[MySQL 条件 UPDATE] B --> C[预扣成功] C --> D[调用上游] D --> E[结算或退款] E --> F[进入当前 Pod 队列] F --> G[定时聚合写 MySQL]
项目 行为
并发控制 预扣仍依赖 MySQL 条件更新
MySQL 预扣同步直写;结算和退款可进入批量队列
优点 结算、退款的 MySQL 写入可以聚合,减少部分写放大
代价 预扣仍然逐请求访问 MySQL,无法像 Redis 主路径那样把实时竞争从数据库转移出去
风险 退款和结算依赖当前 Pod 的内存队列,Pod 退出时可能存在未落库增量

5.4 大流量下的当前方案判断

以下判断基于源码静态分析,当前没有多 Pod 压测数据,不使用具体 QPS 结论。

维度 模式 A:Redis 开 / 批量关 模式 B:Redis 开 / 批量开 模式 C:Redis 关 / 批量关 模式 D:Redis 关 / 批量开
实时扣费位置 Redis Redis MySQL MySQL
预扣并发能力 较好,Lua 共享原子操作 较好,Lua 共享原子操作 依赖 MySQL 行更新 依赖 MySQL 行更新
MySQL 余额写入 每次直写 延迟批量写 每次直写 预扣直写,结算/退款批量写
MySQL 压力 高 较低 很高 高
余额持久化及时性 较好 较低 较好 预扣较好,结算/退款较低
Redis 依赖 有 有 无 无
Pod 内存丢失窗口 无批量队列 有 无批量队列 有
主要风险 MySQL 写入和行锁竞争 Redis/MySQL 双写、队列积压、批量丢失 MySQL 成为实时热点和瓶颈 MySQL 仍承受预扣压力,异步增量存在丢失窗口
更适合的流量特征 中等流量,重视落库及时性 大流量,允许短暂落库延迟 Redis 不可用或低流量场景 Redis 不可用但希望降低结算写入压力

5.5 当前方案的共同限制

限制 影响
Redis 与 MySQL 没有统一事务 Redis 已扣但 MySQL 未成功写入时,需要依赖补偿
批量队列位于 Pod 内存 Pod 退出时,尚未写入 MySQL 的增量可能丢失
用户钱包与 API Token 分步更新 两类额度不是一个统一事务,异常时可能短暂不同步
退款异步执行 Pod 退出或任务执行失败时,退款可能延迟
幂等只在进程内 settled、refunded 只保护当前 Pod 内的 BillingSession
used_quota 不是不可变流水 仅靠余额汇总字段无法完成完整的请求级对账
Redis 共享热点 热点用户或热点 API Token 的 Lua 操作会集中到单个 Redis Key

5.6 当前方案结论

结论项 对当前 new-api 实现的判断
默认主路径 Redis 开启时,Redis Lua 负责实时预扣;批量开关决定 MySQL 是直写还是延迟写
大流量模式 RedisEnabled=true 且 BatchUpdateEnabled=true 能降低 MySQL 写入压力,但必须接受批量延迟和 Pod 内存队列风险
及时落库模式 RedisEnabled=true 且 BatchUpdateEnabled=false 更容易观察和对账,但 MySQL 写压力更高
Redis 关闭模式 预扣退回 MySQL 条件更新;批量开关只能影响后续增量落库,不能消除预扣对 MySQL 的依赖
架构定位 当前实现更接近高并发网关额度系统,具备额度控制能力,但不等同于严格账务流水系统
posted @ 2026-09-04 11:50  alin_qu  阅读(60)  评论(0)    收藏  举报