用户登录

技术文章

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

稳定 API 限流与计费系统设计:计量、账本与降级

一句话回答

稳定 API 限流与计费必须共享同一套数据:先统一权重与用量事件口径,再用 Redis 令牌桶分级限流,并以预扣—结算—对账账本和依赖降级兜底,才能做到不超卖、不丢事件,又能用每日对账证明账目一致。

核心要点 TL;DR
  • 先统一计量口径(权重、用量事件),再选限流算法
  • 令牌桶用 Redis+Lua 原子扣减,429 返回 Retry-After
  • 计费采用预扣—结算—退款三段式,request_id 幂等防重复扣费
  • 日切后做三方对账,差异超阈值即告警并冻结自动结算
  • 依赖故障须降级:Redis 挂切进程内桶,队列失败先写本地磁盘再补偿

引言:当调用方从人变成 Agent#

2026 年,API 的调用方越来越多是 Agent,而不是人。一个 Agent 为了完成「查 IP 归属地、再配一张图」这样的任务,可能在几秒内循环几十次请求;新出现的 AI 原生语言(例如强调 Rust 性能、Python 语法的 Vu)也在降低生成高并发客户端的门槛。结果是两个变化同时发生:流量更突发,成本更难归属。限流回答「这次请求能不能进」,计费回答「进来之后花了多少」,两者共享同一份计量事实,必须一起设计。本文给出一套可落地的结构:统一计量模型、Redis 令牌桶、预扣—结算—对账账本,以及依赖故障时的降级策略。

一、先定计量模型,再选限流算法#

很多系统先写限流,再补计费,最后发现两边的口径对不上:限流按 QPS(Queries Per Second,每秒请求数) 算,计费按调用次数算,图片接口和 JSON 接口成本不同却同价。正确的顺序是先定义「一次调用消耗多少资源」,再决定用什么算法去限制它。

  • 计量单位:请求次数、返回条数、Token 数、计算时长、出流量。聚合型 API 建议用「权重」统一表达,例如普通 JSON 查询权重为 1,返回高清图片的接口权重为 3。
  • 用量事件:每次调用生成一条不可变事件,字段至少包含 request_id、api_key_id、endpoint、units、unit_price、status、created_at。request_id 同时作为幂等键,贯穿限流、计费与排查。
  • 快慢分离:限流走 Redis 计数,要求毫秒级;计费走消息队列落库,允许秒级延迟,但不允许丢事件。可以先把事件写入本地 outbox 再返回响应,由后台任务搬运。
维度限流侧计费侧
一致性要求近似即可,允许少量误差最终一致,事件不能丢
主要存储Redis 计数器 / 令牌桶MySQL 账本 + 消息队列
失败策略按接口分级 fail-open 或 fail-closed重试、死信、本地落盘补偿
时间粒度秒级账单周期 + 日切对账

二、限流:令牌桶 + 分级配额 + 原子扣减#

算法选择上,固定窗口在边界会产生双倍突刺;滑动日志精度高,但内存随 QPS(Queries Per Second,每秒请求数) 增长;令牌桶允许有限突发,最适合开放 API。工程上通常分三层:全站保护防雪崩、租户或套餐配额限制日/月总量、Key 级 QPS 防单点刷量,再加一层 IP 兜底,防止同一来源批量注册。

扣减必须原子。下面是用 Redis + Lua 实现的令牌桶,时间取 Redis 服务端 TIME,避免多实例时钟漂移:

示例说明:以下代码仅用于逻辑验证,生产环境需补充异常处理、超时与重试、日志与监控告警及降级策略。

-- KEYS[1]=bucket key  ARGV[1]=rate(个/秒) ARGV[2]=burst ARGV[3]=now_ms
local rate  = tonumber(ARGV[1])
local burst = tonumber(ARGV[2])
local t = redis.call('TIME')
local now = tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)
local d = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(d[1]) or burst
local ts = tonumber(d[2]) or now
tokens = math.min(burst, tokens + math.max(0, now - ts) / 1000 * rate)
local allowed = 0
if tokens >= 1 then
  tokens = tokens - 1
  allowed = 1
end
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'ts', now)
redis.call('PEXPIRE', KEYS[1], math.ceil(burst / rate * 1000) + 1000)
return {allowed, math.floor(tokens), math.ceil((1 - tokens) / rate * 1000)}

几个容易忽略的细节:

  • key 带时间片(如 rl:{key_id}:{分钟})可以打散热点,但令牌桶本身是单 key,上线前要评估分片与容量。
  • 拒绝时返回 429,并带上 Retry-After 与 X-RateLimit-Remaining,Agent 客户端才能正确退避,而不是立即重试放大流量。
  • 限流组件不可用时要明确策略:查询类接口 fail-open 并记录降级日志,写类或高成本接口 fail-closed。策略写进配置,不要散落在业务代码里。
  • 慢接口加并发闸门(信号量),防止连接被拖满,这往往比单纯限制 QPS(Queries Per Second,每秒请求数) 更有效。

三、计费账本:预扣、结算与对账#

「先查余额,再扣减」在并发下必然超卖。稳妥做法是三段式:reserve 预扣本次调用的最大可能费用,commit 按实际用量结算,refund 释放差额。request_id 加唯一索引,保证重复请求不会重复扣费。

CREATE TABLE usage_event (
  request_id  CHAR(26) PRIMARY KEY,
  api_key_id  BIGINT NOT NULL,
  endpoint    VARCHAR(64) NOT NULL,
  units       INT NOT NULL,
  unit_price  DECIMAL(12,6) NOT NULL,
  status      SMALLINT NOT NULL,
  created_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  KEY idx_key_time (api_key_id, created_at)
);
CREATE TABLE balance_ledger (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  api_key_id BIGINT NOT NULL,
  request_id CHAR(26) NOT NULL,
  delta DECIMAL(14,6) NOT NULL,
  reason VARCHAR(32) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uk_request_reason (request_id, reason)
);

账本要双录:用户余额变动与平台收入流水在同一事务里写,或者通过 outbox 保证最终一致。流式响应、图片下载这类「开始计费但可能中途断开」的场景,按已产生用量结算,未使用部分退回,避免用户为失败请求付费。

对账是稳定性的最后一道闸。每天日切后做三方比对:usage_event 汇总、balance_ledger 汇总、上游供应商账单。差异超过阈值(比如 0.5%)就告警并冻结自动结算,转人工排查。对账脚本要可重跑,按账期幂等,否则补数会引入二次错误。

四、降级、可观测与灰度#

限流和计费系统本身也会故障,所以要按「依赖一定会挂」来设计:Redis 不可用时切进程内令牌桶,精度下降但服务可用;队列写入失败落本地磁盘并后台补偿;计费服务超时不能阻塞业务响应,改为异步补记。所有降级都要有开关和日志,事后能还原当时发生了什么。

  • 核心指标:拒绝率、429 占比、鉴权 P99(99 分位延迟) 延迟、队列积压、负余额请求数、对账差异金额。
  • 影子模式:新算法先只记录不拦截,跑一周对比放行率与误杀率,再灰度切流。
  • 安全审计:Key 轮换、按 Key 记录调用画像,识别同一 IP 多 Key、同一 Key 多地域等异常模式。

五、用真实接口做一次端到端演练#

把上面的结构接到真实接口上验证最直接。先注册拿到 Key,用 IP 查询接口压测 Key 级 QPS(Queries Per Second,每秒请求数):

curl -i 'https://www.api.plus/api/get?apikey=YOUR_KEY&ip=8.8.8.8'

观察响应头中的限流字段,确认 429 时 Retry-After 是否合理。再用随机动漫壁纸接口验证「按响应体积或权重计费」:

curl -i 'https://www.api.plus/api/dongman?type=json'

然后用 POST 示例接口 验证幂等:同一个 request_id 连发两次,账本只应出现一条记录。接口参数见 IP 查询接口文档,更多实现细节可以翻 API 文章中心,遇到接入问题走 技术支持。建议压测脚本固定并发与时长,记录每秒放行数、拒绝数与账本增量,三者应当自洽。

六、总结#

稳定的限流与计费系统可以浓缩成三条原则:计量先行,先统一用量口径再谈算法;预扣后结算,用 request_id 幂等和账本双录消除超卖;可降级可对账,依赖故障时有兜底,每天能用数字证明账是平的。算法不是难点,难点在于让限流与计费共享同一份事实,并在故障时仍然可解释。

如果你正在做自己的 API 服务,可以先用小流量把这条链路跑通。前往 API.PLUS 注册即可获取免费 API Key,用 IP 查询、动漫壁纸等接口做限流与计费联调,成本低、反馈快。

常见问题(FAQ)

为什么限流和计费系统要一起设计?
两者共享同一套计量数据:限流决定请求能否进入,计费算出进入后消耗了多少。若先做限流再补计费,常出现 QPS 与调用次数口径不一致、不同成本接口同价,最终账对不上。
并发场景下如何避免余额超卖或重复扣费?
不要先查余额再扣减。改用 reserve 预扣本次最大费用、commit 按实际用量结算、refund 释放差额,并给 request_id 建唯一索引,同一请求重复提交也只扣一次。
开放 API 的限流为什么选令牌桶?
固定窗口在边界会产生双倍突刺,滑动日志内存随 QPS 增长;令牌桶允许有限突发,最适合开放 API,并可叠加全站、租户、Key、IP 多层配额分级保护。
Redis 或计费服务故障时怎么处理?
按依赖一定会挂来设计:Redis 不可用切进程内令牌桶,精度下降但服务可用;队列写入失败先写本地磁盘,再由后台补偿;计费超时改为异步补记,不阻塞业务响应,同时保留降级开关和日志。
如何验证计费账目是否一致?
每天日切后做三方比对:usage_event 汇总、balance_ledger 汇总、上游供应商账单;差异超过阈值(如 0.5%)就告警并冻结自动结算转人工排查。对账脚本要按账期幂等、可重跑。

作者:沐小悠

本文约 11 分钟读完 · 关键词:API 限流、API 计费、令牌桶、预扣结算、日切对账

更多文章 GET 示例接口

「」标签下的其他文章