用户登录

技术文章

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

如何设计一个稳定的 API 限流与计费系统:把计量、限额与结算拆开

一句话回答

限流的稳定性和计费的准确性是两件事,应使用不同的数据结构与失败策略。文章建议把计量、限额、计费单位三个概念分开,计数放到共享存储并接受近似误差,计费走预扣—结算—对账三段,同时用幂等键、悬挂预扣清理和显式的降级策略处理失败模式。

先说结论:一个限流与计费系统稳定,多半不是因为选对了某个中间件,而是因为做对了三个决定,即计量与限额分开、计数放在共享存储并允许近似、计费走“预扣—结算—对账”三段而不是实时扣款。下面按这三个决定展开;涉及具体厂商行为的部分本次没有可核验材料,明确标注待核实。

先把三个概念拆开#

  • 限额(rate limit):单位时间允许多少次请求,保护后端容量。
  • 配额(quota):一个周期内的总量上限,多为商业约束,如每月调用次数。
  • 计费单位(billing unit):按什么计量——请求数、输入输出 token、字节数或计算时长。

三者常被塞进同一个计数器,后果是限流触发的拒绝和账单上的扣费无法互相解释。计费单位一旦对外公布,改动成本很高,因此要在设计初期固定下来。如果计费单位是 token 类用量,需要先确认上游返回的用量字段是否权威、是否会因重试而变化;这一点取决于具体服务,本次材料未提供,待核实。

计数放在共享存储,并接受近似#

单机内存计数在水平扩容后立刻失效:同一调用方落到不同实例,各自从零开始计数,实际放行量等于实例数的倍数。常见做法是把计数放到共享存储(如 Redis),代价是多一次网络往返,还要面对存储不可用时的降级决策。

算法内存开销突发流量边界误差适合
固定窗口计数低允许,可能双倍窗口交界处偏宽粗略保护
滑动窗口日志高(存每次请求时间)精确无低 QPS、要求精确
滑动窗口计数(加权近似)低较平滑有,可接受通用场景
令牌桶低可配置突发上限无(对桶而言)对外 API 限额

关键判断是:限流允许误差。它的目标是保护后端,窗口交界处多放行几个请求通常可以接受;计费不允许误差,因为那是钱。所以两者不应该共用同一个计数器——限流可以近似,账必须精确。

令牌桶的最小形态(示例未执行)#

下面的代码是设计示意,未实际运行;假设环境为 Python 3、使用支持原子脚本的共享存储。真实实现需要按所选存储的原子操作改写,并补齐并发与失败处理。

# 未执行的示例:仅示意字段与判断顺序
def try_acquire(key, rate, burst, now):
    # 1. 读取 tokens 与 last_refill(须为原子读改写)
    tokens, last = store.get(key)
    # 2. 按时间差补充令牌,上限为 burst
    tokens = min(burst, tokens + (now - last) * rate)
    # 3. 判断后写回,读改写必须是一个原子操作
    if tokens >= 1:
        store.set(key, (tokens - 1, now))
        return True
    store.set(key, (tokens, now))
    return False

注意两个容易写错的点:补充与扣减必须是同一个原子操作,否则并发下会超发;now 应由服务端统一时钟产生,不要信任客户端时间。

计费走三段:预扣、结算、对账#

  1. 预扣:请求进入时按上限预留额度,防止并发请求同时通过额度检查后一起超额。
  2. 结算:请求成功返回后,按实际用量提交,多退少补。
  3. 对账:定时任务扫描超时未结算的预扣记录,释放或转为异常单,避免额度被永久占住。

“悬挂预扣”是这类系统最常见的账目偏差来源:进程在预扣之后、结算之前崩溃,额度就被锁死了。对账任务不是可选项,而是主流程的一部分。

幂等同样重要。客户端重试、网关重放都会让同一个请求被计两次费。解决办法是要求请求携带幂等键(idempotency key),或由服务端从鉴权主体加请求特征派生;没有幂等键的重试,在计费语义上无法区分“新请求”和“重发”。

需要提前写清的失败模式#

  • 共享存储不可用:放行(fail-open)还是拒绝(fail-close)取决于接口性质。读接口放行的风险是短时过载,写或计费接口拒绝的风险是可用性下降。这个选择要写进文档,而不是留给值班时临场决定。
  • 时钟漂移:多实例时间不一致会让窗口计算偏移,应统一时间源并留出容差。
  • 并发竞态:任何“先读后写”的计数在没有原子性保证时都会超发。
  • 限额头部缺失:不返回剩余额度与重置时间,调用方只能靠试错撞限,重试会更密集。

对外契约上至少暴露什么#

返回剩余额度、重置时间,被拒绝时用 429 并给出 Retry-After;限额维度(按密钥、按账号还是按 IP)要在文档里明确。至于具体的头部名称与字段格式,属于接口设计选择,本文不假定某个平台已采用哪种。

最后一点判断标准:如果一个系统能回答“这个请求为什么被拒绝”和“这笔费用由哪些请求构成”,它就算稳定;回答不了,说明计量与限额还混在一起。

作者:沐小悠

本文约 5 分钟读完 · 关键词:API 限流、令牌桶、滑动窗口、计费系统、预扣与对账

更多文章 GET 示例接口

「」标签下的其他文章