如何设计稳定的 API 限流与计费系统:用量事实、预扣与对账
限流与计费应共享同一份不可变的用量事实,但作为不同消费者分别处理:限流允许近似并可降级,计费要求可审计、可对账。文章给出用量事件的字段设计、预扣—提交—对账流程,以及重试、时钟、集中计数器故障等边界条件。
稳定的限流与计费,不应是同一个计数器的两种读法,而应是两个消费者共享同一份“用量事实”。限流要低延迟、允许近似、可降级;计费要准确、可审计、可追溯。把两者合成一个实时计数器,通常会在重试和组件故障时同时失真。本文讨论通用设计边界;API.PLUS 的接口限额、计价单位与结算周期未在材料中给出,需以其官方文档为准。
先把“用量事实”定下来#
每次请求结束后写入一条不可变事件,修正通过追加冲正事件完成,而不是改写历史记录。字段至少覆盖:请求 ID、租户或密钥标识、路由与计量维度、服务端开始与结束时间、实际结算用量、是否计入配额的判定结果、幂等键。
下面只是字段结构的示意,未执行、未接入任何真实端点,仅用于说明边界:
{
"request_id": "req_demo_0001",
"tenant_id": "tenant_demo",
"route": "/v1/example",
"meter": "requests",
"started_at": "2026-11-01T10:00:00Z",
"finished_at": "2026-11-01T10:00:00Z",
"billable_units": 1,
"counted_toward_quota": true,
"idempotency_key": "YOUR_KEY"
}
时间必须取自统一的服务端时间基准。客户端时间只能作为参考,不能作为计量依据,否则跨时区与时钟回拨会直接污染账单。
限流决策:可以近似,但不能静默漂移#
常见组合是本地令牌桶做快速判定,周期性向集中存储同步消耗量;或集中计数器加本地批量预取。前者延迟低,但可能短暂超发;后者精度高,但把可用性压在中心组件上。能否接受瞬时超发是业务判断,应由产品与计费侧确认,不应由实现自行决定。
- 固定窗口实现简单,但窗口边界可能出现接近两倍的突发。
- 滑动窗口与令牌桶更平滑,代价是状态更大、实现更复杂。
- 多实例各自独立计数,等于把阈值乘以实例数量;需要明确这是有意为之还是缺陷。
计费结算:预扣、提交、对账#
- 预扣:请求进入时按上限预扣,防止并发把余额打穿。
- 提交:请求结束后按实际用量提交,差额回冲。
- 对账:定时比较用量事件汇总与账单明细,差异进入修正队列。
预扣必须幂等,用幂等键绑定请求 ID,同一请求重试不得重复扣减。结算允许最终一致,但对账窗口和长尾事件的截止时间要提前写进契约,否则迟到事件会变成无法解释的差异。
失败路径要提前定#
集中计数器不可用时,fail-open 保护可用性,但可能超发并少计费;fail-closed 保护配额,但会放大故障范围。两者都不是默认正确答案,宜按路由分级:读接口可放行并标记待补记,写接口或高价值接口倾向拒绝。降级期间产生的事件必须可追溯,否则对账无从谈起。
几个容易漏掉的边界#
- 重试与超时:客户端重试不必然产生新的计费事件,除非服务端确认前一次未生效。
- 流式与长连接:按连接、按字节还是按时长计量,必须在接口契约里写死。
- 赠送额度:有效期、扣减优先级、是否允许透支,都要落到用量事件里。
- 跨天窗口:结算按事件时间还是入账时间,二者不可混用。
可复用的判断标准只有两条:任何一次扣费都能回溯到一条用量事实;任何一次拒绝都能说明命中了哪条规则。做不到这两点,限流与计费就会在故障时互相污染。API.PLUS 的具体限额与计费口径请以官方文档为准,本文未做端点调用或实测。