Appearance
Sentinel 详解:流量治理
Sentinel 是阿里巴巴开源的流量治理组件,以"流量"为切入点,提供流量控制、熔断降级、系统负载保护等多维度保护能力。它是 Spring Cloud Alibaba 生态中替代 Hystrix 的核心组件。
一、为什么需要 Sentinel?
1.1 没有流量治理会发生什么?
场景:秒杀活动,瞬时流量 10000 QPS,服务只能扛 1000 QPS

Sentinel 的价值在于:在流量到达业务代码之前,就把它拦住。不是等系统崩溃了再补救,而是提前预防。
1.2 Sentinel 部署在哪里?是网关还是每个服务?
Sentinel 不是一个独立的网关或中间件,而是一个嵌入到应用中的库(Library)。 它不是像 Nginx 那样独立部署的代理,而是以 jar 包的形式跑在每个服务进程里。

两层部署策略:
| 层级 | 部署位置 | 作用 | 类比 |
|---|---|---|---|
| 网关层 | Gateway 中引入 Sentinel | 全局入口限流,拦截非法/超量请求 | 小区大门保安 |
| 服务层 | 每个微服务中引入 Sentinel | 单服务精细限流、熔断降级 | 每家每户的门锁 |
最佳实践:两层都加,双层防护。

为什么两层都要加?网关层只能按路由限流(如 /order/** QPS=1000),但无法区分具体接口(如 /order/create 和 /order/query 的 QPS 不同)。服务层可以按资源名精确限流,还能做熔断降级和热点参数限流。网关层挡住 90% 的无效流量,服务层精准控制剩下的 10%。
1.3 Sentinel 能做什么?
| 能力 | 解决的问题 | 一句话 |
|---|---|---|
| 流量控制 | 流量突发,超过系统承载能力 | "多了就拒" — 超过 QPS 阈值直接拒绝 |
| 熔断降级 | 下游服务不稳定,拖垮调用方 | "坏了就断" — 出错率高了自动熔断 |
| 系统保护 | 系统整体负载过高 | "太忙就限" — Load/CPU 高了自动限流 |
| 热点限流 | 某个热点数据被频繁访问 | "热的就限" — 针对热点参数单独限流 |
| 网关流控 | 网关层统一限流 | "门口就拦" — 在网关层拦截 |
1.3 Sentinel vs Hystrix 为什么选 Sentinel?
| 维度 | Sentinel | Hystrix |
|---|---|---|
| 隔离策略 | 信号量隔离 | 线程池隔离 / 信号量隔离 |
| 熔断策略 | 慢调用比例 / 异常比例 / 异常数 | 仅异常比例 |
| 流量控制 | 丰富(QPS/线程/关联/链路/热点) | 无,仅熔断 |
| 规则动态修改 | 控制台实时推送,秒级生效 | 有限支持 |
| 规则持久化 | Nacos / Apollo / ZooKeeper / 文件 | 无原生支持 |
| 控制台 | 功能完善,实时监控 | Dashboard 简陋 |
| 维护状态 | 阿里活跃维护 | Netflix 停维 |
| 性能 | 轻量,对业务代码无侵入 | 线程池隔离有额外开销 |
二、核心架构与工作原理
2.1 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,可自定义 |
| entranceNode | EntranceNode,整棵调用树的根节点 |
| curEntry | 当前正在执行的 Entry |
| origin | 调用来源(如 appA、gateway) |
生命周期:
请求进入 → 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 的核心作用:
- 触发 Slot Chain:
SphU.entry()内部创建 Entry 时,会触发整个 Slot Chain 执行 - 记录 RT:
entry()记录createTime,exit()时计算当前时间 - createTime = RT - 嵌套调用: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) | 固定 |
环形数组的优势:
- O(1) 定位:
timeId % length直接算出下标,无需遍历 - 固定内存:数组长度 = sampleCount,不会无限增长
- 无锁更新:CAS 操作,高并发下比锁机制高效得多
- 自动过期:旧窗口被新窗口自然覆盖,无需额外清理逻辑
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 个线程在执行 slowQueryQPS 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(秒杀页面的入口资源名)
阈值:103.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, strategy | grade, 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(熔断) |
|---|---|---|
| grade | QPS / 线程数 | 慢调用比例 / 异常比例 / 异常数 |
| count | 100(个) | 0.5(50%) |
| timeWindow | 无 | 10(秒) |
| controlBehavior | 快速失败 / Warm Up / 排队 | 无 |
| strategy | 直接 / 关联 / 链路 | 无 |
| minRequestAmount | 无 | 5(最小请求数) |
| 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 的区别:
| 维度 | fallback | blockHandler |
|---|---|---|
| 触发条件 | 业务异常(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: systemNacos 中存储的限流规则 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.jar11.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 线程池隔离有额外开销。
