限流里最常见的一类问题是:系统希望把长期吞吐压住,但又不想把所有短时突发都打掉。
Token Bucket 就是为这个场景设计的。
它的模型很简单。
系统里有一个桶。桶里放 token。token 按固定速率生成,比如每秒 10 个。桶有最大容量,比如 100 个。满了之后,新生成的 token 会被丢弃。
每个请求进来时,需要消耗 token。通常是 1 个。
如果桶里还有 token,请求通过。
如果 token 不够,请求就不能立刻通过。后面怎么处理,要看系统策略:直接拒绝、排队、让客户端稍后重试,或者走降级逻辑。
一个具体例子
假设配置是这样:
refill rate: 10 tokens / second
capacity: 100 tokens
request cost: 1 token这表示两件事。
长期看,系统平均最多每秒处理 10 个请求。
但如果系统之前比较空闲,桶里已经积累了 100 个 token,那么下一瞬间可以最多放行 100 个请求。突发流量过后,token 再按每秒 10 个慢慢补回来。
这就是 Token Bucket 和很多简单限流器不一样的地方。
它不是只关心某一秒有没有超过阈值,而是允许系统把空闲期的额度攒下来,换成后面的 burst capacity。
这在真实系统里很有用。因为流量通常不是平的。用户点击、任务调度、消息消费,经常都会出现短时间集中到达。
伪代码
核心实现不复杂。
每次请求到来时,先根据距离上次 refill 的时间,计算这段时间应该补多少 token。然后把 token 数量限制在 capacity 以内。最后判断当前请求能不能消费 token。
capacity = 100
tokens = 100
refill_rate = 10 # tokens per second
last_refill_time = now()
def allow_request():
global tokens, last_refill_time
current = now()
elapsed = current - last_refill_time
tokens = min(capacity, tokens + elapsed * refill_rate)
last_refill_time = current
if tokens >= 1:
tokens -= 1
return True
return False这里有一个实现细节:不一定真的需要后台线程每秒往桶里放 token。
更常见的做法是 lazy refill。也就是请求来的时候,才根据时间差计算应该补多少。这样实现更简单,也不会多一个定时任务。
Token 用完之后怎么办
Token 用完,不代表这个请求永远不能做。
它只说明:按照当前限流策略,这个请求现在不能通过。
常见处理有几种。
第一种是直接失败。
API 网关里最常见。返回 429 Too Many Requests,通常再带一个 Retry-After,告诉客户端多久之后可以再试。
第二种是客户端稍后重试。
这和直接失败不冲突。服务端拒绝当前请求,客户端按退避策略重试。等 token 补回来,请求就可能成功。
第三种是服务端排队等待。
有些系统不会立刻失败,而是把请求排队,等 token 生成后再执行。这个方案对调用方更友好,但代价也明显:延迟会上升,队列会吃内存,排队时间太长还会把后面的系统拖住。
第四种是降级处理。
比如返回缓存,简化结果,跳过非关键任务,或者只保留核心链路。
我一般会把这几种策略和业务重要性绑在一起看。
用户同步请求更适合快速失败,让客户端或上层决定是否重试。后台任务可以排队,但要有最大等待时间。非关键逻辑最好能降级,不要为了一个可选动作把主链路压垮。
常见使用场景
Token Bucket 很适合这些地方:
- API 网关限流
- 登录防刷
- 短信发送限制
- 消息队列消费控制
- 第三方 API 调用保护
这些场景的共同点是:系统需要一个稳定的平均速率,但又必须接受真实世界里的短时不均匀流量。
所以 Token Bucket 的重点不是“限制每秒只能处理多少个请求”。
更准确地说,它是在平均速率和 burst capacity 之间做了一层工程上足够好用的折中。