await 无法直接实现令牌桶硬性速率管控,需将“取令牌”改造为可 await 的异步操作,通过动态补发令牌、asyncio.lock 保护状态、支持超时与异常反馈,实现精准 qps 限流。

直接用 await 无法实现令牌桶的硬性速率管控,必须配合协程等待逻辑与令牌状态检查。核心在于:把“取令牌”变成一个可 await 的异步操作,让无令牌的协程挂起,直到新令牌生成或超时。
令牌桶需支持异步等待
同步版令牌桶(如 Go 示例)调用 Allow() 是阻塞判断,不适合 asyncio。必须改造为:
– 维护一个内部令牌计数器和上次填充时间
– 提供 acquire() 方法,返回 Awaitable[bool]
– 若无令牌,自动计算等待时长并 await asyncio.sleep(),而非忙等或立即拒绝
关键实现要点
- 使用
asyncio.Lock保护桶状态读写,避免并发修改导致令牌透支 - 填充逻辑不依赖定时器,而是在每次
acquire()时按时间差动态补发,更精准、无资源泄漏 - 支持可选超时(
timeout: float = None),防止协程无限等待,便于熔断或降级 - 拒绝路径应返回明确信号(如抛出
RateLimitExceeded异常),而非静默失败
典型调用方式
不是在任务里简单写 await token_bucket.acquire() 就完事,而是要嵌入到实际业务协程中:
async def api_handler(request):
try:
await rate_limiter.acquire() # 等待令牌,可能挂起
return await do_actual_work(request)
except RateLimitExceeded:
return web.json_response({"error": "429 Too Many Requests"}, status=429)
这样,即使瞬间涌入 1000 个请求,真正执行 do_actual_work 的速率也严格受限于令牌填充速度(例如每秒 10 个),其余协程在 acquire() 处排队或超时退出。
与 BoundedSemaphore 的本质区别
– BoundedSemaphore(10) 控制的是**最大并发数**(同时最多 10 个任务在跑),不限制单位时间请求数;
– 令牌桶控制的是**长期平均速率**(如 10 QPS),允许短时突发(桶容量决定弹性),更适合 API 限速场景。
两者可叠加使用:先用令牌桶控速率,再用 Semaphore 控资源占用(如连接数),形成双层防护。











