如何设计一个稳定的 API 限流与计费系统:先统一配额口径,再拆分拦截与记账
稳定性的关键不是选哪个限流算法,而是先统一“一次调用算几次”的口径,再把拦截链路与记账链路拆开:限流侧明确键、共享存储与存储故障时的失败策略;计费侧用幂等键去重、异步记账并保留原始事件以便对账;两者通过“预扣—执行—结算”衔接,并用拒绝率、记账延迟与对账差异做验收。
- 先定配额口径(单位、时间窗、作用域、超限行为、重置规则),否则限流与计费必然对不上。
- 限流的难点在分布式计数与存储故障时的失败策略,而不是算法本身。
- 计费用幂等键去重、保留原始事件、定期对账,才能承受重试与超时。
- 限额检查与额度扣减应在一个原子流程内完成,避免读改写分离造成超卖。
一个稳定的 API 限流与计费系统,首先要能回答一个很窄的问题:“一次调用算几次、算在谁头上、什么时候算。”限流解决“让不让过”,计费解决“记多少账”,两者只要口径不一致,系统再快也会在账单上出问题。本文没有可用的产品正文与可核验来源,也不声称做过联网核验;以下内容属于设计建议与边界说明,不涉及具体平台的参数、价格或配额。
第一步:把配额口径写成文字#
在选算法之前,先固定五项:计量单位(请求数、令牌数、并发数还是计算量)、时间窗(固定窗口、滑动窗口或滚动周期)、作用域(账号、密钥、IP、端点)、超限行为(拒绝、排队还是降级)、重置规则(自然周期还是滚动周期)。这五项一旦含糊,限流侧和计费侧就会各自实现一套判断,对账时无法解释差异。
限流链路:键、存储与失败策略#
限流键要能定位责任主体
按 IP 限流实现简单,但 NAT 与代理后面的大量用户会共享同一个键,容易误伤;按密钥或账号限流更贴近计费主体,但需要先完成鉴权。常见做法是分层:边缘做粗粒度保护,业务侧做精确配额。
分布式计数只能用共享存储保证一致
进程内计数只适合单实例近似值,多实例部署下必须依赖共享存储或集中式配额服务。令牌桶(token bucket)允许突发,漏桶(leaky bucket)输出更平滑,滑动窗口更精确但存储和计算成本更高。选型取决于目标是保护后端还是约束预算,而不是哪个算法更流行。
存储不可用时的行为要提前配置
配额存储故障时只有两种选择:fail-open(放行,保可用性,可能超支)或 fail-closed(拒绝,保成本,可能误伤)。这是产品决策,应写进配置与文档,而不是故障发生时临时判断。下面是一段预扣与结算的骨架,未执行,仅用于说明流程,键名与存储接口均为占位:
// 未执行的示例,演示顺序而非可运行实现
async function callWithQuota(req) {
const key = `quota:${req.accountId}:${windowId(now())}`;
// 1) 原子预扣:检查与扣减在同一次操作内完成
const reserved = await store.reserve(key, cost(req), limit(req));
if (!reserved) return reject(429, "quota_exceeded");
try {
const res = await upstream(req);
// 2) 成功则结算为实际用量,失败则回滚
await billing.commit(req.requestId, actualUsage(res));
return res;
} catch (e) {
await store.rollback(key, cost(req));
await billing.recordFailure(req.requestId, e.code);
throw e;
}
}
计费链路:幂等、异步与对账#
计费的第一个敌人是重试。客户端超时重发、网关自动重放,都会让同一笔用量被记多次。解决办法是给每次调用分配幂等键(如请求 ID),记账端按该键去重,并保留原始用量事件而不是只保留汇总数——只有原始事件才能支撑重算与差异解释。记账通常异步化以降低调用延迟,代价是出现“已放行未入账”的中间态,因此需要定期对账:比对用量事件总量、已结算总量与配额扣减总量三者是否收敛。
两条链路如何衔接#
推荐“预扣—执行—结算”的顺序:先按上限预扣,成功后按实际用量结算,失败则回滚。关键是限额检查与额度扣减必须在同一个原子操作里完成,读与改写分离会直接导致并发超卖。另一个容易被忽略的点是时间:窗口边界、多节点时钟漂移、滚动窗口的状态迁移,都会让同一请求落在不同窗口里,需要在设计时明确以哪个时钟为准。
验收顺序#
- 单实例下计数与结算是否正确,边界值(等于上限、跨窗口)是否覆盖。
- 多实例并发下是否出现超卖,幂等键是否真的阻止重复扣费。
- 故障注入:配额存储不可用、记账队列积压、上游超时,各自的降级行为是否符合预期。
- 对账闭环:人为制造差异,能否定位到具体请求。
至于具体平台(包括 API.PLUS)的配额单位、重置周期与计费口径,应以官方文档为准,本文不代为确认,这部分待核实。