Skip to content

Sentinel 详解:流量治理

  Sentinel 是阿里巴巴开源的流量治理组件,以"流量"为切入点,提供流量控制、熔断降级、系统负载保护等多维度保护能力。它是 Spring Cloud Alibaba 生态中替代 Hystrix 的核心组件。

一、为什么需要 Sentinel?

1.1 没有流量治理会发生什么?

  场景:秒杀活动,瞬时流量 10000 QPS,服务只能扛 1000 QPS

雪崩

  Sentinel 的价值在于:在流量到达业务代码之前,就把它拦住。不是等系统崩溃了再补救,而是提前预防。

1.2 Sentinel 部署在哪里?是网关还是每个服务?

  Sentinel 不是一个独立的网关或中间件,而是一个嵌入到应用中的库(Library)。 它不是像 Nginx 那样独立部署的代理,而是以 jar 包的形式跑在每个服务进程里。

Sentinel 部署架构

两层部署策略:

层级部署位置作用类比
网关层Gateway 中引入 Sentinel全局入口限流,拦截非法/超量请求小区大门保安
服务层每个微服务中引入 Sentinel单服务精细限流、熔断降级每家每户的门锁

最佳实践:两层都加,双层防护。

Sentinel 最佳实践:两层都加,双层防护。

  为什么两层都要加?网关层只能按路由限流(如 /order/** QPS=1000),但无法区分具体接口(如 /order/create/order/query 的 QPS 不同)。服务层可以按资源名精确限流,还能做熔断降级和热点参数限流。网关层挡住 90% 的无效流量,服务层精准控制剩下的 10%。

1.3 Sentinel 能做什么?

能力解决的问题一句话
流量控制流量突发,超过系统承载能力"多了就拒" — 超过 QPS 阈值直接拒绝
熔断降级下游服务不稳定,拖垮调用方"坏了就断" — 出错率高了自动熔断
系统保护系统整体负载过高"太忙就限" — Load/CPU 高了自动限流
热点限流某个热点数据被频繁访问"热的就限" — 针对热点参数单独限流
网关流控网关层统一限流"门口就拦" — 在网关层拦截

1.3 Sentinel vs Hystrix 为什么选 Sentinel?

维度SentinelHystrix
隔离策略信号量隔离线程池隔离 / 信号量隔离
熔断策略慢调用比例 / 异常比例 / 异常数仅异常比例
流量控制丰富(QPS/线程/关联/链路/热点)无,仅熔断
规则动态修改控制台实时推送,秒级生效有限支持
规则持久化Nacos / Apollo / ZooKeeper / 文件无原生支持
控制台功能完善,实时监控Dashboard 简陋
维护状态阿里活跃维护Netflix 停维
性能轻量,对业务代码无侵入线程池隔离有额外开销

二、核心架构与工作原理

2.1 Sentinel 整体架构

Sentinel 整体架构

核心工作原理:

控制台是独立的管理端: 只负责监控展示、规则配置、集群管理,不参与实际流量拦截

每个应用内置 Sentinel 核心: 所有限流、熔断、降级逻辑都在应用本地的 Slot Chain 责任链里执行,不依赖控制台

通信方式: 应用通过 8719 端口和控制台通信,定期上报心跳和指标,控制台推送规则

高可用特性: 即使控制台挂了,应用本地已经加载的规则依然生效,不会影响流量防护

2.2 责任链模式 — Slot Chain

  Sentinel 的核心是一套责任链(Slot Chain),每个 Slot 负责一个职责,像流水线一样处理请求:

请求进入


┌──────────────────────┐
│ NodeSelectorSlot     │  ← 构建资源调用链,记录调用关系
├──────────────────────┤
│ ClusterBuilderSlot   │  ← 构建集群节点,统计集群维度的数据
├──────────────────────┤
│ LogSlot              │  ← 记录日志(Block 日志)
├──────────────────────┤
│ StatisticSlot        │  ← 核心统计:记录 RT、成功/失败/拒绝数
├──────────────────────┤
│ AuthoritySlot        │  ← 黑白名单控制
├──────────────────────┤
│ SystemSlot           │  ← 系统保护规则检查
├──────────────────────┤
│ ParamFlowSlot        │  ← 热点参数限流检查
├──────────────────────┤
│ FlowSlot             │  ← 流量控制规则检查
├──────────────────────┤
│ DegradeSlot          │  ← 熔断降级规则检查
└──────────────────────┘


执行业务逻辑

关键理解: 每个请求都要按顺序经过所有 Slot,任何一个 Slot 抛出 BlockException,请求就被拦截,不会到达业务代码。

2.3 核心概念:Context、Entry、Node

  这三个概念是理解 Sentinel 内部机制的钥匙。它们不是简单的包含关系,而是三种不同职责的对象,共同协作完成流量治理。

2.3.1 先搞清楚:它们不是包含关系

  很多人误以为 Context → Entry → Node 是层层包含,但实际不是。它们各自独立,通过引用关联:

┌──────────────────────────────────────────────────────────────────────┐
│                        三者是协作关系,不是包含关系                     │
│                                                                      │
│                      ┌──────────────────────┐                        │
│                      │       Context        │                        │
│                      │  (一次请求一个)       │                        │
│                      │                      │                        │
│                      │  entranceNode ──────────────────────────┐     │
│                      │  curEntry ──────────────┐               │     │
│                      └──────────────────────┘  │               │     │
│                                                │               │     │
│              ┌─────────────────────────────────┘               │     │
│              ▼                                                  │     │
│  ┌───────────────────────┐                                      │     │
│  │       Entry           │    每个 Entry 持有                    │     │
│  │  (每次 entry() 一个)   │◄── 两个 Node 引用                     │     │
│  │                       │                                      │     │
│  │  curNode ─────────────┼──→ DefaultNode (Context 维度统计)     │     │
│  │  originNode ──────────┼──→ ClusterNode (全局汇总,共享)        │     │
│  └───────────────────────┘                                      │     │
│                                                                 │     │
│  ┌──────────────────────────────────────────────────────────────┘     │
│  ▼                                                                     │
│  ┌─────────────────┐    ┌─────────────────────────────────────────┐   │
│  │  EntranceNode   │    │          ClusterNode(全局唯一)          │   │
│  │  (调用树根)      │    │                                          │   │
│  │                 │    │  ClusterNode("order")   QPS=100, RT=35ms  │   │
│  │ childList:      │    │  ClusterNode("inventory") QPS=50, RT=20ms │   │
│  │  [DefaultNode]  │    │  ClusterNode("payment")  QPS=30, RT=50ms  │   │
│  └─────────────────┘    └─────────────────────────────────────────┘   │
│                                                                        │
│  ★ Context 不持有 Entry 列表,只持有"当前 Entry"引用                    │
│  ★ Entry 不包含 Node,只是持有 Node 的引用指针                          │
│  ★ ClusterNode 不属于任何 Context,是全局 Map 中的独立对象              │
│  ★ EntranceNode 是特殊的 DefaultNode,作为调用树的根存在                │
└──────────────────────────────────────────────────────────────────────┘

一个更准确的类比:

概念类比说明
Context一次请求的"工单"记录这次请求的基本信息(来源、入口)
Entry工单上的"工序"每执行一个受保护的操作,就记录一道工序,工序之间可以嵌套
DefaultNode工序的"工时统计"这道工序在本工单中花了多少时间、成功/失败次数
ClusterNode全厂的"汇总报表"所有工单中同一道工序的汇总数据,不在任何工单里
EntranceNode工单的"根工序"整个工单的起点,DefaultNode 的树根

2.3.2 三者关系全景图(调用树视角)

一次 HTTP 请求进来


┌─────────────────────────────────────────────────────────────────┐
│  Context(上下文)                                                │
│  name: "sentinel_default_context"                                │
│  entranceNode: EntranceNode ──── 入口节点,整棵调用树的根          │
│  curEntry: 当前正在执行的 Entry 引用                               │
│                                                                  │
│  ★ 调用树(EntranceNode 是根,DefaultNode 形成父子链):            │
│                                                                  │
│  EntranceNode                                                    │
│    └── DefaultNode("orderService")    ← Entry①.curNode            │
│         RT: 35ms, QPS: 100, Thread: 3                            │
│         │                                                        │
│         ├── DefaultNode("inventoryService") ← Entry②.curNode      │
│         │    RT: 20ms, QPS: 50                                    │
│         │                                                        │
│         └── DefaultNode("paymentService")   ← Entry③.curNode      │
│              RT: 50ms, QPS: 30                                    │
│                                                                  │
│  ★ 每个 DefaultNode 同时向对应的 ClusterNode 汇总数据:            │
│                                                                  │
│  DefaultNode("orderService")    ──→ ClusterNode("orderService")   │
│  DefaultNode("inventoryService")──→ ClusterNode("inventoryService")│
│  DefaultNode("paymentService")  ──→ ClusterNode("paymentService") │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  ClusterNode(全局 Map,不属于任何 Context)                       │
│                                                                  │
│  ClusterNode("orderService")   → 所有 Context 的汇总              │
│  ClusterNode("inventoryService")→ 所有 Context 的汇总              │
│  ClusterNode("paymentService") → 所有 Context 的汇总              │
│                                                                  │
│  ★ FlowSlot 限流判断读的就是 ClusterNode 的数据                   │
└─────────────────────────────────────────────────────────────────┘

核心关系一句话:

  • Context 持有 Entry 的引用(curEntry),通过 Entry 间接访问 Node
  • Entry 持有两个 Node 引用(curNode → DefaultNode,originNode → 统计来源)
  • DefaultNode 之间形成父子链(childList),构建调用树,树根是 EntranceNode
  • ClusterNode 是全局独立的,通过 资源名 → ClusterNode 的 Map 访问,不属于任何 Context

2.3.3 Context(上下文)

一句话:一次请求的"身份证",记录整条调用链的信息。

属性说明
name上下文名称,默认 sentinel_default_context,可自定义
entranceNodeEntranceNode,整棵调用树的根节点
curEntry当前正在执行的 Entry
origin调用来源(如 appAgateway

生命周期:

请求进入 → ContextUtil.enter() 创建 Context → 放入 ThreadLocal
        → 多次 SphU.entry() 嵌套调用
        → 请求结束 → ContextUtil.exit() 清理 ThreadLocal

源码关键:

java
// ContextUtil.java
public static Context enter(String name, String origin) {
    // 1. 从 ThreadLocal 获取或创建 Context
    Context context = new Context(entranceNode, name);
    context.setOrigin(origin);
    // 2. 放入 ThreadLocal
    ContextUtil.setContext(context);
    return context;
}

一个线程一个 Context: Context 存储在 ThreadLocal 中,不同请求(不同线程)互不干扰。这也是 Sentinel 能做到线程级别隔离的基础。

命名规则:

  • 默认:sentinel_default_context
  • 服务调用:appA → 调用方应用名
  • 网关调用:gateway_route_xxx → 路由 ID

2.3.3 Entry(入口)

一句话:代表"进入一个被保护资源"的动作,每次 SphU.entry() 创建一个 Entry。

属性说明
resource资源名称,如 "orderService"
createTime进入时间戳,用于计算 RT
curNode当前资源对应的 DefaultNode
originNode调用来源对应的 OriginNode

Entry 的核心作用:

  1. 触发 Slot ChainSphU.entry() 内部创建 Entry 时,会触发整个 Slot Chain 执行
  2. 记录 RTentry() 记录 createTimeexit() 时计算 当前时间 - createTime = RT
  3. 嵌套调用:Entry 可以嵌套,形成父子关系,构建调用树

使用模式:

java
// 模式一:手动 try-finally
Entry entry = null;
try {
    entry = SphU.entry("resourceName");   // ① 创建 Entry,触发 Slot Chain
    // 业务逻辑
} catch (BlockException e) {              // ② 被拦截
    handleBlock();
} finally {
    if (entry != null) {
        entry.exit();                      // ③ 退出,记录 RT,还原调用树
    }
}

// 模式二:注解方式(推荐),Sentinel 自动处理 entry/exit
@SentinelResource("resourceName")
public void doSomething() {
    // 业务逻辑
}

嵌套调用示例:

java
// 订单服务调用库存服务
entry = SphU.entry("orderService");           // 外层 Entry
    // 业务逻辑
    entry2 = SphU.entry("inventoryService");  // 内层 Entry(嵌套)
        // 调用库存
    entry2.exit();
entry.exit();

此时调用树形态:

EntranceNode
  └── DefaultNode("orderService")
        └── DefaultNode("inventoryService")  ← 作为 orderService 的子节点

2.3.4 Node(节点)

一句话:统计某个资源数据的容器,是 Sentinel 限流/熔断判断的数据来源。

Node 有 4 种类型,各司其职:

类型全称作用生命周期
StatisticNode统计节点基础统计类,封装滑动窗口统计逻辑所有 Node 的父类
DefaultNode默认节点记录某个 Context 下某资源的统计 + 子调用关系与资源绑定,JVM 级别
ClusterNode集群节点记录所有 Context 下某资源的汇总统计全局唯一,与资源一一对应
EntranceNode入口节点调用树的根节点,记录入口流量每个 Context 一个

继承关系:

StatisticNode(基础统计能力:滑动窗口、读写锁)
  ├── DefaultNode(增加 childList,记录调用链父子关系)
  │     └── EntranceNode(调用树根节点,每个 Context 一个)

  └── ClusterNode(汇总节点,一个资源全局唯一)

四种 Node 的数据关系:

假设有两个请求同时进来:

Context-A(请求 A)                    Context-B(请求 B)
  EntranceNode                           EntranceNode
    └─ DefaultNode("order")    ──┐        └─ DefaultNode("order")    ──┐
         RT: 30ms               │             RT: 50ms               │
         QPS: 1                  │             QPS: 1                  │
                                 ├──→ 汇总到 ClusterNode("order") ←──┘
                                     RT 平均值: 40ms, QPS: 2
                                     
Context-A(请求 A)                    Context-B(请求 B)
    └─ DefaultNode("inventory") ──┐        └─ DefaultNode("inventory") ──┐
         RT: 20ms               │             RT: 30ms                  │
         QPS: 1                  │             QPS: 1                    │
                                 ├──→ 汇总到 ClusterNode("inventory") ←──┘
                                     RT 平均值: 25ms, QPS: 2

关键理解:

  • DefaultNode 按 Context 隔离:同一个资源"order",在 Context-A 和 Context-B 中各有一个 DefaultNode,各自统计本 Context 内的数据
  • ClusterNode 全局汇总:所有 Context 的同一资源数据汇总到一个 ClusterNode,限流判断用的就是这个
  • 为什么需要 DefaultNode 记录 childList? 为了构建调用链,知道"orderService 调用了哪些下游服务",这对故障定位和链路分析很重要

数据流:

请求 → SphU.entry("order")
  → StatisticSlot 调用 node.addPassRequest()
  → DefaultNode("order").addPassRequest()     ← 记录到 Context 维度
  → ClusterNode("order").addPassRequest()     ← 同时记录到全局汇总
  → 业务执行
  → StatisticSlot 调用 node.addRtAndSuccess()
  → DefaultNode("order").addRtAndSuccess()    ← 记录 RT 到 Context 维度
  → ClusterNode("order").addRtAndSuccess()    ← 同时记录 RT 到全局汇总

限流判断读的是 ClusterNode:

java
// FlowSlot 判断是否限流
if (ClusterNode("order").passQps() > threshold) {
    throw new FlowException("限流");
}

2.3.5 三者协作总结

一个请求的生命周期:

1. 请求进入 → ContextUtil.enter() → 创建 Context,放入 ThreadLocal

2. SphU.entry("order") → 创建 Entry("order")

3. 触发 Slot Chain → NodeSelectorSlot → 创建/获取 DefaultNode("order")
                  → ClusterBuilderSlot → 创建/获取 ClusterNode("order")
                  → StatisticSlot → 向两个 Node 记录 pass + RT
                  → FlowSlot → 读取 ClusterNode 数据判断是否限流

4. 业务逻辑执行

5. entry.exit() → 记录 RT,还原 Context 调用树

6. 请求结束 → ContextUtil.exit() → 清理 ThreadLocal

一句话总结: Context 是请求的"身份证",Entry 是"进入资源"的动作,Node 是"统计数据的账本"。三者就像:你去商场(Context),进入每家店(Entry),店里的 POS 机记录消费(Node)。ClusterNode 是商场的总账本,汇总所有顾客的消费数据。

2.4 滑动窗口统计

  Sentinel 需要实时统计每个资源的 QPS、RT、异常数等指标来做限流/熔断判断。滑动窗口就是 Sentinel 用来做这项统计的核心数据结构。

2.4.1 先理解:固定窗口有什么问题?

  最简单的统计方式是"固定窗口计数器":记下当前秒的起始时间,来一个请求计数器 +1,到了下一秒清空重新计数。

固定窗口:每秒重置计数器

时间轴:  0s        1s        2s        3s
         ├─────────┼─────────┼─────────┤
         │ 窗口0    │ 窗口1    │ 窗口2    │
         │ count=95 │ count=0  │ count=0  │
         └─────────┴─────────┴─────────┘

问题:"临界突变"(Critical Burst)
                                    
         0.5s ────────── 1.5s
         ├──────┼──────┤
         窗口0   │ 窗口1  │
         最后     │ 开头   │
         0.5s    │ 0.5s   │
         95个    │ 95个   │
                 │        │
         这 1 秒内实际通过了 190 个请求,但限流阈值是 100!

  固定窗口的问题在于:窗口边界是固定的,流量可以在两个窗口交界处"钻空子"。0.5s~1.5s 这 1 秒内,窗口 0 的后半段 + 窗口 1 的前半段,各 95 个请求,加起来 190 个,远超 100 的阈值,但两个窗口各自都没有超过 100,所以全部放行。

2.4.2 滑动窗口如何解决?

  滑动窗口的核心思想:不按固定边界统计,而是"以当前时间为基准,看最近一段时间内的数据"。相当于一个随时间滑动的框,框内始终是最近 N 秒的数据。

滑动窗口:以当前时间为基准,向前看最近 1 秒

假设每 200ms 一个窗口,共 5 个窗口覆盖 1 秒:

时刻 T=0ms(初始状态):
  ┌──────┬──────┬──────┬──────┬──────┐
  │ 窗口1 │ 窗口2 │ 窗口3 │ 窗口4 │ 窗口5 │
  │  0   │  0   │  0   │  0   │  0   │
  └──────┴──────┴──────┴──────┴──────┘
  ◄────────── 滑动窗口 1s ──────────►
  当前时间:0ms,总 QPS = 0

时刻 T=200ms(窗口1 收到 30 个请求):
  ┌──────┬──────┬──────┬──────┬──────┐
  │ 窗口1 │ 窗口2 │ 窗口3 │ 窗口4 │ 窗口5 │
  │  30  │  0   │  0   │  0   │  0   │
  └──────┴──────┴──────┴──────┴──────┘
  ◄────────── 滑动窗口 1s ──────────►
  当前时间:200ms,总 QPS = 30

时刻 T=400ms(窗口2 收到 20 个请求):
  ┌──────┬──────┬──────┬──────┬──────┐
  │ 窗口1 │ 窗口2 │ 窗口3 │ 窗口4 │ 窗口5 │
  │  30  │  20  │  0   │  0   │  0   │
  └──────┴──────┴──────┴──────┴──────┘
  ◄────────── 滑动窗口 1s ──────────►
  当前时间:400ms,总 QPS = 30+20 = 50

时刻 T=1200ms(窗口1 已过期,被新窗口覆盖):
  ┌──────┬──────┬──────┬──────┬──────┐
  │ 窗口6 │ 窗口2 │ 窗口3 │ 窗口4 │ 窗口5 │
  │  15  │  20  │  10  │  25  │  30  │   ← 窗口1 被窗口6 覆盖
  └──────┴──────┴──────┴──────┴──────┘
  ◄────────── 滑动窗口 1s ──────────►
  当前时间:1200ms,总 QPS = 15+20+10+25+30 = 100
  注意:窗口1(30) 已过期,不再计入

2.4.3 环形数组(Ring Buffer)实现

  Sentinel 用 LeapArray(环形数组)实现滑动窗口,而不是真的维护一个随时间滑动的线性数组。

核心设计:

LeapArray 参数:
  intervalInMs = 1000ms(总统计时长)
  sampleCount = 5(窗口数量)
  windowLengthInMs = 200ms(每个窗口长度 = 1000/5)

数组结构(环形,5 个槽位):
         ┌───┐
         │ 0 │ ← 槽位 0
         ├───┤
     ┌──→│ 1 │ ← 槽位 1
     │   ├───┤
     │   │ 2 │ ← 槽位 2
     │   ├───┤
     │   │ 3 │ ← 槽位 3
     │   ├───┤
     └───│ 4 │ ← 槽位 4
         └───┘
    时间前进,槽位循环使用

如何定位当前窗口?

timeId = 当前时间戳 / windowLengthInMs

例如 windowLengthInMs = 200ms:
  时间 0ms    → timeId = 0
  时间 200ms  → timeId = 1
  时间 400ms  → timeId = 2
  时间 1000ms → timeId = 5
  时间 1200ms → timeId = 6

数组下标 = timeId % 数组长度

  时间 0ms    → idx = 0 % 5 = 0
  时间 200ms  → idx = 1 % 5 = 1
  时间 1000ms → idx = 5 % 5 = 0  ← 回到槽位 0!
  时间 1200ms → idx = 6 % 5 = 1  ← 回到槽位 1!

关键判断:槽位里的窗口是否已过期?

时间 1000ms,idx = 0:
  槽位 0 里是老窗口(timeId=0, start=0ms)
  当前窗口应该是 timeId=5, start=1000ms
  old.windowStart() (0) != 当前窗口起始 (1000) → 已过期,重置!

时间 200ms,idx = 1:
  槽位 1 里是空
  → 创建新窗口

完整源码注解:

java
public abstract class LeapArray<T> {
    // 核心参数
    protected int windowLengthInMs;  // 每个窗口长度,如 200ms
    protected int sampleCount;       // 窗口数量,如 5
    protected int intervalInMs;      // 总时间跨度,如 1000ms

    // 环形数组,固定长度 = sampleCount
    protected final AtomicReferenceArray<WindowWrap<T>> array;

    // 获取当前时间窗口(核心方法)
    public WindowWrap<T> currentWindow(long timeMillis) {
        // 1. 计算当前时间对应的 timeId
        long timeId = timeMillis / windowLengthInMs;
        //    例如:1200ms / 200ms = 6

        // 2. 环形数组下标 = timeId % 数组长度
        int idx = (int) (timeId % array.length());
        //    例如:6 % 5 = 1,槽位 1

        // 3. 计算当前窗口的起始时间
        long windowStart = timeId * windowLengthInMs;
        //    例如:6 * 200 = 1200ms

        // 4. 循环获取有效窗口(解决并发问题)
        while (true) {
            WindowWrap<T> old = array.get(idx);
            // 情况 A:槽位为空,直接创建新窗口
            if (old == null) {
                WindowWrap<T> window = new WindowWrap<>(
                    windowLengthInMs, windowStart, newEmptyBucket(timeMillis)
                );
                if (array.compareAndSet(idx, null, window)) {
                    return window;  // CAS 成功,返回
                } else {
                    Thread.yield(); // CAS 失败,说明其他线程抢先了,重试
                }
            }
            // 情况 B:窗口已过期(时间对不上),重置
            else if (old.windowStart() < windowStart) {
                // 老窗口起始时间 < 当前窗口起始时间 → 已过期
                old.resetWindowTo(windowStart, windowLengthInMs);
                return old;
            }
            // 情况 C:窗口正好是当前窗口,直接返回
            else if (old.windowStart() == windowStart) {
                return old;
            }
            // 情况 D:内核时间回调,等一等
            else {
                Thread.yield();
            }
        }
    }

    // 获取所有有效窗口(用于计算 QPS)
    public List<WindowWrap<T>> list() {
        List<WindowWrap<T>> result = new ArrayList<>();
        long currentTime = TimeUtil.currentTimeMillis();
        for (WindowWrap<T> window : array) {
            if (window != null && !isWindowDeprecated(currentTime, window)) {
                result.add(window);
            }
        }
        return result;
    }

    // 判断窗口是否过期:窗口起始时间 + 总时间跨度 < 当前时间
    private boolean isWindowDeprecated(long currentTime, WindowWrap<T> window) {
        return currentTime - window.windowStart() > intervalInMs;
    }
}

2.4.4 数据如何写入和读取?

每个窗口(WindowWrap)内部包含一个 MetricBucket:

java
public class MetricBucket {
    // 使用 LongAdder 记录各项指标(高并发下比 AtomicLong 性能更好)
    private final LongAdder pass = new LongAdder();    // 通过数
    private final LongAdder block = new LongAdder();   // 拒绝数
    private final LongAdder exception = new LongAdder(); // 异常数
    private final LongAdder rt = new LongAdder();      // 响应时间总和
    private volatile long minRt = Long.MAX_VALUE;      // 最小响应时间

    // 请求通过
    public void addPass(int n) { pass.add(n); }

    // 记录响应时间
    public void addRt(long rt) { this.rt.add(rt); }
}

一次请求的完整统计流程:

请求到达 StatisticSlot

  ├── ① node.addPassRequest()    ← 当前窗口 pass++
  │                               实际上是:
  │                               LeapArray.currentWindow()
  │                               .value()          ← 获取 MetricBucket
  │                               .addPass(1)       ← 通过数 +1

  │     业务执行……(耗时 RT)

  ├── ② node.addRtAndSuccess(rt)  ← 当前窗口 rt += rt

  └── ③ 后续 Slot 判断时读取:
         LeapArray.list()          ← 获取所有有效窗口
         .values()                 ← 获取所有 MetricBucket
         .sum(pass)                ← 汇总 pass 数 = QPS

判断 QPS 是否超限:

  long totalPass = 0;
  for (WindowWrap<MetricBucket> window : array.list()) {
      totalPass += window.value().pass();
  }
  if (totalPass >= threshold) {
      throw new FlowException("限流");
  }

2.4.5 为什么用环形数组而不是真的滑动窗口?

方案实现方式时间复杂度内存
真滑动窗口每次滑动都要移动数组元素,或维护链表O(n)
环形数组timeId % 数组长度定位,CAS 更新O(1)固定

环形数组的优势:

  1. O(1) 定位timeId % length 直接算出下标,无需遍历
  2. 固定内存:数组长度 = sampleCount,不会无限增长
  3. 无锁更新:CAS 操作,高并发下比锁机制高效得多
  4. 自动过期:旧窗口被新窗口自然覆盖,无需额外清理逻辑

2.4.6 常见配置

java
// 默认配置:1 秒内 2 个窗口,每个 500ms
// 适合大多数场景
new LeapArray<>(2, 1000);

// 高精度配置:1 秒内 10 个窗口,每个 100ms
// 适合需要更平滑统计的场景
new LeapArray<>(10, 1000);

// 长窗口配置:60 秒内 60 个窗口,每个 1s
// 适合熔断(需要更长时间窗口的统计数据)
new LeapArray<>(60, 60000);

窗口数怎么选?

  • 窗口太少(如 2 个)→ 统计粗糙,临界突变问题依然存在
  • 窗口太多(如 100 个)→ 内存占用大,list() 遍历慢
  • 建议:QPS 限流用 2~5 个窗口,熔断用 60 个窗口(1 分钟)

三、流量控制(限流)

  先说结论:Sentinel 的限流体系有 4 种规则,不是只有 QPS 和线程数两种。

Sentinel 限流体系
├── FlowRule(流量控制规则)       ← 本章 3.1~3.3
│   ├── QPS 限流                  ← "每秒最多多少个"
│   └── 线程数限流                ← "同时最多几个线程"

├── ParamFlowRule(热点参数限流)  ← 本章 3.4
│   └── 按参数值限流              ← "商品ID=1001 限 10 QPS,其他不限"

├── SystemRule(系统自适应限流)   ← 本章 3.5
│   ├── Load 限流                 ← "系统负载 > 1.0 时限流"
│   ├── RT 限流                   ← "平均 RT > 100ms 时限流"
│   ├── 线程数限流                ← "总线程数 > 200 时限流"
│   └── 入口 QPS 限流             ← "总入口 QPS > 1000 时限流"

└── AuthorityRule(授权规则)      ← 本章 3.6
    ├── 白名单                    ← "只允许 app-A 调用"
    └── 黑名单                    ← "禁止 app-B 调用"

四种规则的区别:

规则类型判断维度典型场景
FlowRule针对单个资源的 QPS/线程数"下单接口 QPS 限 100"
ParamFlowRule针对单个资源的某个参数值"秒杀商品 A 限 10,商品 B 不限"
SystemRule针对整个 JVM 的负载/RT/线程"系统整体负载高了就限流"
AuthorityRule针对调用来源的黑白名单"不让爬虫调用"

3.0 先搞清楚:限流是针对接口还是针对资源?

  Sentinel 的限流针对的是"资源",不是"接口"。 一个接口可以定义多个资源,不同资源独立限流,互不影响。

什么是资源?

  资源就是一段你希望被 Sentinel 保护的业务逻辑,用一个字符串命名。一段代码用 SphU.entry("资源名") 包裹起来,它就成为一个被保护资源。资源可以是:

  • 一个 Controller 方法(最常用)
  • 一个 Service 方法
  • 一个数据库操作
  • 一个远程调用
  • 一段任意代码块

具体例子:下单接口里有两个资源

java
@PostMapping("/order/create")
public Result<String> createOrder(OrderDTO dto) {
    // ──────────── 资源 "createOrder" 开始 ────────────
    Entry entry1 = null;
    try {
        entry1 = SphU.entry("createOrder");       // ← 资源名在代码中定义
        // ① 参数校验、计算价格……
        // ② 这段逻辑单独受 "createOrder" 规则保护

        // ──── 资源 "createOrder:db" 开始 ────
        Entry entry2 = null;
        try {
            entry2 = SphU.entry("createOrder:db"); // ← 资源名在代码中定义
            // ③ 写数据库
            orderMapper.insert(order);
            // ④ 这段逻辑单独受 "createOrder:db" 规则保护
        } catch (BlockException e) {
            return Result.fail("系统繁忙,下单失败");
        } finally {
            if (entry2 != null) entry2.exit();
        }
        // ──── 资源 "createOrder:db" 结束 ────

    } catch (BlockException e) {
        return Result.fail("系统繁忙,请稍后重试");
    } finally {
        if (entry1 != null) entry1.exit();
    }
    // ──────────── 资源 "createOrder" 结束 ────────────
    return Result.success("下单成功");
}

资源名在代码中定义,规则在控制台或配置中管理:

┌─────────────────────────────────────────────────────────────────────┐
│                                                                      │
│   代码中                             控制台 / Nacos 配置中            │
│   ┌─────────────────────┐           ┌─────────────────────────────┐  │
│   │ SphU.entry(         │           │ 资源名:createOrder          │  │
│   │   "createOrder"     │───映射───→│ 阈值类型:QPS                │  │
│   │ )                   │           │ 阈值:100                    │  │
│   └─────────────────────┘           │ 流控模式:直接               │  │
│                                     │ 流控效果:快速失败           │  │
│   ┌─────────────────────┐           └─────────────────────────────┘  │
│   │ SphU.entry(         │           ┌─────────────────────────────┐  │
│   │   "createOrder:db"  │───映射───→│ 资源名:createOrder:db       │  │
│   │ )                   │           │ 阈值类型:线程数              │  │
│   └─────────────────────┘           │ 阈值:5                      │  │
│                                     │ 流控效果:排队等待           │  │
│                                     └─────────────────────────────┘  │
│                                                                      │
│  ★ 代码只管"起名字",规则在外部配置                                    │
│  ★ 名字对不上 → 规则不生效,资源不受保护                               │
│  ★ 只配了规则没写代码 → 规则永远不会触发                               │
└─────────────────────────────────────────────────────────────────────┘

配置方式:

java
// 方式一:代码配置(适合固定规则)
@PostConstruct
public void initFlowRules() {
    List<FlowRule> rules = new ArrayList<>();

    FlowRule rule1 = new FlowRule();
    rule1.setResource("createOrder");    // 必须和代码中的名字一模一样
    rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
    rule1.setCount(100);

    FlowRule rule2 = new FlowRule();
    rule2.setResource("createOrder:db"); // 必须和代码中的名字一模一样
    rule2.setGrade(RuleConstant.FLOW_GRADE_THREAD);
    rule2.setCount(5);

    FlowRuleManager.loadRules(rules);
}
json
// 方式二:Nacos 配置(适合动态调整)
[
  {
    "resource": "createOrder",    // 必须和代码中的名字一模一样
    "grade": 1,
    "count": 100,
    "controlBehavior": 0
  },
  {
    "resource": "createOrder:db", // 必须和代码中的名字一模一样
    "grade": 0,
    "count": 5,
    "controlBehavior": 1
  }
]

控制台可视化配置:

Sentinel Dashboard → 流控规则 → 新增流控规则
┌─────────────────────────────────────────┐
│  资源名:  createOrder         ← 输入   │
│  阈值类型: ◎ QPS  ○ 线程数             │
│  阈值:    100                          │
│  流控模式: ◎ 直接  ○ 关联  ○ 链路      │
│  流控效果: ◎ 快速失败  ○ Warm Up  ○ 排队│
└─────────────────────────────────────────┘
  • 代码只管定义资源名SphU.entry("xxx")
  • 规则在控制台/Nacos/代码中配置,通过资源名匹配
  • 名字必须完全一致,多一个空格都匹配不上

一次请求过来,限流判断经历两步:

用户请求 POST /order/create


┌───────────────────────────────────────────────────────────────┐
│  第一步:SphU.entry("createOrder")                            │
│                                                               │
│  Sentinel 查:资源 "createOrder" 的规则是什么?                 │
│            → QPS 限 100,直接模式,快速失败                     │
│                                                               │
│  判断:当前窗口 QPS = 85 < 100                                │
│       → 通过!进入业务逻辑                                     │
│                                                               │
│  ★ 如果 QPS = 120 > 100 → BlockException,直接返回错误         │
│  ★ createOrder:db 根本不会执行,请求在第一关就被拦了            │
└───────────────────────────────────────────────────────────────┘


┌───────────────────────────────────────────────────────────────┐
│  第二步:SphU.entry("createOrder:db")                         │
│                                                               │
│  Sentinel 查:资源 "createOrder:db" 的规则是什么?              │
│            → 线程数限 5,直接模式,排队等待                     │
│                                                               │
│  判断:当前正在执行 createOrder:db 的线程数 = 3 < 5            │
│       → 通过!执行数据库操作                                   │
│                                                               │
│  ★ 如果线程数 = 5 → 排队等待,等前面的线程释放                  │
│  ★ 如果排队超时 → BlockException,返回错误                     │
└───────────────────────────────────────────────────────────────┘


    写数据库成功,返回 "下单成功"

两个资源的统计是独立的:

┌─────────────────────────────────────────────────────────────────┐
│                    滑动窗口统计(各自独立)                       │
│                                                                  │
│  资源 "createOrder"          资源 "createOrder:db"               │
│  ┌──────┬──────┬──────┐     ┌──────┬──────┬──────┐               │
│  │ 窗口1 │ 窗口2 │ 窗口3 │     │ 窗口1 │ 窗口2 │ 窗口3 │               │
│  │ QPS=30│ QPS=40│ QPS=45│     │线程=3 │线程=4 │线程=2 │               │
│  └──────┴──────┴──────┘     └──────┴──────┴──────┘               │
│                                                                  │
│  总 QPS = 30+40+45 = 115      总线程数 = 当前 = 3                │
│  → 超过 100,第 116 个请求     → 未超过 5,继续放行              │
│    会被 createOrder 拒绝                                          │
│                                                                  │
│  ★ 两个资源各自维护自己的 LeapArray,互不干扰                     │
│  ★ QPS 统计的是滑动窗口内的 pass 次数                             │
│  ★ 线程数统计的是当前正在执行 entry() 但未 exit() 的线程数         │
└─────────────────────────────────────────────────────────────────┘

为什么要拆成两个资源?

场景不拆分拆成两个资源
流量暴增整个接口 QPS 被限,连参数校验都进不来参数校验不受限,只限制数据库操作
数据库慢线程池被数据库操作占满,所有请求都超时数据库操作单独限线程数,参数校验照样快
精细控制只能用一种策略外层用 QPS 限流,内层用线程数限流

一句话: 资源就是 Sentinel 保护的最小单元。一次请求经过多个资源时,每个资源独立判断、独立统计、独立限流。请求只要在任何一个资源上被拦截,后续的资源都不会执行。


3.1 流量控制规则(FlowRule)

  最常用的限流规则,针对单个资源,由 3 个维度组合 形成灵活策略:

一条 FlowRule = 阈值类型(2选1) + 流控模式(3选1) + 流控效果(3选1)
                   ↓                    ↓                    ↓
              QPS / 线程数        直接 / 关联 / 链路    快速失败 / Warm Up / 排队等待
维度选项含义一句话
阈值类型QPS每秒请求数"每秒最多放行多少个"
线程数并发执行的线程数"同时最多几个线程在执行"
流控模式直接对当前资源限流"谁超标就限谁"
关联关联资源超限时限制当前资源"A 超标了,限 B"
链路只限制从某个入口来的请求"只限秒杀入口,不限普通入口"
流控效果快速失败超过阈值直接拒绝"满了就拒绝"
Warm Up预热,阈值缓慢上升"刚启动时少放点,慢慢加量"
排队等待超过阈值排队,匀速通过"满了就排队,1 秒内能排到就等"

3.1.1 QPS 限流

QPS 限流原理: 统计每秒通过的请求数,超过阈值就拒绝。

java
@RestController
public class OrderController {

    @GetMapping("/order/create")
    @SentinelResource(
        value = "createOrder",
        blockHandler = "createOrderBlockHandler"
    )
    public Result<String> createOrder() {
        return Result.success("下单成功");
    }

    public Result<String> createOrderBlockHandler(BlockException e) {
        return Result.fail("系统繁忙,请稍后重试");
    }
}

控制台配置规则:

资源名:createOrder
阈值类型:QPS
阈值:100
流控模式:直接
流控效果:快速失败

QPS 限流的底层逻辑:

请求到达 createOrder 资源


FlowSlot 检查

  ├── 获取 createOrder 关联的秒级滑动窗口
  ├── 统计当前窗口的通过 QPS
  ├── QPS >= 100?→ 抛出 FlowException → blockHandler 处理
  └── QPS < 100?→ 放行

3.1.2 线程数限流

原理: 限制同时处理该资源的线程数,超过则排队或拒绝。适用于慢接口,防止线程池耗尽。

java
// 控制台配置
// 资源名:slowQuery
// 阈值类型:线程数
// 阈值:5
// 含义:同一时刻最多只有 5 个线程在执行 slowQuery

QPS vs 线程数限流:

维度QPS 限流线程数限流
统计维度每秒请求数当前正在执行的线程数
适合场景快速接口,QPS 是瓶颈慢接口,线程数是瓶颈
示例下单接口 QPS 100导出接口同时 5 个线程

3.2 三种流控模式

3.2.1 直接模式

对当前资源直接限流,最常用。

请求 → 检查 createOrder 的 QPS → 放行/拒绝

3.2.2 关联模式

场景: 写操作(updateOrder)QPS 过高时,自动限制读操作(queryOrder),让出资源给写操作。

控制台配置:
资源名:queryOrder(被保护的资源)
流控模式:关联
关联资源:updateOrder
阈值:100

含义:当 updateOrder 的 QPS 超过 100 时,限制 queryOrder

为什么需要关联模式? 读写共享同一个数据库连接池。写操作优先级高,写操作压力大时限制读操作,保证写操作不被影响。

3.2.3 链路模式

场景: 同一个方法被多个入口调用,只限制从某个入口进来的请求。

入口 A(秒杀页面) → /order/create ──── 限制 QPS=10
入口 B(普通页面) → /order/create ──── 不限制
java
// 1. 关闭 Sentinel 对 URL 的聚合
spring.cloud.sentinel.web-context-unify=false

// 2. 控制台配置
资源名:createOrder
流控模式:链路
入口资源:seckillPage(秒杀页面的入口资源名)
阈值:10

3.3 三种流控效果

3.3.1 快速失败(默认)

超过阈值直接拒绝,抛出 FlowException

请求 QPS:120
阈值 QPS:100
结果:100 个通过,20 个被拒绝

3.3.2 Warm Up(预热)

原理: 系统刚启动时,阈值从 阈值/冷启动因子 逐渐增加到设定值。防止冷启动时被瞬时流量打垮。

阈值 = 100,预热时长 = 10s,冷启动因子 = 3

时间轴:
0s    → 阈值 = 100/3 = 33
5s    → 阈值 = 66
10s   → 阈值 = 100(稳定)

               ┌────────────────────────────────
               │              ╭─
               │           ╭─╯
               │        ╭─╯
               │     ╭─╯
               │  ╭─╯
               └──╯
              0s                         10s

适用场景: 秒杀系统刚启动,JIT 还没预热,数据库连接池还没建立满,需要逐步增加流量。

java
// 控制台配置
资源名:createOrder
阈值类型:QPS
阈值:100
流控模式:直接
流控效果:Warm Up
预热时长:10(秒)

3.3.3 排队等待

原理: 匀速排队,请求以恒定速率通过。超过排队等待时间的请求被拒绝。类似漏桶算法。

阈值 = 10(每秒通过 10 个,即每 100ms 通过 1 个),超时时间 = 500ms

时间轴(ms):
0ms    → 请求1 通过
50ms   → 请求2 排队(等待 50ms)
100ms  → 请求2 通过
150ms  → 请求3 排队(等待 50ms)
200ms  → 请求3 通过
...
600ms  → 请求7 排队,等待时间超过 500ms → 拒绝

适用场景: 消息队列消费、定时任务,需要匀速处理,不能瞬间处理完。

java
// 控制台配置
资源名:handleMessage
阈值类型:QPS
阈值:10
流控模式:直接
流控效果:排队等待
超时时间:500(毫秒)

3.5 代码方式配置限流规则

除了控制台,也可以直接在代码中加载规则:

java
@Component
public class SentinelRuleConfig {

    @PostConstruct
    public void initFlowRules() {
        List<FlowRule> rules = new ArrayList<>();

        // 规则1:QPS 限流
        FlowRule rule1 = new FlowRule();
        rule1.setResource("createOrder");
        rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);  // QPS 类型
        rule1.setCount(100);                           // 阈值 100
        rules.add(rule1);

        // 规则2:线程数限流
        FlowRule rule2 = new FlowRule();
        rule2.setResource("exportData");
        rule2.setGrade(RuleConstant.FLOW_GRADE_THREAD);  // 线程数类型
        rule2.setCount(5);                                // 最多 5 个线程
        rules.add(rule2);

        // 规则3:关联模式
        FlowRule rule3 = new FlowRule();
        rule3.setResource("queryOrder");
        rule3.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule3.setCount(100);
        rule3.setStrategy(RuleConstant.STRATEGY_RELATE);  // 关联模式
        rule3.setRefResource("updateOrder");               // 关联资源
        rules.add(rule3);

        // 规则4:Warm Up
        FlowRule rule4 = new FlowRule();
        rule4.setResource("seckill");
        rule4.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule4.setCount(200);
        rule4.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
        rule4.setWarmUpPeriodSec(10);  // 预热 10 秒
        rules.add(rule4);

        // 规则5:排队等待
        FlowRule rule5 = new FlowRule();
        rule5.setResource("handleMessage");
        rule5.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule5.setCount(10);
        rule5.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER);
        rule5.setMaxQueueingTimeMs(500);  // 排队超时 500ms
        rules.add(rule5);

        FlowRuleManager.loadRules(rules);
    }
}

3.4 热点参数限流(ParamFlowRule)

  FlowRule 的局限: 假设创建订单接口 QPS 限 100,它的粒度是"整个接口"。但实际场景中,对秒杀商品 A 和普通商品 B 的调用频率天差地别——需要按参数值分别限流。

                    ┌─────────────────────────────────┐
                    │  POST /order/create              │
                    │  FlowRule: QPS 限 100(全局)     │
                    │                                  │
                    │  ┌─────────────────────────────┐ │
                    │  │  goodsId = 10001(秒杀商品)  │ │
                    │  │  ParamFlowRule: QPS 限 10    │ │  ← 热点参数单独限
                    │  └─────────────────────────────┘ │
                    │                                  │
                    │  ┌─────────────────────────────┐ │
                    │  │  goodsId = 20002(普通商品)  │ │
                    │  │  不限流(走全局 FlowRule)     │ │  ← 普通参数不限制
                    │  └─────────────────────────────┘ │
                    └─────────────────────────────────┘

核心概念: 统计参数值相同的请求,对"热点"参数值单独限流,非热点参数不受影响。

java
@GetMapping("/order/create")
@SentinelResource(value = "createOrder", blockHandler = "blockHandler")
public Result<String> createOrder(@RequestParam Long goodsId) {
    // goodsId=10001(秒杀商品)→ 限 10 QPS
    // goodsId=20002(普通商品)→ 不限
    return Result.success("下单成功");
}
java
// 代码配置热点参数限流规则
@PostConstruct
public void initParamFlowRule() {
    List<ParamFlowRule> rules = new ArrayList<>();

    ParamFlowRule rule = new ParamFlowRule();
    rule.setResource("createOrder");     // 资源名
    rule.setParamIdx(0);                 // 第 0 个参数(goodsId)
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    rule.setCount(10);                   // 全局阈值 10 QPS

    // 对特定参数值设置不同阈值
    ParamFlowItem item = new ParamFlowItem();
    item.setObject("10001");             // goodsId=10001
    item.setCount(1);                    // 限 1 QPS(秒杀极低限制)
    rule.setParamFlowItemList(List.of(item));

    ParamFlowRuleManager.loadRules(rules);
}

更多细节见 第六章:热点参数限流


3.5 系统自适应限流(SystemRule)

  FlowRule 和 ParamFlowRule 的局限: 它们都是针对"某个资源"的限流,看的是 QPS/线程数。但当系统整体负载飙升时(比如 CPU 飙到 90%),即使每个资源单看都没超阈值,整个系统已经快挂了——需要从系统层面自适应限流。

SystemRule 的 4 种判断维度:

  ┌──────────────────────────────────────────────────────┐
  │                  JVM 进程                             │
  │                                                      │
  │  CPU 使用率 ──────→ LOAD 限流(系统负载 > 阈值)       │
  │  所有接口 RT ─────→ RT 限流(平均 RT > 阈值)          │
  │  Tomcat 线程数 ───→ 线程数限流(总线程 > 阈值)         │
  │  入口总 QPS ─────→ 入口 QPS 限流(总 QPS > 阈值)      │
  └──────────────────────────────────────────────────────┘
规则类型指标含义示例
LOAD系统负载load > 1.0 时触发只适用于 Linux/Unix
RT平均响应时间所有入口平均 RT > 阈值avgRT > 100ms
线程数使用中的线程数所有入口线程数 > 阈值threadCount > 200
入口 QPS总入口 QPS所有入口 QPS > 阈值totalQPS > 1000

与 FlowRule 的区别: FlowRule 按资源名限流("下单接口 QPS 限 100"),SystemRule 按整个 JVM 进程限流("整个系统负载超 1.0 时,所有入口都限流")。两者可以同时生效,SystemRule 是最后一道防线。

java
@PostConstruct
public void initSystemRule() {
    List<SystemRule> rules = new ArrayList<>();

    SystemRule loadRule = new SystemRule();
    loadRule.setHighestSystemLoad(1.0);  // 系统负载 > 1.0 时限流
    rules.add(loadRule);

    SystemRule rtRule = new SystemRule();
    rtRule.setAvgRt(100);               // 平均 RT > 100ms 时限流
    rules.add(rtRule);

    SystemRuleManager.loadRules(rules);
}

更多细节见 第七章:系统自适应保护


3.6 授权规则(AuthorityRule)

  限制调用来源,本质是黑白名单。在微服务场景中,根据调用方应用名(origin)进行访问控制。

AuthorityRule 两种模式:

  白名单:只允许指定的调用方访问
  ┌─────────────────────────────────┐
  │  app-A ──→ 允许                  │
  │  app-B ──→ 允许                  │
  │  app-C ──→ 拒绝(不在白名单)      │
  └─────────────────────────────────┘

  黑名单:禁止指定的调用方访问
  ┌─────────────────────────────────┐
  │  app-A ──→ 允许                  │
  │  app-B ──→ 拒绝(在黑名单)        │
  │  app-C ──→ 允许                  │
  └─────────────────────────────────┘
java
@PostConstruct
public void initAuthorityRule() {
    List<AuthorityRule> rules = new ArrayList<>();

    AuthorityRule rule = new AuthorityRule();
    rule.setResource("createOrder");
    rule.setStrategy(RuleConstant.AUTHORITY_WHITE);  // 白名单模式
    rule.setLimitApp("app-A,app-B");                 // 只允许 app-A 和 app-B

    AuthorityRuleManager.loadRules(rules);
}

// 调用方需要设置 origin,否则 Sentinel 不知道调用者是谁
// 方式一:通过 RequestOriginParser
@Component
public class MyRequestOriginParser implements RequestOriginParser {
    @Override
    public String parseOrigin(HttpServletRequest request) {
        return request.getHeader("X-App-Name");  // 从请求头获取调用方
    }
}

3.7 四种规则总结

限流体系全景:

  请求进来

    ├── AuthorityRule  ← 第一关:你是谁?(黑白名单)
    │   ├── 白名单放行
    │   └── 黑名单拒绝

    ├── SystemRule     ← 第二关:系统还能扛住吗?(整体负载)
    │   ├── LOAD/RT/线程数/入口QPS
    │   └── 超过 → 全局限流

    ├── ParamFlowRule  ← 第三关:这个参数值特殊吗?(热点参数)
    │   └── goodsId=10001 → 单独限 1 QPS

    └── FlowRule       ← 第四关:这个接口超限了吗?(单资源)
        ├── QPS / 线程数
        └── 直接 / 关联 / 链路

一句话总结: FlowRule 是"单资源限流"(最常用),ParamFlowRule 是"按参数值限流"(秒杀场景),SystemRule 是"系统级自适应限流"(最后防线),AuthorityRule 是"黑白名单"(来源控制)。四种规则组合使用,实现从粗到细、从外到内的多层防护。


四、限流 vs 熔断:先搞清楚两者的区别

  很多人把限流和熔断混为一谈,但它们解决的是完全不同的问题,配置也完全不同

4.0 一句话区分

限流(Flow Control)熔断(Circuit Breaking)
核心问题流量太大,扛不住下游挂了,别白费力气
触发条件QPS/线程数超过阈值请求失败率/慢调用比例超过阈值
判断依据统计"通过的请求数"统计"失败的请求数"
动作拒绝超额请求拒绝所有请求一段时间
恢复方式下一秒 QPS 降下来自动恢复熔断时长结束后,试探性放行
保护对象保护自己(不被流量打垮)保护自己(不被下游拖死)

4.0.1 用生活例子理解

限流 = 餐厅限流
─────────────────
餐厅只有 10 张桌子(线程池 10 个线程)。
门口放一个取号机,里面最多容纳 10 桌客人。
第 11 个客人来了 → 取号机显示"已满" → 拒绝入场。

★ 只要里面有客人吃完出来,下一个客人就能进去。
★ 餐厅本身没坏,只是容量有限。


熔断 = 电路保险丝
─────────────────
你家电饭煲短路了,一插上就跳闸。
保险丝熔断 → 整条电路断电 → 保护整栋房子不着火。

★ 不是电路容量不够,是用电器坏了。
★ 保险丝不会自动恢复,需要你拔掉坏电器、手动合闸。
★ 合闸后如果没有短路,电路恢复正常。

4.0.2 用微服务场景理解

场景:订单服务 → 调用 → 库存服务

限流场景:
  订单服务 QPS = 500,库存服务 QPS = 100
  库存服务没挂,只是处理不过来
  → Sentinel 在库存服务入口限制 QPS=100
  → 100 个请求正常处理,400 个被拒绝
  → 库存服务自身稳定,不会崩溃

熔断场景:
  库存服务挂了(数据库连接断开、OOM、死锁……)
  订单服务每次调用库存服务都超时,线程池快满了
  → Sentinel 发现库存服务异常率 > 50%
  → 熔断器打开,之后 10 秒内订单服务直接拒绝调用库存
  → 订单服务的线程被释放,不会跟着一起挂

4.0.3 核心区别对照表

维度限流熔断
统计指标pass 数(通过数)exception 数(异常数)+ RT
触发逻辑pass > threshold → 拒绝exceptionRate > threshold → 熔断
状态无状态,每次请求独立判断有状态(CLOSED / OPEN / HALF_OPEN)
计数窗口滑动窗口(默认 1 秒)滑动窗口(可配置,如 10 秒)
恢复机制下一秒窗口滚动,QPS 自然下降熔断时间到 → HALF_OPEN → 探测 → 成功则 CLOSED
拒绝时长瞬时,窗口滚动后即可恢复持久,必须等熔断时长结束
配置粒度按资源(createOrder按资源(createOrder
配置项grade, count, controlBehavior, strategygrade, count, timeWindow, minRequestAmount, statIntervalMs

4.0.4 配置完全不一样

限流规则(FlowRule):

java
FlowRule rule = new FlowRule();
rule.setResource("createOrder");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);     // 阈值类型:QPS
rule.setCount(100);                              // 阈值为 100
rule.setControlBehavior(0);                      // 效果:快速失败
rule.setStrategy(0);                             // 模式:直接
FlowRuleManager.loadRules(List.of(rule));

熔断规则(DegradeRule):

java
DegradeRule rule = new DegradeRule();
rule.setResource("createOrder");
rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO); // 策略:异常比例
rule.setCount(0.5);                               // 阈值:异常比例 > 50%
rule.setTimeWindow(10);                           // 熔断时长:10 秒
rule.setMinRequestAmount(5);                      // 最小请求数:5
rule.setStatIntervalMs(1000);                     // 统计窗口:1 秒
DegradeRuleManager.loadRules(List.of(rule));

配置项对比:

配置项FlowRule(限流)DegradeRule(熔断)
gradeQPS / 线程数慢调用比例 / 异常比例 / 异常数
count100(个)0.5(50%)
timeWindow10(秒)
controlBehavior快速失败 / Warm Up / 排队
strategy直接 / 关联 / 链路
minRequestAmount5(最小请求数)
statIntervalMs无(默认 1s)可配置

4.0.5 两者可以同时生效

  同一个资源可以同时配置限流规则和熔断规则,它们独立判断、互不影响:

请求 → createOrder 资源

         ├── FlowSlot 检查:QPS >= 100?→ 限流拒绝

         ├── DegradeSlot 检查:异常率 > 50%?→ 熔断拒绝

         └── 两者都通过 → 执行业务逻辑

经典组合:

策略规则作用
限流QPS=100正常流量保护,超过 100 就拒绝
熔断异常比例 > 50%下游故障保护,错误率太高就熔断

限流防的是"流量太大",熔断防的是"下游挂了"。限流在门口数人头,熔断看调用结果决定是否拉闸。两者配置完全不同,但可以同时作用于同一个资源。


五、熔断降级

5.0 熔断的核心原理

  熔断这个名字来自电路里的保险丝——电流过大时保险丝熔断,切断电路,保护电器不烧毁。Sentinel 的熔断同理:下游服务出问题时,断开调用链路,保护调用方不跟着一起崩溃。

核心问题:为什么要主动断开?

  假设订单服务调用库存服务,库存服务 1 秒超时。如果库存服务挂了,每次调用都要等 1 秒才能拿到超时结果。1000 个并发请求,1000 个线程都在干等——这就是线程资源被无效等待耗尽。雪崩就是这样发生的:一个服务挂了,拖死所有依赖它的服务。

没有熔断器(库存服务挂了):
──────────────────────────────────────────────────
时间线:  0s        1s        2s        3s
         │         │         │         │
请求1 ──►│ 等待……  │ 超时    │         │    ← 线程占用 1 秒
请求2 ──►│ 等待……  │ 超时    │         │    ← 线程占用 1 秒
请求3 ──►│ 等待……  │ 超时    │         │    ← 线程占用 1 秒
 ...     │         │         │         │
请求N ──►│ 等待……  │ 超时    │         │    ← 全部线程在等,新请求进不来

结果:1000 个线程全被占满,订单服务 → 雪崩


有熔断器(库存服务挂了,熔断器 OPEN):
──────────────────────────────────────────────────
时间线:  0s        1s        2s        3s
         │         │         │         │
请求1 ──►│ 超时     │         │         │    ← 触发熔断,熔断器 OPEN
请求2 ──►│ 立即拒绝 │         │         │    ← 0ms 返回,线程马上释放
请求3 ──►│ 立即拒绝 │         │         │    ← 0ms 返回
 ...     │         │         │         │
请求N ──►│ 立即拒绝 │         │         │    ← 所有请求立即失败,线程不占

结果:线程全部释放,订单服务正常运行,只是下单功能降级

熔断器回答三个问题:

┌──────────────────────────────────────────────────────────────┐
│                                                              │
│  问题一:什么时候断开?                                       │
│  → 统计窗口内,失败率 / 慢调用比例超过阈值                     │
│  → 状态:CLOSED → OPEN                                       │
│                                                              │
│  问题二:断开多久?                                           │
│  → 熔断时长(timeWindow),如 10 秒                           │
│  → 状态:OPEN →(计时结束)→ HALF_OPEN                        │
│                                                              │
│  问题三:怎么恢复?                                           │
│  → HALF_OPEN 状态放行一个探测请求,成功则恢复,失败则继续熔断    │
│  → 状态:HALF_OPEN → CLOSED 或 OPEN                          │
│                                                              │
└──────────────────────────────────────────────────────────────┘

HALF_OPEN 是熔断器最精妙的设计:

  如果没有 HALF_OPEN,只有两种选择:① 熔断后永远不恢复 → 服务不可用;② 时间一到立刻恢复 → 如果下游还没好,请求再次失败,再次熔断,反复震荡。HALF_OPEN 用一个探测请求解决了这个问题:放一个请求试试水,成功就恢复,失败就继续熔断。

                    ┌──────────────┐
          ┌────────►│    CLOSED    │◄─────────┐
          │         │   正常放行    │           │
          │         └──────┬───────┘           │
          │                │                   │
          │                │ 失败率 > 阈值      │
          │                ▼                   │
          │         ┌──────────────┐           │
          │         │     OPEN     │           │
          │         │   全部拒绝    │           │
          │         └──────┬───────┘           │
          │                │                   │
          │                │ 熔断时间到          │
          │                ▼                   │
          │         ┌──────────────┐           │
          │         │  HALF_OPEN   │           │
          │         │  放 1 个探测  │           │
          │         └──────┬───────┘           │
          │                │                   │
          │    ┌───────────┴───────────┐       │
          │    │ 探测成功               │ 探测失败│
          │    ▼                       ▼       │
          │  CLOSED                   OPEN     │
          └────────────────────────────────────┘

核心公式:

熔断器状态 = f(统计窗口内的失败统计, 熔断时长, 探测结果)

其中:
  统计窗口 = 滑动窗口(如 10 秒内 100 个请求)
  统计指标 = 慢调用比例 / 异常比例 / 异常数
  熔断时长 = 10 秒(可配置)
  探测逻辑 = 放行 1 个请求,根据结果决定下一步状态

熔断 vs 降级:"断"是手段,"降"是目的:

概念谁触发干什么代码体现
熔断Sentinel 自动打开断路器,拒绝调用Sentinel 状态机
降级开发者预设返回兜底数据fallback / blockHandler

  熔断是自动的、被动的——Sentinel 发现下游不行了,断开调用。降级是手动的、预设的——你提前写好"如果熔断了,返回什么"。熔断发信号,降级出预案。 熔断器打开后,Sentinel 抛 DegradeException,你的降级方法接管,返回兜底数据。

关键误区:熔断不是服务级别,是资源级别!

  很多人以为"熔断了,整个服务就不能用了",这是错的。Sentinel 的熔断粒度是资源,不是服务。一个服务里通常有多个资源,每个资源独立熔断,互不影响。

订单服务(order-service)

├── 资源 "createOrder"     → 调用库存服务 ──► 库存挂了 → 熔断!
│   └── 之后请求 createOrder → 立即拒绝,返回兜底

├── 资源 "getOrder"        → 调用订单数据库 ──► 数据库正常 → 不熔断!
│   └── 之后请求 getOrder → 正常返回,完全不受影响

└── 资源 "sendSms"         → 调用短信服务 ──► 短信正常 → 不熔断!
    └── 之后请求 sendSms → 正常发送,完全不受影响

实际场景:

用户打开订单详情页:
  ├── GET /order/123          → 调用 getOrder → 正常返回 ✅
  │                               (订单数据库没坏,这个资源没熔断)

  └── POST /order/create      → 调用 createOrder → 返回"下单失败" ❌
                                  (库存服务挂了,这个资源被熔断了)

页面效果:
  订单详情正常显示,但点击"下单"按钮提示"系统繁忙,请稍后重试"
  而不是整个页面 500

三个资源,三个独立的熔断器:

资源依赖状态熔断器用户看到
getOrder订单数据库正常CLOSED订单详情正常
createOrder库存服务挂了OPEN"下单失败,请稍后重试"
sendSms短信服务正常CLOSED短信验证码正常发送

这与服务熔断的区别:

服务级别熔断(不是 Sentinel 的做法):
  "库存服务挂了 → 整个订单服务都熔断 → 所有功能不可用"
  ❌ 粒度太粗,一个下游挂了,整个上游跟着瘫痪


资源级别熔断(Sentinel 的做法):
  "库存服务挂了 → 只熔断 createOrder 这个资源 → 其他功能正常"
  ✅ 粒度精确,只影响有问题的调用链路

一句话: 熔断是"精准打击"不是"全城轰炸"。哪个资源调用出问题,就熔断哪个资源,其他资源不受牵连。服务的整体可用性不受影响,只是部分功能短暂降级。

每个资源一个熔断器——但熔断器不是线程!

  每个资源都有自己独立的熔断器,但熔断器本身只是一个数据结构,不是线程,不消耗线程资源。

一个资源 = 一个熔断器 = 内存中的几个变量:

┌──────────────────────────────────────────────────┐
│ 资源 "createOrder" 的熔断器                        │
│                                                   │
│   state:       CLOSED / OPEN / HALF_OPEN           │
│   failureCount: 15      ← 滑动窗口内的失败次数      │
│   successCount: 85      ← 滑动窗口内的成功次数      │
│   lastOpenTime: 0       ← 上次熔断时间              │
│   timeWindow:   10      ← 熔断时长(秒)            │
│   count:        0.5     ← 阈值(50%)              │
│                                                   │
│   消耗:约 50 字节内存,0 个线程                     │
│   检查:请求线程同步执行,无上下文切换                │
└──────────────────────────────────────────────────┘

判断过程是同步的,不是异步的:

请求线程(如 Tomcat 线程)进入 Slot Chain:

  Request Thread


  DegradeSlot.entry()  ──────► 读熔断器状态
       │                            │
       │                   state == OPEN?
       │                     ├── 是 → 抛 DegradeException,不调用下游
       │                     └── 否 → 放行,继续下一个 Slot


  执行业务逻辑 → 调用下游 → 成功 / 失败


  DegradeSlot.exit()  ──────► 更新熔断器统计
       │                            │
       │                    failureCount++ / successCount++
       │                    判断是否触发熔断 → 更新 state


  返回结果

★ 整个过程都在请求线程中完成,没有额外线程
★ 检查和更新都是 O(1) 的变量读写,CPU 开销可忽略

10 个资源 = 10 个熔断器 = 10 组变量,不是 10 个线程:

┌─────────────────────────────────────────────────┐
│  资源             熔断器状态        内存   线程  │
│─────────────────────────────────────────────────│
│  createOrder  →  {state=OPEN,  count=15, ...}  │  50B    0
│  getOrder     →  {state=CLOSED,count=0,  ...}  │  50B    0
│  sendSms      →  {state=CLOSED,count=0,  ...}  │  50B    0
│  queryStock   →  {state=CLOSED,count=2,  ...}  │  50B    0
│  ...          →  ...                            │  ...    ...
│─────────────────────────────────────────────────│
│  合计:100 个资源 = 100 组变量 ≈ 5KB 内存,0 线程│
└─────────────────────────────────────────────────┘

什么情况下会有额外线程?

  熔断器本身没有线程,但 Sentinel 有一个后台统计线程负责滑动窗口的定时滚动。这个线程是全局共享的,所有资源的滑动窗口共用这一个线程,不是每个资源一个线程。

Sentinel 全局只有一个后台统计线程:
  ┌──────────────────────────────────────────────┐
  │  Sentinel 统计线程(唯一,全局共享)            │
  │                                               │
  │  每隔 1 秒:                                   │
  │    ├── 滚动所有资源的滑动窗口                   │
  │    ├── 清理过期数据                             │
  │    └── 上报监控指标                             │
  │                                               │
  │  100 个资源共用一个统计线程,                   │
  │  不是 100 个线程                                │
  └──────────────────────────────────────────────┘

一句话: 熔断器是"轻量级状态机",存在内存里,请求线程同步检查,极低开销。一个资源一个熔断器,但不是一个资源一个线程。

熔断器如何管理?——DegradeRuleManager 是"管家"

  熔断器本身只是变量,需要一个管理者来创建、查找、加载规则。Sentinel 中这个管理者是 DegradeRuleManager

┌──────────────────────────────────────────────────────────────────┐
│                      DegradeRuleManager                          │
│                                                                  │
│  职责一:存储规则                                                  │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │  Map<String, List<DegradeRule>> ruleMap                     │ │
│  │                                                             │ │
│  │  "createOrder" → [DegradeRule{grade=ERROR_RATIO, count=0.5}]│ │
│  │  "getOrder"    → [DegradeRule{grade=SLOW_REQUEST, count=200}]│ │
│  │  "sendSms"     → [DegradeRule{grade=ERROR_COUNT, count=10}] │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                                                                  │
│  职责二:存储熔断器实例                                            │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │  Map<String, CircuitBreaker> circuitBreakerMap              │ │
│  │                                                             │ │
│  │  "createOrder" → ExceptionCircuitBreaker{state=OPEN, ...}   │ │
│  │  "getOrder"    → ResponseTimeCircuitBreaker{state=CLOSED}   │ │
│  │  "sendSms"     → ExceptionCircuitBreaker{state=CLOSED}      │ │
│  └─────────────────────────────────────────────────────────────┘ │
│                                                                  │
│  职责三:加载规则                                                  │
│  public static void loadRules(List<DegradeRule> rules) {         │
│      清空旧规则 → 按资源名分组 → 创建/更新 CircuitBreaker 实例      │
│  }                                                               │
└──────────────────────────────────────────────────────────────────┘

规则和熔断器的关系:

规则(DegradeRule)          熔断器(CircuitBreaker)
─────────────────────       ─────────────────────────
"配置,告诉你怎么熔断"        "状态机,执行熔断逻辑"

DegradeRule                 CircuitBreaker
  ├── resource: "createOrder"    ├── state: CLOSED/OPEN/HALF_OPEN
  ├── grade: ERROR_RATIO         ├── failureCount: 15
  ├── count: 0.5                 ├── successCount: 85
  ├── timeWindow: 10             ├── lastOpenTime: 0
  └── minRequestAmount: 5        └── 来自 rule 的配置

规则加载时:
  loadRules(List<DegradeRule>)
    → 遍历每条规则
    → 根据 grade 创建对应的 CircuitBreaker 实例
    → 存入 circuitBreakerMap

一次请求的完整查找链路:

请求 → 资源 "createOrder"


DegradeSlot.entry("createOrder")

  ├── ① 从 DegradeRuleManager.getCircuitBreaker("createOrder")
  │      → 从 circuitBreakerMap 中获取熔断器实例

  ├── ② 调用 circuitBreaker.tryPass()
  │      → 读 state:OPEN → 拒绝;CLOSED → 放行

  ├── ③ 请求执行完成

  └── ④ DegradeSlot.exit("createOrder")
        → 调用 circuitBreaker.onRequestComplete(rt, error)
        → 更新统计:successCount++ / failureCount++
        → 判断是否触发熔断 → 更新 state

规则的动态加载:

方式一:代码加载
  DegradeRuleManager.loadRules(rules);
  → 内部清空 circuitBreakerMap,重新创建所有熔断器

方式二:Nacos 动态配置
  Nacos 配置变更 → Sentinel 监听器 → loadRules()
  → 无需重启,实时生效

方式三:Sentinel 控制台
  控制台下发规则 → Sentinel API → loadRules()
  → 实时生效,但默认不持久化

一句话: DegradeRuleManager 是熔断器的管家。它维护一个 Map<资源名, CircuitBreaker>,规则加载时创建熔断器实例,请求时按资源名查找,规则变更时重建。你只管配规则,管家负责创建和管理熔断器。

配置实例:每条规则只针对一个资源

  熔断配置没有"服务级别"的写法,每条规则必须指定 resource 字段,精确到具体资源名。

java
// 每条规则绑定一个资源,没有"全局熔断"配置
DegradeRuleManager.loadRules(List.of(
    // 规则一:只对 createOrder 生效
    new DegradeRule("createOrder")
        .setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType())
        .setCount(0.5)
        .setTimeWindow(10),

    // 规则二:只对 getOrder 生效
    new DegradeRule("getOrder")
        .setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())
        .setCount(200)
        .setTimeWindow(5),

    // 规则三:只对 sendSms 生效
    new DegradeRule("sendSms")
        .setGrade(CircuitBreakerStrategy.ERROR_COUNT.getType())
        .setCount(10)
        .setTimeWindow(30)
));
配置效果:
┌────────────────────────────────────────────────────────────────┐
│                                                                │
│  createOrder → 异常比例 > 50% → 熔断 10 秒                      │
│  getOrder    → 慢调用比例 > 50%(RT>200ms)→ 熔断 5 秒          │
│  sendSms     → 1 分钟内异常 > 10 个 → 熔断 30 秒               │
│                                                                │
│  三个资源,三种策略,三种阈值,互不干扰                           │
│                                                                │
│  没有配置的资源(如 queryLog)→ 不受保护,永远不会熔断            │
│                                                                │
└────────────────────────────────────────────────────────────────┘

为什么不做"整个服务熔断"?

如果做服务级别熔断:
  "库存服务挂了 → 订单服务整个熔断 → 所有接口 502"
  ❌ 一刀切:一个下游挂了,整个上游不可用
  ❌ 无法区分:导出接口慢(应该慢调用熔断)和下单接口报错(应该异常熔断)

如果做资源级别熔断(Sentinel 的做法):
  "库存服务挂了 → 只熔断 createOrder → getOrder 正常"
  ✅ 精准控制:只影响有问题的调用链路
  ✅ 差异化策略:每个资源根据自身特点配置不同策略

一句话: 熔断配置永远绑定资源名,不存在"配置一个规则,熔断整个服务"的写法。你控制的是"哪个资源在什么条件下断开",而不是"哪个服务整个下线"。

5.1 三种熔断策略详解

5.1.1 慢调用比例(SLOW_REQUEST_RATIO)

原理: 统计时间窗口内,慢调用(RT 超过设定阈值)的比例。超过比例则熔断。

条件:1 秒内发起 10 个请求
最大 RT:200ms(超过 200ms 算慢调用)
慢调用比例阈值:0.5(50%)
熔断时长:10 秒
最小请求数:5

场景:10 个请求中,6 个 RT > 200ms(60%)
     慢调用比例 60% > 阈值 50% → 触发熔断,10 秒内拒绝所有请求
java
// 代码配置
DegradeRule rule = new DegradeRule();
rule.setResource("slowService");
rule.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType());
rule.setCount(200);       // 最大 RT 200ms
rule.setSlowRatioThreshold(0.5);  // 慢调用比例阈值 50%
rule.setTimeWindow(10);   // 熔断时长 10 秒
rule.setMinRequestAmount(5);  // 最小请求数 5

适用场景: 下游服务响应变慢,但还没报错。比如数据库慢查询、第三方接口超时。

5.1.2 异常比例(ERROR_RATIO)

原理: 统计时间窗口内,异常比例超过阈值则熔断。

条件:1 秒内发起 10 个请求
异常比例阈值:0.5(50%)
熔断时长:10 秒
最小请求数:5

场景:10 个请求中,6 个异常(60%)
     异常比例 60% > 阈值 50% → 触发熔断
java
DegradeRule rule = new DegradeRule();
rule.setResource("errorService");
rule.setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType());
rule.setCount(0.5);        // 异常比例阈值 50%
rule.setTimeWindow(10);    // 熔断时长 10 秒
rule.setMinRequestAmount(5);

5.1.3 异常数(ERROR_COUNT)

原理: 统计时间窗口内,异常数量超过阈值则熔断。与异常比例不同,这里用绝对值。

条件:1 分钟内异常数超过 10 个
熔断时长:10 秒

场景:1 分钟内发生了 11 个异常
     11 > 10 → 触发熔断
java
DegradeRule rule = new DegradeRule();
rule.setResource("errorCountService");
rule.setGrade(CircuitBreakerStrategy.ERROR_COUNT.getType());
rule.setCount(10);          // 异常数阈值
rule.setTimeWindow(10);     // 熔断时长 10 秒
rule.setMinRequestAmount(5);
rule.setStatIntervalMs(60000);  // 统计窗口 1 分钟

异常比例 vs 异常数:

维度异常比例异常数
适用场景流量波动大流量稳定
低流量不适用(1 个异常就 100%)适用
高流量适用容易误触发

5.2 熔断的 HALF_OPEN 探测逻辑

熔断器进入 HALF_OPEN 后,不是一次性恢复所有请求,而是逐步放行

HALF_OPEN 状态(最多允许 1 个探测请求):

请求1 → 通过(探测请求,开始执行)
请求2 → 等待(请求1 还没返回结果)
请求3 → 等待

请求1 返回结果:
  ├── 成功 → 熔断器关闭(CLOSED),请求2、3 放行
  └── 失败 → 熔断器重新打开(OPEN),请求2、3 拒绝

5.3 Fallback 降级处理

java
@RestController
public class OrderController {

    @GetMapping("/order/{id}")
    @SentinelResource(
        value = "getOrder",
        fallback = "getOrderFallback",           // 业务异常时降级
        fallbackClass = OrderFallbackHandler.class,
        blockHandler = "getOrderBlockHandler",    // 限流/熔断时降级
        blockHandlerClass = OrderBlockHandler.class
    )
    public Result<Order> getOrder(@PathVariable Long id) {
        // 可能抛出 RuntimeException 的业务逻辑
        return Result.success(orderService.getById(id));
    }

    // 当前类的降级方法
    public Result<Order> getOrderFallback(Long id, Throwable e) {
        log.error("getOrder 业务异常,id={}", id, e);
        return Result.fail("查询订单失败");
    }
}

// 降级处理类(fallbackClass)
public class OrderFallbackHandler {
    public static Result<Order> getOrderFallback(Long id, Throwable e) {
        log.error("getOrder fallback", e);
        return Result.fail("订单服务暂时不可用");
    }
}

// 限流/熔断处理类(blockHandlerClass)
public class OrderBlockHandler {
    public static Result<Order> getOrderBlockHandler(Long id, BlockException e) {
        return Result.fail("系统繁忙,请稍后重试");
    }
}

fallback vs blockHandler 的区别:

维度fallbackblockHandler
触发条件业务异常(RuntimeException)限流/熔断(BlockException)
参数原参数 + Throwable原参数 + BlockException
用途处理业务错误处理流量控制
是否可以共存可以可以

fallback 和 blockHandler 同时配置时的执行逻辑:

请求到达

  ├── 被限流/熔断?→ blockHandler 处理 → 返回

  └── 执行业务逻辑

        ├── 成功 → 返回
        └── 抛出异常

              ├── BlockException → 不触发 fallback(已被 blockHandler 处理)
              └── 其他异常 → fallback 处理

5.4 熔断规则代码配置

java
@Component
public class DegradeRuleConfig {

    @PostConstruct
    public void initDegradeRules() {
        List<DegradeRule> rules = new ArrayList<>();

        // 慢调用比例熔断
        DegradeRule slowRule = new DegradeRule("slowService")
                .setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())
                .setCount(200)          // 最大 RT 200ms
                .setSlowRatioThreshold(0.5)  // 慢调用比例 50%
                .setTimeWindow(10)      // 熔断 10 秒
                .setMinRequestAmount(5) // 最小请求数
                .setStatIntervalMs(1000);    // 统计窗口 1 秒
        rules.add(slowRule);

        // 异常比例熔断
        DegradeRule errorRatioRule = new DegradeRule("errorService")
                .setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType())
                .setCount(0.5)          // 异常比例 50%
                .setTimeWindow(10)
                .setMinRequestAmount(5)
                .setStatIntervalMs(1000);
        rules.add(errorRatioRule);

        // 异常数熔断
        DegradeRule errorCountRule = new DegradeRule("errorCountService")
                .setGrade(CircuitBreakerStrategy.ERROR_COUNT.getType())
                .setCount(10)           // 异常数 10
                .setTimeWindow(10)
                .setMinRequestAmount(5)
                .setStatIntervalMs(60000);  // 统计窗口 1 分钟
        rules.add(errorCountRule);

        DegradeRuleManager.loadRules(rules);
    }
}

5.5 限流 + 熔断实战示例

  下面是一个完整的下单服务示例,同时配置了限流和熔断,展示两者如何配合工作。

场景设定:

订单服务(order-service)
├── POST /order/create  → 资源 "createOrder"  → 调用库存服务
└── GET  /order/{id}    → 资源 "getOrder"     → 查询订单数据库

要求:
  资源 createOrder:
    限流:QPS ≤ 100,超过直接拒绝
    熔断:异常比例 > 50% 时熔断 10 秒

  资源 getOrder:
    限流:QPS ≤ 500,超过直接拒绝
    熔断:不配置(查询接口不会雪崩,不需要熔断)

步骤一:Controller 代码

java
@RestController
public class OrderController {

    @PostMapping("/order/create")
    @SentinelResource(
        value = "createOrder",
        fallback = "createOrderFallback",
        blockHandler = "createOrderBlockHandler"
    )
    public Result<String> createOrder(@RequestBody OrderDTO dto) {
        // 调用库存服务(可能失败)
        inventoryClient.deductStock(dto.getProductId(), dto.getQuantity());
        // 写订单数据库
        orderMapper.insert(dto);
        return Result.success("下单成功");
    }

    @GetMapping("/order/{id}")
    @SentinelResource(
        value = "getOrder",
        fallback = "getOrderFallback",
        blockHandler = "getOrderBlockHandler"
    )
    public Result<Order> getOrder(@PathVariable Long id) {
        return Result.success(orderMapper.selectById(id));
    }

    // ──────────── 降级方法 ────────────

    // 业务异常降级(如库存不足)
    public Result<String> createOrderFallback(OrderDTO dto, Throwable e) {
        log.error("createOrder 业务异常", e);
        return Result.fail("下单失败:" + e.getMessage());
    }

    // 限流/熔断降级
    public Result<String> createOrderBlockHandler(OrderDTO dto, BlockException e) {
        if (e instanceof DegradeException) {
            return Result.fail("下单功能暂时不可用,请稍后重试");  // 熔断
        }
        return Result.fail("系统繁忙,请稍后重试");  // 限流
    }

    public Result<Order> getOrderFallback(Long id, Throwable e) {
        log.error("getOrder 业务异常, id={}", id, e);
        return Result.fail("查询订单失败");
    }

    public Result<Order> getOrderBlockHandler(Long id, BlockException e) {
        return Result.fail("系统繁忙,请稍后重试");
    }
}

步骤二:规则配置类

java
@Configuration
public class SentinelRuleConfig {

    @PostConstruct
    public void initRules() {
        initFlowRules();
        initDegradeRules();
    }

    // ──────────── 限流规则 ────────────
    private void initFlowRules() {
        // 规则一:createOrder 最多 100 QPS
        FlowRule createOrderRule = new FlowRule("createOrder")
            .setGrade(RuleConstant.FLOW_GRADE_QPS)   // QPS 限流
            .setCount(100)                             // 阈值 100
            .setControlBehavior(0);                    // 快速失败

        // 规则二:getOrder 最多 500 QPS
        FlowRule getOrderRule = new FlowRule("getOrder")
            .setGrade(RuleConstant.FLOW_GRADE_QPS)
            .setCount(500)
            .setControlBehavior(0);

        FlowRuleManager.loadRules(List.of(createOrderRule, getOrderRule));
    }

    // ──────────── 熔断规则 ────────────
    private void initDegradeRules() {
        // 只对 createOrder 配置熔断(getOrder 不需要)
        DegradeRule createOrderRule = new DegradeRule("createOrder")
            .setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType())
            .setCount(0.5)           // 异常比例 > 50%
            .setTimeWindow(10)       // 熔断 10 秒
            .setMinRequestAmount(5)  // 最小请求数 5
            .setStatIntervalMs(1000);

        DegradeRuleManager.loadRules(List.of(createOrderRule));
    }
}

步骤三:application.yml 配置

yaml
spring:
  application:
    name: order-service
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8719
      eager: true   # 启动时立即初始化,避免懒加载丢失规则

feign:
  sentinel:
    enabled: true   # 开启 Feign 的 Sentinel 支持

步骤四:测试场景

场景一:正常流量(QPS < 100,无异常)
──────────────────────────────────────────
  createOrder: QPS = 80  → 通过 ✅
  getOrder:    QPS = 200 → 通过 ✅
  
  结果:一切正常,两个接口都返回成功


场景二:createOrder 流量暴增(QPS > 100)
──────────────────────────────────────────
  createOrder: QPS = 150
    ├── 100 个请求 → 通过 ✅
    └── 50 个请求  → BlockException → "系统繁忙,请稍后重试" ❌
  
  getOrder: QPS = 200 → 通过 ✅(不受 createOrder 影响)
  
  结果:createOrder 部分拒绝,getOrder 完全正常


场景三:库存服务挂了(createOrder 异常率 > 50%)
──────────────────────────────────────────
  第 1 秒:库存服务超时
    createOrder 的 10 个请求:
      ├── 6 个超时异常 → failureCount = 6
      └── 4 个成功     → successCount = 4
  
    异常比例 = 6/10 = 60% > 50% → 熔断器 CLOSED → OPEN
  
  第 2 秒 ~ 第 11 秒:熔断器 OPEN
    createOrder 的全部请求 → DegradeException → "下单功能暂时不可用" ❌
    getOrder 的全部请求 → 正常返回 ✅(不受影响)
  
  第 12 秒:熔断时间到 → HALF_OPEN
    探测请求 → 成功 → CLOSED(恢复正常)
    探测请求 → 失败 → OPEN(继续熔断,再等 10 秒)
  
  结果:createOrder 10 秒内快速失败,线程不阻塞;getOrder 始终正常


场景四:流量暴增 + 库存挂了(限流和熔断同时触发)
──────────────────────────────────────────
  createOrder: QPS = 200,且库存服务异常
  
  请求进入 Slot Chain:
    ├── FlowSlot 检查:QPS=200 > 100 → BlockException(限流)
    │   → 100 个请求被限流,返回 "系统繁忙"

    └── 100 个通过限流的请求,进入 DegradeSlot:
          └── 异常率 60% > 50% → DegradeException(熔断)
              → 熔断器 OPEN,后续请求全部拒绝
  
  结果:限流拦住 50%,熔断拦住剩下的 50%,双重保护

完整流程图:

请求 POST /order/create


  @SentinelResource("createOrder")


  ┌─────────────────────────────┐
  │  FlowSlot.entry()           │
  │  QPS = 85 < 100             │
  │  → 通过 ✅                   │
  └──────────────┬──────────────┘


  ┌─────────────────────────────┐
  │  DegradeSlot.entry()        │
  │  circuitBreaker.state       │
  │  = CLOSED                   │
  │  → 通过 ✅                   │
  └──────────────┬──────────────┘


  调用 inventoryClient.deductStock()

        ├── 成功 → DegradeSlot.exit(rt=50ms, error=null)
        │         → successCount++ → 不触发熔断
        │         → 返回 "下单成功"

        └── 超时 → DegradeSlot.exit(rt=1000ms, error=TimeoutException)
                  → failureCount++, successCount++
                  → 异常率 = 60% > 50% → 熔断器 OPEN
                  → 返回 "下单失败:超时"

一句话总结: 限流和熔断配置在同一个 @SentinelResource 上,通过 Slot Chain 按顺序执行。限流在门口数人头(QPS),熔断在调用后看结果(异常率)。两者独立配置、独立判断,组合起来形成完整的流量防护体系。


六、热点参数限流

6.1 什么场景需要热点限流?

普通限流是对整个资源限流,热点限流是对某个参数值单独限流:

场景:商品详情接口,大部分商品 QPS=100 没问题
      但某个爆款商品(id=888)被频繁刷,需要单独限制

普通限流:
  /product/detail?id=888 → QPS=100
  /product/detail?id=999 → QPS=100
  问题:无法区分热门商品和普通商品

热点限流:
  /product/detail?id=888 → QPS=10(热门商品单独限制)
  /product/detail?id=999 → QPS=100(普通商品不受影响)

6.2 代码实现

java
@RestController
public class ProductController {

    @GetMapping("/product/{id}")
    @SentinelResource(
        value = "productDetail",
        blockHandler = "productDetailBlockHandler"
    )
    public Result<Product> getProduct(@PathVariable Long id) {
        return Result.success(productService.getById(id));
    }

    public Result<Product> productDetailBlockHandler(Long id, BlockException e) {
        return Result.fail("该商品访问过于频繁,请稍后再试");
    }
}

6.3 热点规则配置

java
@Component
public class ParamFlowRuleConfig {

    @PostConstruct
    public void initParamFlowRules() {
        ParamFlowRule rule = new ParamFlowRule();
        rule.setResource("productDetail");       // 资源名
        rule.setParamIdx(0);                      // 参数索引(第 0 个参数,即 id)
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule.setCount(100);                       // 默认 QPS 阈值

        // 设置热点参数的特殊阈值
        ParamFlowItem item1 = new ParamFlowItem();
        item1.setObject("888");                   // 商品 id=888
        item1.setClassType(Long.class.getName());
        item1.setCount(10);                       // 特殊阈值:QPS=10

        ParamFlowItem item2 = new ParamFlowItem();
        item2.setObject("666");
        item2.setClassType(Long.class.getName());
        item2.setCount(5);                        // 商品 id=666 只能 QPS=5

        rule.setParamFlowItemList(Arrays.asList(item1, item2));
        ParamFlowRuleManager.loadRules(Collections.singletonList(rule));
    }
}

执行效果:

请求 /product/888 → QPS 限制 10(特殊阈值)
请求 /product/666 → QPS 限制 5(特殊阈值)
请求 /product/999 → QPS 限制 100(默认阈值)

6.4 热点参数限流原理

Sentinel 按参数值分别统计 QPS,而不是把整个资源的 QPS 混在一起:

productDetail 资源

  ├── id=888 → 独立时间窗口,统计 id=888 的 QPS
  ├── id=666 → 独立时间窗口,统计 id=666 的 QPS
  ├── id=999 → 独立时间窗口,统计 id=999 的 QPS
  └── ...   → 独立时间窗口

七、系统自适应保护

7.1 为什么需要系统保护?

前面讲的限流和熔断都是针对单个资源,但系统保护是针对整个机器

场景:机器总共有 4 核 CPU
      接口 A 限流 QPS=100,接口 B 限流 QPS=100
      看起来没问题,但 A+B 同时达到 100,CPU 爆了

系统保护:不管单个接口的 QPS 阈值是多少
         只要整机 Load 或 CPU 超过阈值,所有接口都限流

7.2 四种系统保护规则

规则含义阈值示例适用场景
LOAD系统负载(load1)超过阈值load1 > 4通用
RT所有入口的平均 RT 超过阈值avgRT > 200ms通用
线程数并发线程数超过阈值并发 > 500通用
入口 QPS所有入口的总 QPS 超过阈值总 QPS > 1000有明确压测数据
CPU 使用率CPU 使用率超过阈值CPU > 80%通用

7.3 代码配置

java
@Component
public class SystemRuleConfig {

    @PostConstruct
    public void initSystemRules() {
        List<SystemRule> rules = new ArrayList<>();

        // CPU 使用率超过 80% 时触发保护
        SystemRule cpuRule = new SystemRule();
        cpuRule.setHighestCpuUsage(0.8);  // 80%
        rules.add(cpuRule);

        // 系统负载超过 4 时触发保护
        SystemRule loadRule = new SystemRule();
        loadRule.setHighestSystemLoad(4.0);
        rules.add(loadRule);

        // 平均 RT 超过 200ms 时触发保护
        SystemRule rtRule = new SystemRule();
        rtRule.setAvgRt(200);
        rules.add(rtRule);

        // 并发线程数超过 500 时触发保护
        SystemRule threadRule = new SystemRule();
        threadRule.setMaxThread(500);
        rules.add(threadRule);

        // 入口 QPS 超过 1000 时触发保护
        SystemRule qpsRule = new SystemRule();
        qpsRule.setQps(1000);
        rules.add(qpsRule);

        SystemRuleManager.loadRules(rules);
    }
}

7.4 系统保护与资源限流的区别

资源限流:
  每个资源独立限流
  /api/a → QPS=100,/api/b → QPS=200
  各自独立,互不影响

系统保护:
  全局限流
  只要整机 Load > 4,所有资源都限流
  不管 /api/a 和 /api/b 各自的 QPS 阈值是多少

最佳实践: 资源限流 + 系统保护双保险。资源限流防止单个接口打满,系统保护防止所有接口加起来打满。

八、规则持久化

8.1 为什么要持久化?

Sentinel 默认规则存储在内存中,服务重启后规则丢失。生产环境必须持久化。

8.2 持久化到 Nacos(推荐)

xml
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
yaml
spring:
  cloud:
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080
        port: 8719
      datasource:
        # 限流规则
        flow:
          nacos:
            server-addr: 127.0.0.1:8848
            namespace: sentinel
            group-id: DEFAULT_GROUP
            data-id: ${spring.application.name}-flow-rules
            data-type: json
            rule-type: flow
        # 熔断规则
        degrade:
          nacos:
            server-addr: 127.0.0.1:8848
            namespace: sentinel
            group-id: DEFAULT_GROUP
            data-id: ${spring.application.name}-degrade-rules
            data-type: json
            rule-type: degrade
        # 系统规则
        system:
          nacos:
            server-addr: 127.0.0.1:8848
            namespace: sentinel
            group-id: DEFAULT_GROUP
            data-id: ${spring.application.name}-system-rules
            data-type: json
            rule-type: system

Nacos 中存储的限流规则 JSON:

json
[
  {
    "resource": "createOrder",
    "grade": 1,
    "count": 100,
    "strategy": 0,
    "controlBehavior": 0,
    "limitApp": "default"
  }
]

规则同步流程:

Sentinel 控制台修改规则


推送到应用服务

  ├── 应用内存中生效(实时)
  └── 同步到 Nacos(持久化)


      服务重启后,从 Nacos 加载规则

8.3 持久化到 Apollo

yaml
spring:
  cloud:
    sentinel:
      datasource:
        flow:
          apollo:
            namespace-name: application
            flow-rules-key: sentinel.flow.rules
            default-flow-rule-value: "[]"
            rule-type: flow

九、@SentinelResource 注解详解

9.1 注解参数一览

参数说明默认值
value资源名,必须唯一
entryType入口类型(IN/OUT)EntryType.OUT
blockHandler限流/熔断处理函数名
blockHandlerClass限流处理函数所在类当前类
fallback业务异常降级函数名
fallbackClass降级函数所在类当前类
exceptionsToIgnore忽略的异常类型(不触发 fallback)
defaultFallback默认降级函数名(不指定 fallback 时使用)

9.2 exceptionsToIgnore — 忽略特定异常

某些异常不需要降级,比如参数校验异常,直接返回错误即可:

java
@SentinelResource(
    value = "createOrder",
    fallback = "createOrderFallback",
    exceptionsToIgnore = {IllegalArgumentException.class}  // 参数错误不降级
)
public Result<String> createOrder(OrderRequest request) {
    if (request.getAmount() <= 0) {
        throw new IllegalArgumentException("金额必须大于0");
    }
    return orderService.create(request);
}

9.3 fallback 方法的签名要求

java
// 签名1:参数与原方法完全一致 + Throwable(推荐)
public Result<String> createOrderFallback(OrderRequest request, Throwable e) {
    log.error("降级", e);
    return Result.fail("下单失败");
}

// 签名2:只有 Throwable,不关注原参数
public Result<String> createOrderFallback(Throwable e) {
    return Result.fail("服务暂不可用");
}

// 注意:返回值类型必须与原方法一致
// 注意:blockHandler 必须包含 BlockException 参数

十、Sentinel 与 Feign 集成

10.1 开启 Feign 的 Sentinel 支持

yaml
feign:
  sentinel:
    enabled: true  # 开启 Sentinel 对 Feign 的支持

10.2 配置 fallbackFactory

java
@FeignClient(
    name = "inventory-service",
    fallbackFactory = InventoryClientFallbackFactory.class  // Sentinel 生效
)
public interface InventoryClient {

    @PostMapping("/api/inventory/deduct")
    Result<Boolean> deductStock(@RequestBody DeductRequest request);

    @GetMapping("/api/inventory/query/{skuId}")
    Result<Integer> queryStock(@PathVariable Long skuId);
}

@Component
@Slf4j
public class InventoryClientFallbackFactory implements FallbackFactory<InventoryClient> {

    @Override
    public InventoryClient create(Throwable cause) {
        // 区分不同异常类型,做不同降级
        if (cause instanceof BlockException) {
            log.warn("库存服务被限流或熔断", cause);
        } else {
            log.error("库存服务调用异常", cause);
        }

        return new InventoryClient() {
            @Override
            public Result<Boolean> deductStock(DeductRequest request) {
                return Result.fail("库存服务暂时不可用,请稍后重试");
            }

            @Override
            public Result<Integer> queryStock(Long skuId) {
                return Result.fail("库存查询失败");
            }
        };
    }
}

10.3 Feign + Sentinel 的完整调用链

Feign 调用 inventory-service


Sentinel 拦截(Feign 调用被视为一个 Sentinel 资源)

  ├── 限流检查 → 被限流 → BlockException → fallbackFactory
  ├── 熔断检查 → 已熔断 → BlockException → fallbackFactory
  └── 放行


     实际 HTTP 调用

        ├── 成功 → 返回结果
        └── 失败 → 异常 → fallbackFactory

十、Sentinel 与 Gateway 集成

10.1 网关层限流

Gateway 是流量的第一道防线,在网关层限流可以拦截非法请求,保护后端服务:

xml
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
yaml
spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/order/**
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080
      scg:
        fallback:
          mode: response  # 降级模式:response(返回 JSON)/ redirect(重定向)
          response-status: 429
          response-body: '{"code":429,"msg":"请求过于频繁,请稍后重试"}'

10.2 网关限流规则

在 Sentinel 控制台配置网关规则:

资源名:order-service(网关路由 ID)
阈值类型:QPS
阈值:100

效果: 所有 /order/** 的请求在网关层被限流,不会到达后端服务。

10.3 两层防护策略

用户请求


┌──────────────────────────────┐
│ Sentinel 网关层(第一道防线)   │  ← 全局限流:拦截非法、超量请求
│ 按路由限流,QPS=1000          │
└──────────────┬───────────────┘

┌──────────────────────────────┐
│ Sentinel 服务层(第二道防线)   │  ← 精细限流:按资源、按参数限流
│ 按资源限流,QPS=100           │
│ 热点参数限流,QPS=10          │
└──────────────┬───────────────┘

           业务处理

十一、Sentinel 控制台

11.1 启动控制台

bash
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 \
     -Dproject.name=sentinel-dashboard \
     -jar sentinel-dashboard.jar

11.2 控制台功能

功能说明
实时监控每个资源的 QPS、RT、通过数、拒绝数、线程数,实时刷新
簇点链路列出所有 Sentinel 资源,看到每个资源的调用链
流控规则新增/修改/删除限流规则,实时生效
熔断规则新增/修改/删除熔断规则,实时生效
热点规则新增/修改/删除热点参数限流规则
系统规则新增/修改/删除系统保护规则
授权规则黑白名单控制
集群流控Token Server 集群流控配置

11.3 应用接入控制台

yaml
spring:
  cloud:
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080     # 控制台地址
        port: 8719                      # 本机与控制台通信端口
        client-ip: 192.168.1.100        # 本机 IP(可选)

注意: Sentinel 1.8.0+ 控制台默认端口改为 8080(原 8080),应用端口 8719 不变。

11.4 控制台通信原理

应用服务(8719 端口)                     Sentinel 控制台(8080 端口)
      │                                          │
      │──── 心跳(每秒)──────────────────────────→│  上报机器信息
      │                                          │
      │←─── 规则查询(应用主动拉取)──────────────│  获取最新规则
      │                                          │
      │──── 监控数据(每秒)──────────────────────→│  QPS/RT/线程数
      │                                          │
      │←─── 规则下发(控制台推送)────────────────│  规则变更通知

十二、核心源码分析

12.1 SphU.entry() — 入口方法

java
// 这是 Sentinel 最核心的入口
public static Entry entry(String name) throws BlockException {
    // 1. 构建 Context
    Context context = ContextUtil.getContext();

    // 2. 创建 ProcessorSlotChain(责任链)
    ProcessorSlotChain chain = lookProcessChain(resourceWrapper);

    // 3. 创建 Entry
    Entry entry = new CtEntry(resourceWrapper, chain, context);

    // 4. 执行责任链,任何一个 Slot 可以抛出 BlockException
    chain.entry(context, resourceWrapper, null, count, args);

    return entry;
}

12.2 Slot 执行顺序

java
// Sentinel 默认的责任链构建顺序
public class DefaultSlotChainBuilder implements SlotChainBuilder {
    @Override
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();

        chain.addLast(new NodeSelectorSlot());     // 构建调用链
        chain.addLast(new ClusterBuilderSlot());   // 构建集群节点
        chain.addLast(new LogSlot());              // 记录日志
        chain.addLast(new StatisticSlot());        // 统计指标
        chain.addLast(new AuthoritySlot());        // 黑白名单
        chain.addLast(new SystemSlot());           // 系统保护
        chain.addLast(new ParamFlowSlot());        // 热点参数
        chain.addLast(new FlowSlot());             // 流量控制
        chain.addLast(new DegradeSlot());          // 熔断降级

        return chain;
    }
}

12.3 FlowSlot — 限流检查

java
public class FlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper,
                      DefaultNode node, int count, boolean prioritized, Object... args)
            throws Throwable {

        // 1. 获取该资源的所有限流规则
        Map<String, List<FlowRule>> flowRules = FlowRuleManager.getFlowRuleMap();

        // 2. 逐个检查规则
        for (FlowRule rule : flowRules.get(resourceWrapper.getName())) {
            // 3. 调用 FlowRuleChecker 检查
            if (!FlowRuleChecker.checkFlow(rule, context, node, count, prioritized)) {
                // 不通过 → 抛出 FlowException
                throw new FlowException(rule.getLimitApp(), rule);
            }
        }

        // 4. 通过 → 继续下一个 Slot
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }
}

12.4 DegradeSlot — 熔断检查

java
public class DegradeSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper,
                      DefaultNode node, int count, boolean prioritized, Object... args)
            throws Throwable {

        // 1. 获取该资源的所有熔断规则
        List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(resourceWrapper.getName());

        // 2. 逐个检查熔断器
        for (CircuitBreaker cb : circuitBreakers) {
            if (!cb.tryPass(context)) {
                // 熔断器打开 → 抛出 DegradeException
                throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule());
            }
        }

        // 3. 通过 → 继续下一个 Slot
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }
}

十三、生产最佳实践

13.1 规则规划

1. 核心接口:QPS 限流 + 熔断降级
   - 下单、支付、秒杀 → 先限流,再熔断,双重保护

2. 非核心接口:QPS 限流(低优先级)
   - 查询、搜索 → 限流即可,挂了不影响主流程

3. 网关层:全局 QPS 限流 + 系统保护
   - 第一道防线,拦截流量洪峰

4. 热门数据:热点参数限流
   - 爆款商品、热门活动 → 单独限制

13.2 阈值设定原则

1. 不要拍脑袋,要压测
   - 上线前压测,拿到真实 QPS 上限
   - 阈值 = 上限 × 0.7(留 30% 余量)

2. 不同接口不同阈值
   - 下单接口:100 QPS
   - 查询接口:500 QPS
   - 导出接口:5 线程

3. 熔断阈值要保守
   - 慢调用比例:50%(超过一半请求都慢,肯定有问题)
   - 异常比例:50%
   - 熔断时长:5-30 秒(根据恢复时间确定)

13.3 降级逻辑设计

1. 必须记录日志
   - 降级发生时,带异常信息打日志,方便排查

2. 兜底数据要合理
   - 查询:返回空列表 ≠ 返回错误
   - 下单:返回失败 ≠ 静默丢弃
   - 详情:返回缓存数据 ≠ 返回 null

3. 降级不能雪崩
   - 降级逻辑本身不能调用远程服务
   - 降级逻辑不能耗时过长
   - 降级逻辑不能抛异常

13.4 监控告警

1. Sentinel 控制台实时监控
   - 关注 QPS、RT、拒绝数、熔断状态

2. 接入 Prometheus + Grafana
   - 持久化监控数据
   - 配置告警规则

3. 告警规则
   - 熔断器打开 → 立即告警
   - 限流拒绝数 > 阈值 → 告警
   - RT 突增 > 2 倍 → 告警

13.5 常见坑

说明解决
规则不生效资源名拼写错误检查 @SentinelResource 的 value
控制台看不到应用未配置 sentinel.transport.port检查 8719 端口是否可达
重启后规则丢失未持久化规则配置 Nacos 数据源
fallback 不生效方法签名错误检查参数类型和返回值
blockHandler 不生效未声明 BlockException 参数必须加 BlockException 参数
Feign 不熔断未开启 feign.sentinel.enabled配置 feign.sentinel.enabled=true

十四、面试要点

Q1:Sentinel 的限流原理是什么?

基于滑动窗口统计 QPS,当 QPS 超过阈值时触发限流。支持三种流控效果:快速失败(直接拒绝)、Warm Up(预热)、排队等待(匀速排队)。

Q2:Sentinel 的熔断降级有哪几种策略?

三种:慢调用比例(RT 超过阈值的比例)、异常比例、异常数。触发后熔断器从 CLOSED → OPEN → HALF_OPEN → CLOSED/OPEN 自动流转。

Q3:Sentinel 的责任链(Slot Chain)是什么?

每个请求按顺序经过一组 Slot:NodeSelectorSlot → ClusterBuilderSlot → StatisticSlot → AuthoritySlot → SystemSlot → ParamFlowSlot → FlowSlot → DegradeSlot。任一 Slot 抛出 BlockException 则请求被拦截。

Q4:fallback 和 blockHandler 有什么区别?

blockHandler 处理限流/熔断(BlockException),fallback 处理业务异常(RuntimeException)。可以同时配置,blockHandler 优先执行。

Q5:热点参数限流和普通限流有什么区别?

普通限流对资源整体限流,热点限流针对特定参数值单独限流。例如爆款商品 id=888 单独限制 QPS=10,其他商品 QPS=100。

Q6:Sentinel 规则持久化怎么做?

通过 Nacos/Apollo 等配置中心持久化规则。配置 datasource 后,规则从控制台下发到应用的同时同步到配置中心,重启后自动加载。

Q7:Sentinel 和 Hystrix 的核心区别?

Sentinel 支持 QPS 限流、热点参数限流、系统保护,规则可动态修改持久化;Hystrix 只有熔断和线程池隔离。Sentinel 更轻量(信号量隔离),Hystrix 线程池隔离有额外开销。