asyncio.Semaphore用于控制同时发出的请求数量,而非单纯每秒请求数;需在每个session.request()前用async with semaphore:包裹,配合aiohttp.TCPConnector和ClientTimeout配置才能安全限流。

用 aiohttp + asyncio.Semaphore 控制并发请求数
速率限制不等于单纯“每秒发N个请求”,更常见且可靠的做法是控制**同时发出的请求数量**,避免压垮服务端或触发限流。直接用 asyncio.Semaphore 包裹 aiohttp.ClientSession.request() 最轻量、无额外依赖。
常见错误是把 Semaphore 放在循环外但没 await,导致所有协程抢同一个信号量而阻塞;或者误以为设成 1 就等于“串行”,其实仍需确保每个 session.request() 都被 semaphore.acquire() 包裹。
- 初始化时传入期望的最大并发数,比如
semaphore = asyncio.Semaphore(5) - 每个请求前用
async with semaphore:获取许可,离开作用域自动释放 - 不要在
ClientSession外层加semaphore,那只会限制 session 创建,不是请求
import aiohttp import asyncio <p>semaphore = asyncio.Semaphore(3)</p><p>async def fetch(url): async with semaphore: # ← 关键:每个请求单独 acquire async with aiohttp.ClientSession() as session: async with session.get(url) as resp: return await resp.text() </p>
需要精确“每秒N次”时,用 asyncio.sleep() 补齐间隔
如果服务端明确要求 “X requests per second”,仅靠 Semaphore 不够——因为请求耗时不同,可能 1 秒内密集发出,下一秒全空。必须记录上一次请求时间,并在每次请求前 sleep 补齐最小间隔。
注意别用 time.sleep(),它会阻塞整个事件循环;也别在 sleep 前忘记 await,否则协程不会暂停。
- 维护一个全局
last_request_time = 0.0 - 计算当前时间与上次时间差,若不足间隔(如 1/10 秒),就
await asyncio.sleep(needed_delay) - 更新
last_request_time必须在 sleep 后、请求前,否则第一次请求就可能超速
import asyncio
import time
<p>rate_limit = 10 # 10 req/sec → 每次至少间隔 0.1 秒
min_interval = 1.0 / rate_limit
last_request_time = 0.0</p><p>async def fetch_with_rate(url):
global last_request_time
now = time.time()
elapsed = now - last_request_time
if elapsed </p><pre class="brush:php;toolbar:false;">async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
return await resp.text()组合使用 Semaphore 和时间窗口,应对真实生产场景
纯并发限制扛不住突发流量,纯时间间隔又浪费连接池资源。线上常用组合:先用 Semaphore 控制最大并发(比如 10),再在每个请求前做滑动窗口计数(比如最近 1 秒内不超过 100 次)。
简单实现可用 collections.deque 记录时间戳,每次清理超时项并检查长度。但要注意 deque 的 append() 是线程安全的,但在协程中无需锁——只要不跨 Task 共享同一 deque 实例即可。
- 每个请求开始时 append 当前时间戳
- pop 左侧早于
now - window_size的时间戳 - 若 len(deque) ≥ max_requests,就
await asyncio.sleep(...)等待 - 避免在高频请求下频繁遍历 deque,可定期清理而非每次清理
别忽略 aiohttp 连接池和超时配置的影响
即使加了限流,如果 aiohttp.TCPConnector 的 limit 或 limit_per_host 设得太大,仍可能瞬间建立大量 TCP 连接,被防火墙重置或触发系统级限制。
同样,没设 timeout 会导致单个卡住的请求拖慢整个限流队列——因为 Semaphore 许可不会被释放,后续请求全在等它。
- 显式配置
connector = aiohttp.TCPConnector(limit=20, limit_per_host=5) - 给每个请求加
timeout=aiohttp.ClientTimeout(total=10) - 避免复用全局
ClientSession时未设置 connector,否则用默认值(通常 limit=100)
真正难的不是写限流逻辑,而是判断该用哪种策略:并发数?QPS?还是令牌桶?以及怎么让它们和连接池、重试、熔断协同工作。多数情况下,从 Semaphore + 合理 connector 配置起步,比一上来就套复杂算法更稳。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











