2. 行情API速率限制的本质与常见策略

行情API的速率限制,本质上是服务端在单位时间内对请求“配额”的约束。它并不关心你请求的是BTC/USDT的深度还是最新成交价,只关心在某个时间窗口内,来自同一API Key、同一IP或同一账户的请求次数是否超过了约定阈值。理解这一点很关键:限流是资源保护手段,而不是业务规则。因此,当我们讨论固定窗口、滑动窗口、令牌桶、漏桶时,实际上是在讨论“配额如何被计量、何时被补充、超限后如何表现”这四个问题的不同答案。

在行情场景中,限流通常以三种维度出现:按请求次数(如每分钟1200次)、按权重(不同接口消耗不同配额,深度接口权重高于ticker)、按并发连接数(WebSocket订阅数上限)。多数交易所会同时叠加多种维度,例如“REST每分钟1200权重 + WebSocket每连接最多200个订阅”。这意味着客户端不能只盯着一个计数器,而要对多个配额同时做预算管理。

四种经典策略的计量逻辑

固定窗口把时间切成等长区间,例如每60秒一个窗口,窗口开始时计数器归零。实现最简单,但存在“边界突刺”问题:假设限额为100次/分钟,客户端在第59秒用掉100次,第61秒又用掉100次,则在跨越窗口边界的约2秒内实际发出了200次请求,服务端若按窗口统计不会拦截,但真实压力已经翻倍。行情API中,固定窗口常见于较简单的公共接口或文档中标注“per minute”的场景。

滑动窗口不再依赖对齐的时间块,而是始终统计“最近60秒”内的请求数。实现上通常用时间戳队列或分桶计数(如把60秒切成6个10秒小桶,滚动求和)。它消除了边界突刺,代价是需要保存请求时间戳或维护多个小桶,内存与计算开销略高。对于行情API这种请求密集、对突发敏感的场景,滑动窗口更贴近真实负载。

令牌桶把配额想象成一个桶,桶以恒定速率r放入令牌,桶容量为b。每个请求取走一个令牌,取不到则拒绝或排队。它的关键特性是允许“突发”:只要桶里有积累的令牌,客户端可以在短时间内以高于r的速率发送请求,上限为b。行情API中,令牌桶非常适合“平时低频、行情剧烈时短时高频拉取”的客户端,例如平时每秒2次、非农数据公布瞬间短时冲到每秒20次。

漏桶则相反,请求先进入桶中,桶以恒定速率流出处理,桶满则拒绝。它强制输出速率平滑,不允许突发。对于服务端保护下游行情推送系统、避免瞬时洪峰冲击,漏桶更合适;但对客户端而言,漏桶意味着即使你有配额,也无法在关键时刻“攒一波”使用。

四种限流策略的配额计量与突发行为对比 固定窗口:按对齐时间块计数 窗口1 窗口2 窗口3 边界突刺:两窗口交界处短时翻倍 实现简单,但边界处可能放过双倍流量 典型:文档标注 per minute 的公共接口 滑动窗口:始终统计最近60秒 最近60秒滚动区间 窗口随当前时刻滚动,无边界突刺 需保存时间戳或分桶滚动求和 典型:对突发敏感的行情REST接口 令牌桶:恒定补充 + 允许突发 桶容量 b 已存令牌 速率 r 补充令牌 允许短时突发,上限为桶容量 b 典型:平时低频、行情剧烈时短时高频 漏桶:恒定流出 + 强制平滑 请求队列 恒定流出速率 输出速率平滑,桶满即拒绝,不允许突发 典型:服务端保护下游推送系统

在行情API中如何选择与落地

选择策略前,先要弄清服务端实际采用的是哪一种。很多交易所文档只写“1200 requests per minute”,但真实实现可能是滑动窗口或令牌桶。一个实用的判断方法是观察超限响应:如果返回429 Too Many Requests并附带Retry-After,说明服务端希望你等待固定时长;如果返回的限流头包含X-RateLimit-Remaining且数值在窗口内单调递减、到点跳回,则更接近固定窗口;若剩余量在窗口内缓慢恢复,则可能是令牌桶。客户端应优先读取这些响应头,而不是自己猜。

以某交易所REST行情接口为例,假设限额为每分钟1200权重,深度接口权重为5,ticker权重为1。若客户端用固定窗口思维,在每分钟最后10秒集中拉取240次深度(消耗1200权重),下一分钟开始又立即拉取,就会在边界处形成2400权重的瞬时压力。改用滑动窗口预算后,客户端维护一个最近60秒的权重时间戳队列,每次发请求前先剔除过期记录,再判断已用权重 + 本次权重 ≤ 1200。伪代码如下:

窗口 = 60 秒
队列 = []  # 存 (时间戳, 权重)

def 可以发送(权重):
    现在 = 当前时间()
    while 队列 非空 and 现在 - 队列[0].时间戳 > 窗口:
        队列.pop(0)
    已用 = sum(项.权重 for 项 in 队列)
    return 已用 + 权重 <= 1200

def 发送请求(权重):
    if not 可以发送(权重):
        等待到 队列[0].时间戳 + 窗口
    发出请求()
    队列.append((当前时间(), 权重))

这段逻辑的关键在于“发送前预算、超限时等待最早记录过期”,而不是简单睡眠固定秒数。它把限流判断从“事后补救”变成“事前准入”,能显著降低429出现频率。

重试与限流的配合关系

限流策略决定了“什么时候不该发”,重试机制决定了“被拒绝后怎么办”。两者必须协同:如果服务端是漏桶且桶已满,立即重试只会继续被拒,正确做法是读取Retry-After并退避;如果服务端是令牌桶,短暂等待后令牌会恢复,指数退避加随机抖动(jitter)能避免多个客户端同时重试造成二次洪峰。一个常见的错误是:客户端在收到429后固定等待1秒重试,结果所有实例在同一时刻再次撞上限流。加入随机抖动,例如等待base * 2^n + random(0, 0.5 * base),可以把重试打散。

判断限流策略的实用顺序:先看文档声明的窗口单位,再看响应头中的剩余配额变化规律,最后用低频探测请求观察恢复曲线。三者结合,比单纯猜测算法更可靠。

还需要注意,行情API的限流往往与WebSocket订阅数、连接数、消息频率共同作用。REST侧的令牌桶预算再充足,如果WebSocket订阅超过上限,仍然会被断开。因此客户端应把“REST权重预算”和“WS订阅预算”分开建模,各自维护计数器,而不是用一个全局计数器笼统处理。只有把每种配额当作独立资源来管理,限流策略才真正落地为可执行的工程约束。