Sentinel限流核心:ThrottlingController设计详解

本文系统性地总结 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. 基于"期望放行时间"模型

  • 每个请求计算其 应被放行的时间点

    java 复制代码
    costTime = (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);
    }

}
相关推荐
松涛和鸣2 小时前
DAY27 Linux File IO and Standard IO Explained: From Concepts to Practice
linux·运维·服务器·c语言·嵌入式硬件·ubuntu
GeniuswongAir2 小时前
飞牛NAS死机排查
linux·运维·服务器
hhcgchpspk2 小时前
linux查找并杀死进程部分方法
linux·运维·服务器·网络·经验分享
董世昌412 小时前
JavaScript 变量声明终极指南:var/let/const 深度解析(2025 版)
java·服务器·前端
gaize12132 小时前
服务器选购指南
服务器
中屹指纹浏览器2 小时前
基于机器学习的代理 IP 风险动态评估与指纹协同技术
服务器·网络·经验分享·笔记·媒体
爱尔兰极光3 小时前
操作系统--进程同步
运维·服务器
深盾科技3 小时前
Linux跨进程内存操作的3种方法及防护方案
java·linux·网络
HalvmånEver3 小时前
Linux:基础IO(一)
linux·运维·服务器