示例说明
固定窗口限流在边界处会双倍放行,令牌桶没有这个问题:容量内的瞬时突发直接放行,之后按每秒回填速率恢复配额。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);