本文系统性地总结 ThrottlingController 的设计要点 。这是 Sentinel 中实现 匀速排队限流(Throttling / Leaky Bucket) 的核心控制器,其设计兼顾了 准确性、并发安全、性能与容错性。
✅ 一、整体目标
以固定速率放行请求,超出速率的请求排队等待(最多
maxQueueingTimeMs),超时则拒绝。
- 类似 漏桶算法(Leaky Bucket);
- 保证请求处理的 平滑性 和 可预测性;
- 适用于对下游有严格 QPS 要求的场景(如调用第三方 API、数据库写入等)。
✅ 二、关键设计要点
1. 时间精度自适应:毫秒 vs 纳秒
java
this.useNanoSeconds = statDurationMs % Math.round(maxCountPerStat) != 0
|| maxCountPerStat / statDurationMs > 1;
- 高 QPS(>1000)或非整除速率 → 使用
System.nanoTime()提高精度; - 低 QPS(如 1~100)且整除 → 使用
currentTimeMillis(),避免纳秒开销; - ✅ 平衡精度与性能。
2. 基于"期望放行时间"模型
-
每个请求计算其 应被放行的时间点 :
javacostTime = (statDurationMs * acquireCount) / count; // 单位:ms 或 ns expectedTime = latestPassedTime + costTime; -
如果当前时间 ≥
expectedTime→ 立即放行; -
否则 → 需要等待
waitTime = expectedTime - currentTime。
💡 这是 时间槽预约机制:每个请求"预订"自己的放行时刻。
3. 双重检查机制(关键!)
为解决 并发竞争 问题,采用 "预判 + 原子确认" 两阶段:
第一阶段:快速预判(非原子)
java
long waitTime = costTime + latestPassedTime.get() - currentTime;
if (waitTime > maxQueueingTime) return false;
- 快速过滤明显超时的请求,避免不必要的 CAS 操作。
第二阶段:原子抢占 + 最终校验
java
long oldTime = latestPassedTime.addAndGet(costTime); // 原子占位
waitTime = oldTime - currentTime;
if (waitTime > maxQueueingTime) {
latestPassedTime.addAndGet(-costTime); // 回滚
return false;
}
- ✅ 确保在真正"抢到位置"后仍满足超时限制;
- ❌ 若省略第二步,会导致 并发下越界放行或超时违规。
🔍 这正是你之前问的"红色框逻辑是否重复"------不是重复,而是必要校验。
4. 线程安全:AtomicLong + CAS
- 使用
AtomicLong latestPassedTime保证多线程下时间戳更新的原子性; addAndGet实现无锁并发控制;- 即使多个线程同时进入,也能正确分配放行时间槽。
5. 阻塞式等待(同步限流)
- 通过
Thread.sleep()(毫秒)或LockSupport.parkNanos()(纳秒)实现等待; - ⚠️ 会阻塞当前线程,不适合高并发 Web 请求(可能耗尽线程池);
- ✅ 适合 后台任务、异步消费、API 调用限速 等场景。
6. 边界条件处理完善
| 条件 | 处理方式 |
|---|---|
acquireCount <= 0 |
直接放行(无消耗) |
count <= 0 |
直接拒绝(禁止通行) |
waitTime <= 0(竞态) |
不 sleep,直接放行 |
| 超时 | 拒绝,并回滚 latestPassedTime |
7. 兼容性与扩展性
- 实现
TrafficShapingController接口,可插拔; - 支持自定义
statDurationMs(不局限于 1 秒); - 支持
acquireCount > 1(一次消耗多个 token)。
✅ 三、典型使用场景
| 场景 | 是否适用 | 说明 |
|---|---|---|
| Web 接口防刷 | ❌ 不推荐 | 会阻塞 Tomcat 线程 |
| 调用第三方 API(QPS=10) | ✅ 强烈推荐 | 保证不超限 |
| 消息队列消费者限速 | ✅ 推荐 | 平滑消费,避免压垮 DB |
| 日志写入限流 | ✅ 可用 | 防止 I/O 飙升 |
✅ 四、与其他限流模式对比
| 控制器 | 行为 | 并发模型 | 适用场景 |
|---|---|---|---|
DefaultController(普通 QPS) |
超过直接拒绝 | 非阻塞 | 高并发 Web 接口 |
WarmUpController |
预热模式 | 非阻塞 | 冷启动保护 |
ThrottlingController |
排队等待 | 阻塞 | 需要平滑、不能丢弃请求 |
✅ 五、总结:设计哲学
"精确控制时间槽 + 并发安全抢占 + 超时兜底"
- 精确:自适应纳秒/毫秒,支持任意 QPS;
- 安全:AtomicLong + 双重检查,避免并发错误;
- 可靠:超时拒绝 + 回滚机制,保证 SLA;
- 实用:虽阻塞,但在合适场景下无可替代。
如果你在系统中需要 严格匀速、不丢请求、可容忍等待 的限流策略,ThrottlingController 是一个经过生产验证的优秀实现。
以下为源码
java
/**
* ThrottlingController 是一个基于时间窗口的流量控制控制器。
* 它通过限制单位时间内允许通过的请求数量来实现限流功能,
* 并支持请求排队等待机制以平滑突发流量。
*
* <p>该类使用原子操作保证线程安全,并根据配置选择纳秒或毫秒级精度进行时间计算。</p>
*/
public class ThrottlingController implements TrafficShapingController {
// Refactored from legacy RateLimitController of Sentinel 1.x.
private static final long MS_TO_NS_OFFSET = TimeUnit.MILLISECONDS.toNanos(1);
/**
* 最大排队等待时间(单位:毫秒)
*/
private final int maxQueueingTimeMs;
/**
* 统计周期长度(单位:毫秒),用于定义限流的时间窗口大小
*/
private final int statDurationMs;
/**
* 在统计周期内允许的最大请求数量
*/
private final double count;
/**
* 是否启用纳秒级别的时间精度来进行更精确的控制
*/
private final boolean useNanoSeconds;
/**
* 上一次成功通过请求的时间戳(可能为纳秒或毫秒)
*/
private final AtomicLong latestPassedTime = new AtomicLong(-1);
/**
* 构造方法,创建一个新的 ThrottlingController 实例。
*
* @param queueingTimeoutMs 请求在被拒绝前可以排队等待的最大时间(单位:毫秒)
* @param maxCountPerStat 每个统计周期内允许的最大请求数量
*/
public ThrottlingController(int queueingTimeoutMs, double maxCountPerStat) {
this(queueingTimeoutMs, maxCountPerStat, 1000);
}
/**
* 构造方法,创建一个新的 ThrottlingController 实例。
*
* @param queueingTimeoutMs 请求在被拒绝前可以排队等待的最大时间(单位:毫秒)
* @param maxCountPerStat 每个统计周期内允许的最大请求数量
* @param statDurationMs 统计周期长度(单位:毫秒)
*/
public ThrottlingController(int queueingTimeoutMs, double maxCountPerStat, int statDurationMs) {
AssertUtil.assertTrue(statDurationMs > 0, "statDurationMs should be positive");
AssertUtil.assertTrue(maxCountPerStat >= 0, "maxCountPerStat should be >= 0");
AssertUtil.assertTrue(queueingTimeoutMs >= 0, "queueingTimeoutMs should be >= 0");
this.maxQueueingTimeMs = queueingTimeoutMs;
this.count = maxCountPerStat;
this.statDurationMs = statDurationMs;
// Use nanoSeconds when durationMs%count != 0 or count/durationMs> 1 (to be accurate)
if (maxCountPerStat > 0) {
this.useNanoSeconds = statDurationMs % Math.round(maxCountPerStat) != 0 || maxCountPerStat / statDurationMs > 1;
} else {
this.useNanoSeconds = false;
}
}
/**
* 判断当前请求是否可以通过流量控制策略。
*
* @param node 当前节点信息
* @param acquireCount 需要获取的令牌数量(即本次请求占用的配额)
* @return 如果请求可通行则返回 true,否则返回 false
*/
@Override
public boolean canPass(Node node, int acquireCount) {
return canPass(node, acquireCount, false);
}
/**
* 使用纳秒精度检查请求是否可以通过。
*
* @param acquireCount 需要获取的令牌数
* @param maxCountPerStat 每个统计周期内的最大请求数
* @return 如果请求可通行则返回 true,否则返回 false
*/
private boolean checkPassUsingNanoSeconds(int acquireCount, double maxCountPerStat) {
final long maxQueueingTimeNs = maxQueueingTimeMs * MS_TO_NS_OFFSET;
long currentTime = System.nanoTime();
// 计算两个连续请求之间应间隔的时间(纳秒)
final long costTimeNs = Math.round(1.0d * MS_TO_NS_OFFSET * statDurationMs * acquireCount / maxCountPerStat);
// 期望本次请求应该通过的时间点
long expectedTime = costTimeNs + latestPassedTime.get();
if (expectedTime <= currentTime) {
// Contention may exist here, but it's okay.
latestPassedTime.set(currentTime);
return true;
} else {
final long curNanos = System.nanoTime();
// 计算需要等待的时间
long waitTime = costTimeNs + latestPassedTime.get() - curNanos;
if (waitTime > maxQueueingTimeNs) {
return false;
}
long oldTime = latestPassedTime.addAndGet(costTimeNs);
waitTime = oldTime - curNanos;
if (waitTime > maxQueueingTimeNs) {
latestPassedTime.addAndGet(-costTimeNs);
return false;
}
// in race condition waitTime may <= 0
if (waitTime > 0) {
sleepNanos(waitTime);
}
return true;
}
}
/**
* 使用毫秒缓存方式检查请求是否可以通过。
*
* @param acquireCount 需要获取的令牌数
* @param maxCountPerStat 每个统计周期内的最大请求数
* @return 如果请求可通行则返回 true,否则返回 false
*/
private boolean checkPassUsingCachedMs(int acquireCount, double maxCountPerStat) {
long currentTime = TimeUtil.currentTimeMillis();
// 计算两个连续请求之间应间隔的时间(毫秒)
long costTime = Math.round(1.0d * statDurationMs * acquireCount / maxCountPerStat);
// 期望本次请求应该通过的时间点
long expectedTime = costTime + latestPassedTime.get();
if (expectedTime <= currentTime) {
// Contention may exist here, but it's okay.
latestPassedTime.set(currentTime);
return true;
} else {
// 计算需要等待的时间
long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();
if (waitTime > maxQueueingTimeMs) {
return false;
}
long oldTime = latestPassedTime.addAndGet(costTime);
waitTime = oldTime - TimeUtil.currentTimeMillis();
if (waitTime > maxQueueingTimeMs) {
latestPassedTime.addAndGet(-costTime);
return false;
}
// in race condition waitTime may <= 0
if (waitTime > 0) {
sleepMs(waitTime);
}
return true;
}
}
/**
* 判断当前请求是否可以通过流量控制策略。
*
* @param node 当前节点信息
* @param acquireCount 需要获取的令牌数量(即本次请求占用的配额)
* @param prioritized 是否是优先级较高的请求
* @return 如果请求可通行则返回 true,否则返回 false
*/
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
// Pass when acquire count is less or equal than 0.
if (acquireCount <= 0) {
return true;
}
// Reject when count is less or equal than 0.
// Otherwise, the costTime will be max of long and waitTime will overflow in some cases.
if (count <= 0) {
return false;
}
if (useNanoSeconds) {
return checkPassUsingNanoSeconds(acquireCount, this.count);
} else {
return checkPassUsingCachedMs(acquireCount, this.count);
}
}
/**
* 使当前线程休眠指定的毫秒数。
*
* @param ms 要休眠的毫秒数
*/
private void sleepMs(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
}
}
/**
* 使当前线程休眠指定的纳秒数。
*
* @param ns 要休眠的纳秒数
*/
private void sleepNanos(long ns) {
LockSupport.parkNanos(ns);
}
}