← 中文资源

简体中文版本

异步令牌桶限流器

实现异步令牌桶:桶容量允许突发、按速率连续回填、按调用方分键隔离,take 语义让排队式调用方礼貌等待而非失败。

阅读英文原文

示例说明

固定窗口限流在边界处会双倍放行,令牌桶没有这个问题:容量内的瞬时突发直接放行,之后按每秒回填速率恢复配额。take 的等待语义适合批处理与队列消费——限流不再是失败而是排队,调用方写 await 就行。按调用方分桶让重客户无法挤占其他人的配额。

技术要点

  • 容量与回填速率解耦突发与均值
  • take 循环等待实现排队语义
  • Map 分桶实现调用方级隔离

代码解读

refill 按时间差折算应补令牌并封顶容量,惰性计算无需后台定时器;tryTake 立即判定,take 在不足时按缺口与速率折算等待时间后重试,最小间隔防忙转。示例先 take 三个令牌再尝试立即取八个,演示突发耗尽后的拒绝路径。

使用建议

单机令牌桶仅适合单实例或分片键控场景,全局口径要外置到 Redis。等待上限要有兜底防止无限排队。桶对象长期增长需配 LRU 淘汰。

代码示例

// async token bucket rate limiter with jitter
class TokenBucket {
  private tokens: number;
  private lastRefill: number;

  constructor(
    private readonly capacity: number,
    private readonly refillPerSecond: number,
  ) {
    this.tokens = capacity;
    this.lastRefill = performance.now();
  }

  private refill(now: number) {
    const elapsed = (now - this.lastRefill) / 1000;
    this.tokens = Math.min(
      this.capacity,
      this.tokens + elapsed * this.refillPerSecond,
    );
    this.lastRefill = now;
  }

  tryTake(count = 1): boolean {
    this.refill(performance.now());
    if (this.tokens < count) return false;
    this.tokens -= count;
    return true;
  }

  async take(count = 1): Promise<void> {
    while (!this.tryTake(count)) {
      const deficit = count - this.tokens;
      const waitMs = (deficit / this.refillPerSecond) * 1000;
      await new Promise((r) => setTimeout(r, Math.max(waitMs, 10)));
    }
  }
}

const buckets = new Map<string, TokenBucket>();

function bucketFor(key: string) {
  let b = buckets.get(key);
  if (!b) {
    b = new TokenBucket(10, 5);
    buckets.set(key, b);
  }
  return b;
}

await bucketFor('api:upload').take(3);
console.log('upload quota acquired, burst-friendly');
const immediate = bucketFor('api:upload').tryTake(8);
console.log('immediate take of 8:', immediate);