请求频率控制算法

常规算法

基于信号量和周期重置的限速器,它能防止并发量超标,但在高并发时可能导致请求在周期末尾阻塞,然后在周期开始时集中释放。

import asyncio
import time

class RateLimiter:
    """
    用于控制异步调用频率的类
    """
    def __init__(self, max_calls: int, period: float):
        """
        参数:
        - max_calls: 在指定周期内允许的最大调用次数。
        - period: 限制的周期,单位为秒。
        """
        self.max_calls = max_calls
        self.period = period
        self.semaphore = asyncio.Semaphore(self.max_calls)
        self.last_reset = time.time()

    async def __aenter__(self):
        await self.wait_for_slot()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        self.release_slot()

    async def wait_for_slot(self):
        await self.semaphore.acquire()
        
        current_time = time.time()
        elapsed = current_time - self.last_reset
        
        # 如果一个周期已经过去,重置信号量
        if elapsed > self.period:
            # 释放所有已获取的槽位
            while self.semaphore._value < self.max_calls:
                self.semaphore.release()
            self.last_reset = current_time

    def release_slot(self):
        pass

令牌桶算法

令牌桶算法限速器则能提供更平滑、更精准的速率控制

class RateLimiter:
    """
    基于令牌桶算法的异步调用限速器,实现了平滑的请求频率控制。
    """
    def __init__(self, rate: float, capacity: int):
        """
        参数:
        - rate: 每秒生成的令牌数(即调用速率),例如 50次/60秒 -> 50/60次/秒
        - capacity: 令牌桶的容量,即可以瞬间处理的最大请求数。
        """
        self.rate = rate
        self.capacity = capacity
        
        # 当前可用的令牌数量
        self._tokens = capacity
        
        # 上次令牌更新时间
        self._last_update = time.time()
        
        self._lock = asyncio.Lock()

    async def __aenter__(self):
        await self.acquire()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        pass

    async def acquire(self):
        async with self._lock:
            now = time.time()
            
            # 计算自上次更新以来应该生成的新令牌
            elapsed = now - self._last_update
            new_tokens = elapsed * self.rate
            
            # 更新令牌数量,但不能超过容量
            self._tokens = min(self.capacity, self._tokens + new_tokens)
            self._last_update = now
            
            # 如果没有足够的令牌,需要等待
            if self._tokens < 1:
                # 计算需要等待的时间
                wait_time = (1 - self._tokens) / self.rate
                if wait_time > 0:
                    await asyncio.sleep(wait_time)
                    # 等待后更新令牌
                    now_after_wait = time.time()
                    elapsed_after_wait = now_after_wait - self._last_update
                    self._tokens = min(self.capacity, self._tokens + elapsed_after_wait * self.rate)
                    self._last_update = now_after_wait
            
            # 使用一个令牌
            self._tokens -= 1