基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

Sentinel 的热点参数限流是如何实现的?

知识点图片

Sentinel 的热点参数限流(ParamFlow)是为了防止由于某个具体参数(如某个商品 ID、某个用户 ID)的突发高并发请求导致系统崩溃而设计的。

与 Sentinel 普通的基于资源的限流(使用滑动时间窗口 LeapArray)不同,热点参数限流在底层实现上有着显著的区别。它的核心实现原理可以概括为:LRU 缓存淘汰算法 + 令牌桶算法(动态计算)

下面从核心组件、数据结构、算法细节等几个维度来深入剖析它的实现机制。


1. 为什么不能用普通的滑动窗口?

如果使用普通的滑动窗口统计每个参数的 QPS,假设有一个接口带有 userId 参数,系统里有千万级别的活跃用户,如果为每一个 userId 都创建一个滑动窗口(LeapArray),会导致极其严重的内存溢出(OOM)
因此,热点参数限流必须解决“无限的参数值空间”与“有限的内存资源”之间的矛盾。

2. 核心数据结构:LRU Map

为了解决内存问题,Sentinel 引入了 LRU(Least Recently Used,最近最少使用) 策略。
Sentinel 使用了 Google Guava 内部非常著名的 ConcurrentLinkedHashMap 的变体(ConcurrentLinkedHashMapWl)作为底层存储结构。

  • 作用: 只保留最近被访问到的、最活跃的参数的统计信息。
  • 机制: 当不同的参数值(如不同的 userId)数量超过设定的最大容量(默认是 100000,或由规则指定),最久未被访问的参数的统计信息会被移除。未被记录的参数说明并发不高,不需要限流;只有真正被频繁访问的“热点”参数才会一直留在缓存中并受到限流控制。

3. 核心算法:令牌桶(Token Bucket)的惰性计算

在热点参数限流中,Sentinel 使用的是令牌桶算法,并且采用的是“惰性计算”(Lazy Calculation)的方式,而不是启动一个后台线程定时去发放令牌(后台线程去遍历几万个参数发令牌极其消耗 CPU)。

惰性计算的过程解析:

当一个携带具体参数(比如 userId=123)的请求到达时,底层在 ParamFlowChecker.passCheck() 方法中执行以下逻辑:

  1. 获取当前时间:获取当前系统的时间戳。
  2. 查询 LRU 缓存:从 LRU Map 中查找 userId=123 对应的上次添加令牌的时间lastAddTokenTime)和当前剩余令牌数oldQps)。
  3. 计算时间差与应发令牌数
    • 如果当前时间与上次添加令牌的时间在同一个统计周期(通常是 1 秒)内,说明不需要新增令牌。
    • 如果超过了时间间隔,则计算这段时间内应该生成的令牌数:
      新增令牌数 = (当前时间 - 上次添加时间) * (限流阈值 / 1000ms)
  4. 更新令牌桶
    • 当前令牌数 = 旧令牌数 + 新增令牌数
    • 注意:当前令牌数不能超过配置的最大阈值(桶的容量)。
  5. 扣减令牌与判断
    • 如果当前令牌数 \ge 请求需要的令牌数(通常是 1),则允许通过,扣减令牌,并更新 LRU 缓存中的“剩余令牌数”和“最后发放时间”。
    • 如果令牌不够,则拒绝请求(抛出 ParamFlowException)。

以上逻辑通过 CAS(Compare-And-Swap)或加锁操作保证并发安全。

4. 源码执行链路

当你的代码执行 SphU.entry(resourceName, EntryType.IN, count, args) 时,包含参数的 args 会被传入 Sentinel 的责任链。

  1. 请求进入 ParamFlowSlot(热点参数限流槽)。
  2. ParamFlowSlot 会检查当前资源是否配置了 ParamFlowRule(热点规则)。
  3. 如果配置了,会根据规则中的 paramIdx(参数索引)从 args 数组中提取出具体的参数值。
  4. 调用 ParamFlowChecker.passCheck(resourceWrapper, rule, count, value) 进行核心校验。
  5. 校验通过则放行进入下一个 Slot,不通过则抛出异常。

5. 特殊处理:参数例外项(Exception Items)

在实际业务中,我们经常遇到这种需求:“普通商品限流 10 QPS,但 iPhone 14(ID=999)是大促商品,单独给它限流 10000 QPS”。

Sentinel 热点限流支持参数例外项paramFlowItemList)。
ParamFlowChecker 的源码中,逻辑是这样的:

  1. 首先检查当前提取到的参数值,是否存在于例外项 Map 中。
  2. 如果存在,则直接使用例外项配置的特定阈值去进行上述的令牌桶计算。
  3. 如果不存在,才使用规则中配置的全局默认阈值进行计算。

6. 线程数限流模式

除了 QPS 模式,热点参数也支持线程数(Thread Count)限流
相比于 QPS 的令牌桶,线程数的实现非常简单:

  • 依然使用 LRU Map 存储每个参数的当前并发线程数。
  • 请求进入时,在 LRU Map 中找到对应参数,原子性地增加计数(AtomicInteger.incrementAndGet)。
  • 如果计数超过阈值,直接拒绝。
  • 请求结束退出(exit)时,原子性地减少计数。

总结

Sentinel 热点参数限流的实现精髓在于:

  1. 防 OOM:放弃了标准限流的 LeapArray 数组,使用基于 ConcurrentLinkedHashMapLRU Cache 来存储状态,只统计真正的热点参数。
  2. 高性能:放弃了后台定时发令牌的模式,采用基于请求驱动的令牌桶惰性计算(Lazy Token Bucket)算法,极大降低了 CPU 消耗。
  3. 灵活性:内置支持“例外参数”配置,能够满足精细化运营和大促场景的诉求。
00:00
00:00