令牌桶算法——流量控制和限流
背景
近期在做调用第三方系统接口的业务场景时,经常会出现接口超时的问题,跟第三方系统一番扯皮后发现原来是他们做了限流,限制了我们一秒钟只能请求2次,所以需要我们自己做好限流,控制我们的请求频率。
令牌桶算法(Token Bucket Algorithm)是一种常用的流量控制和限流算法,广泛用于网络传输、API 限流、系统资源管理等场景中。它通过一个“桶”来模拟请求的处理能力,并以固定的速率往桶中放入令牌(token),只有拿到令牌的请求才能被处理。
🧠 基本原理
令牌桶算法的核心思想是:
- 桶中可以存放一定数量的“令牌”。
- 系统以固定速率往桶中添加令牌(比如每秒添加10个令牌)。
- 当有请求到来时,必须从桶中取出一个令牌,如果桶中没有令牌,则请求被拒绝或等待。
- 桶有容量上限,超过容量的令牌会被丢弃。
这样就可以限制请求的平均速率,同时允许一定程度的突发流量(因为桶中可以存储令牌)。
🔍 特点
特性 | 描述 |
---|---|
平均速率限制 | 可以限制请求的平均速率 |
支持突发流量 | 如果桶中有积压的令牌,可以在短时间内处理大量请求 |
实现简单 | 逻辑清晰,容易实现 |
非阻塞 | 可以选择是否等待令牌 |
📦 示例说明
假设我们有一个令牌桶:
- 容量为 10 个令牌;
- 每秒添加 2 个令牌;
- 请求需要获取令牌才能执行。
场景举例:
- 正常情况:每秒最多处理 2 个请求;
- 空闲后突发:如果前几秒没有请求,桶里积累了多个令牌,这时可以处理突发的多个请求;
- 超出限制:当请求速度过快,桶中没有令牌时,后续请求将被拒绝。
💻 Java 实现示例
下面是两个简单的 Java 实现
非阻塞,获取不到令牌就直接拒绝:
import java.util.concurrent.atomic.AtomicLong;public class TokenBucket {// 每秒生成的令牌数private final long capacity;// 桶的最大容量private final long rate;// 当前令牌数量private AtomicLong tokens = new AtomicLong(0);// 上一次补充令牌的时间private long lastRefillTime = System.currentTimeMillis();public TokenBucket(long rate, long capacity) {this.rate = rate;this.capacity = capacity;this.tokens.set(capacity); // 初始填满}/*** 尝试获取一个令牌*/public synchronized boolean tryConsume() {refill();if (tokens.get() > 0) {tokens.decrementAndGet();return true;}return false;}/*** 根据时间差补充令牌*/private void refill() {long now = System.currentTimeMillis();long timeElapsed = now - lastRefillTime;// 计算应该补充的令牌数long tokensToAdd = (timeElapsed * rate) / 1000; // 毫秒转秒if (tokensToAdd > 0) {lastRefillTime = now;long newTokens = Math.min(tokens.get() + tokensToAdd, capacity);tokens.set(newTokens);}}public static void main(String[] args) throws InterruptedException {TokenBucket bucket = new TokenBucket(2, 5); // 每秒2个令牌,最多存5个for (int i = 0; i < 10; i++) {if (bucket.tryConsume()) {System.out.println("Request " + (i + 1) + " processed.");} else {System.out.println("Request " + (i + 1) + " rejected.");}Thread.sleep(200); // 模拟请求频率}}
}
阻塞,获取不到令牌就等待执行:
package org.ffjy.lld.ffcrm.common;import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;public class TokenBucket {// 桶的最大容量private final long capacity;// 每秒生成的令牌数private final long rate;// 当前令牌数量private AtomicLong tokens = new AtomicLong(0);// 上一次补充令牌的时间private long lastRefillTime = System.currentTimeMillis();// 使用锁和条件变量实现等待机制private final Lock lock = new ReentrantLock();private final Condition notEmpty = lock.newCondition();public TokenBucket(long rate, long capacity) {this.rate = rate;this.capacity = capacity;this.tokens.set(capacity); // 初始填满}/*** 获取一个令牌,如果没有令牌则等待*/public void acquire() throws InterruptedException {lock.lock();try {while (true) {refill();if (tokens.get() > 0) {tokens.decrementAndGet();return;}// 计算需要等待的时间(毫秒)long now = System.currentTimeMillis();long timeUntilRefill = 1000 / rate; // 下一次补充令牌的时间间隔long waitTime = Math.max(0, lastRefillTime + timeUntilRefill - now);// 等待直到有新的令牌补充或超时notEmpty.await(waitTime, TimeUnit.MILLISECONDS);}} finally {lock.unlock();}}/*** 根据时间差补充令牌*/private void refill() {long now = System.currentTimeMillis();long timeElapsed = now - lastRefillTime;// 计算应该补充的令牌数long tokensToAdd = (timeElapsed * rate) / 1000; // 毫秒转秒if (tokensToAdd > 0) {lastRefillTime = now;long newTokens = Math.min(tokens.get() + tokensToAdd, capacity);tokens.set(newTokens);// 唤醒所有等待线程notEmpty.signalAll();}}public static void main(String[] args) throws InterruptedException {TokenBucket bucket = new TokenBucket(2, 5); // 每秒2个令牌,最多存5个for (int i = 0; i < 10; i++) {bucket.acquire();System.out.println("Request " + (i + 1) + " processed.");}}
}
⚙️ 与漏桶算法的区别(Guava 的 RateLimiter
使用的是平滑化的令牌桶)
对比项 | 令牌桶 | 漏桶 |
---|---|---|
控制方式 | 控制请求是否能拿取令牌 | 控制请求流出的速度 |
流量特性 | 允许突发流量 | 强制平滑输出 |
实现复杂度 | 简单 | 较复杂 |
应用场景 | API限流、网络带宽控制 | 需要严格控制输出速率的系统 |
🛠 实际应用
- Spring Cloud Gateway / Zuul:做网关限流
- Nginx:基于令牌桶实现请求限速
- Guava RateLimiter:使用了改进版的令牌桶算法
- 分布式系统限流:结合 Redis 实现分布式令牌桶
✅ 总结
令牌桶算法是一种非常实用的限流机制,具有以下优点:
- 能够限制请求的平均速率
- 支持一定的突发流量
- 实现简单,性能好
在实际开发中,推荐结合使用如 Guava 的 RateLimiter
或 Spring Cloud Gateway 的限流组件,但在理解其背后原理(如令牌桶)之后,你可以根据业务需求进行定制化开发。