用户登录

技术文章

外部链接将在新标签页打开。

设计稳定的 API 限流与计费系统:先共用一份用量事实

一句话回答

限流与计费常被分别实现,导致对账不一致。稳定的做法是让两者共用请求标识、计量维度与时间基准:限流做事前决策,计费做事后结算。限流建议本地挡洪峰、中心管配额,并预先写死中心存储故障时的失败开放或关闭策略;计费采用预扣、提交、释放三段式,账本只追加,靠请求 ID 去重。具体阈值与容量须按实际流量确定。

核心要点 TL;DR
  • 限流与计费应共用请求 ID、计量维度与时间基准,二者是同一条数据链的两端。
  • 计费采用预扣—提交—释放,账本只追加,纠错用冲正记录。
  • 中心存储故障时的失败开放或失败关闭策略,须按用途区分并提前写死。

限流和计费常被当成两个模块分别实现,结果是对不上账:限流放过的请求,计费侧因为重试记了两次;或者计费已经扣了额度,限流侧还按旧配额放行。稳定的关键不是算法多精巧,而是两者共用同一份“用量事实”——同一个请求标识、同一套计量维度、同一个时间基准。限流是事前决策,计费是事后结算,二者是同一条数据链的两端。

先定义计量事实,再谈算法#

动手写限流器之前,需要先确定以下内容,否则后续改动会同时影响两个系统:

  • 计量单位:按请求数、token 数、字节数还是时长?单位不同,限流算法要重选。
  • 维度:账号、密钥、端点、模型通常都要,但计费归属只能有一个主维度(一般是账号),其余维度用于细分统计。
  • 时间窗:窗口长度既决定突发容忍度,也决定账单的可解释性。
  • 幂等标识:由入口网关生成请求 ID,向限流、记账、对账三处透传。没有它,超时重发与重试就无法去重。

限流:本地挡洪峰,中心管配额#

常见算法的取舍:

算法特点代价
令牌桶允许一定突发,速率上限清晰桶容量需按业务调
漏桶输出平滑,适合下游脆弱时整形突发请求会被排队或丢弃
固定窗口实现简单窗口边界可能放过双倍流量
滑动窗口统计更准存储与计算成本更高

工程上更可行的组合是:本地令牌桶按实例数分摊额度,挡住瞬时洪峰,避免每个请求都访问中心存储;中心存储只承担跨实例的全局配额。两层阈值不同源,需要显式配置对应关系,否则扩容时全局配额会被悄悄放大。

中心存储不可用时的行为必须提前写死:防滥用类限流可以失败开放并触发告警;付费配额类应失败关闭,或降级到本地保守值。返回 429 时一并给出 Retry-After 与剩余配额,客户端才好做退避。

计费:预扣、提交、释放#

请求进入时按最坏用量预扣,处理完成后按实际用量提交差额,失败则释放。这样上限可控,不会因为事后才发现超额而透支。用量事件写入追加型账本,聚合异步进行:账本只追加不修改,纠错用冲正记录,重复投递靠请求 ID 去重。

# 示意,未执行,仅表达调用顺序
hold = quota.reserve(account_id, request_id, max_cost)
if not hold.ok:
    return 429
try:
    cost = handle(request)
    quota.commit(request_id, cost)   # 多退少补
except Exception:
    quota.release(request_id)        # 释放预扣

共用的时间与一致性边界#

速率计算用单调时钟,窗口边界按 UTC 对齐,避免实例间时钟漂移放大误差。跨实例计数器通常是最终一致,会带来短时超发,需要评估可接受量。账单与限流报表应能追溯到同一条原始事件,否则争议发生时无从复核。

本文的边界#

这里不给具体 QPS、超时、存储容量或成本数字,它们取决于真实流量形态,须压测后确定。上面的代码是示意片段,未在真实环境执行。具体中间件选型与最新版本信息,本文未核实。

常见问题(FAQ)

限流阈值和计费套餐要用同一套数字吗?
不必直接绑定。限流阈值服务于稳定性,套餐服务于商业承诺,两者可以同源但不相等,应显式配置映射关系,并在任一侧变更时同步评估另一侧。
最终一致的计数器能直接用于计费吗?
不建议。计数器适合限流决策,计费应以追加型用量事件为准;聚合延迟不影响最终金额,但聚合结果不应反过来作为扣费依据。

作者:沐小悠

本文约 4 分钟读完 · 关键词:API 限流、计费系统设计、令牌桶、配额预扣、幂等

更多文章 GET 示例接口

「」标签下的其他文章