Spring Cloud Alibaba 微服务系列 - Sentinel

Sentinel 是一款专为分布式微服务架构打造的、久经洗礼的高可用轻量流控和防护组件。如果说 Nacos 是微服务的 “大本营”和“总调度室”,那么 Sentinel 就是微服务面对线上惊涛骇浪时的 “防洪堤坝” 与 “特种盾牌”。关于 Sentinel 的更多基本介绍,可以参考 官方文档 ,这里主要对它的具体使用进行探究和说明。


Sentinel 解决了什么问题

实际问题的解决

要真正理解 Sentinel,我们需要知道它是被什么场景 “逼” 出来的,解决了哪些核心的问题。首先 Sentinel 诞生于阿里巴巴内部。早在 2012 年,它就在阿里内部孵化,最初的使命极其纯粹——保障阿里双十一大宗电商交易的核心链路稳定性。它曾正面迎击过每秒钟数百万级的极端脉冲流量、全球最大规模的抢购秒杀、以及错综复杂的微服务级联依赖。2018 年,阿里将这一久经沙场的 “流量防汛大闸” 贡献给开源社区,并将其作为 Spring Cloud Alibaba 生态圈的核心元老,直接成为了当下国内微服务高可用防护的实际标准。

在高度分散的分布式微服务架构中,Sentinel 核心要解决的就是局部故障与服务雪崩的问题。在生产实践中,它精准打击以下四大核心痛点:

  • 突发激增流量(脉冲流量)
    • 典型场景:大局域网内运营突然搞秒杀、大促,或者黑客发起恶意刷单,原本 100 QPS 的接口瞬间飙升到 100,000 QPS。
    • Sentinel 的解法:流量控制(Flow Control)。在入口处架设闸门,超过系统承载能力的请求立刻执行排队、平滑预热或直接拒绝,保证系统内部永远风平浪静。
  • 下游故障引发的长链阻塞
    • 典型场景:Order 服务去调 Payment 接口,Payment 的数据库卡死了。由于没有拦截,Order 的线程池瞬间被卡死的调用全部占满,进而导致整个 Order 服务瘫痪。
    • Sentinel 的解法:熔断降级(Circuit Breaking)。一旦发现下游响应时间过长或异常比例过高,Sentinel 会直接切断对下游的真实调用,在前端执行快速失败并返回 Fallback 结果,保全上游。
  • 线程打满与系统全面瘫痪
    • 典型场景:某个非核心接口(如拉取广告、送积分)因外部原因响应极慢,导致 Tomcat 全局线程池满。
    • Sentinel 的解法:舱壁隔离(Bulkhead - 信号量隔离)。严格限制某一个微服务接口所能使用的最大并发线程数(如最多允许 20 个并发),哪怕这个接口彻底卡死,也绝不影响其他高价值业务。
  • 机器 CPU 飙升导致的整机猝死
    • 典型场景:随着并发升高,服务器的 CPU 利用率瞬间飙到 95% 以上,机器面临假死或宕机。
    • Sentinel 的解法:系统自适应保护(System Adaptive Protection)。Sentinel 会自适应监控服务器的 CPU 使用率、系统负载(Load)、整体入口 QPS、响应时间(RT)和总线程数。一旦发现某项指标越过红线,立刻启动自愈限流,强行把系统拉回安全水位。


同类产品间的比较

在微服务高可用防护领域,历史上有过三款标杆产品:Netflix Hystrix(曾经的霸主)、Resilience4j(Spring官方推荐),以及 Alibaba Sentinel(当下的王者)。

Hystrix 最致命的硬伤在于它主推的 “线程池隔离”。每一个被保护的接口都要额外开辟一个独立的线程池。这在高并发下会带来极其恐怖的线程上下文切换(Context Switch)开销,白白浪费了大量 CPU 资源。此外,Hystrix 无法做到平滑的流量控制(限流),且官方已于 2018 年宣布停更。

Resilience4j 是一个非常纯粹、优秀的 Java 函数式编程库,但它没有开箱即用的可视化控制台。在线上排查问题或需要紧急微调限流阈值时,开发和运维只能去改代码或 YML 重新部署,这在争分夺秒的线上生产事故面前是无法接受的。

Sentinel 能够全面绝杀对手,成为国内首选,凭借的是以下四个降维打击级的能力:

  • 轻量级且高性能(信号量拓扑):Sentinel 默认采用 “信号量隔离(Counter)” 的设计。它在内存中通过极低损耗的 “滑动窗口(Sliding Window)” 算法来统计各项指标。引入 Sentinel 几乎不增加微服务的线程开销,吞吐量极高,完美规避了 Hystrix 线程池上下文切换带来的性能损耗。
  • 极其丰富的流量整形算法:Sentinel 不是简单地把超过阈值的请求丢弃,而是提供了一整套流量控制手段:
    • 直接拒绝:超出直接抛异常。
    • 冷启动 / 预热(Warm Up):当系统长期处于空闲状态突然迎来大促时,让通过的流量在设定的预热时间内平滑递增到阈值上限,给底层的数据库连接池、缓存留出充裕的 “苏醒时间”。
    • 排队等待(匀速器):利用漏桶算法,让多余的请求在队列里规整地排队,以固定的时间间隔匀速通过。特别适合处理大促脉冲流量,把惊涛骇浪削减成潺潺流水。
  • 直观的可视化控制台(Dashboard):在此你可以实时看到全公司所有微服务、所有接口的秒级 QPS、响应时间(RT)、通过量、拒绝量。当你发现某台机器快撑不住时,也可以在控制台上在线把限流阈值从 1000 调到 500,点击保存,规则通过底层的通信协议瞬间同步到运行中的微服务内存中,秒级生效!
  • 与 Spring Cloud Alibaba 生态的绝佳适配:作为全家桶的一员,Sentinel 提供了几乎是零代码侵入的无缝对接。只要你引入了 Starter,它会自动接管你的 Spring Cloud Gateway(网关限流)Feign/OpenFeign(RPC接口防护)WebMvc(Controller层切面拦截) 以及 Dubbo(RPC高可用治理)。你只需要写配置,就能瞬间拉起一张全方位立体的微服务防护大网。


Sentinel 的四板斧

如果把高并发流量比作汹涌的洪水进入微服务,Sentinel 核心的防护手段可以完美重组成以下四大防御梯队:

第一梯队:入口整形(限流与流量整形)

核心思想就是把惊涛骇浪削减成潺潺流水。这是 Sentinel 最基本、也是最强大的第一道防线。它通过在网关层或 Controller 入口层架设 “水闸”,防止后端微服务被瞬间冲垮。

  • 直接流控(QPS / 线程数限流):最直接的硬拦截。一旦当前接口的秒级 QPS 超过设定阈值(例如 1000),超出部分的请求立刻直接拒绝,抛出异常。
    • QPS 关心的是速度。它在网关或接口入口处架设了一个“计数器”(如令牌桶或滑动窗口)。比如只要在 1 秒钟内,进来的 HTTP 请求个数超过了设定的阈值(比如 100),第 101 个请求就会被立刻拦截并拒绝。它完全不关心这些请求进入系统后,是 1 毫秒就处理完了,还是卡了 10 秒钟。它只管“把好大门”。
    • 线程数关心的是资源(排队容积)。它在系统内部架设了一个“天花板”,控制同时有多少个 Tomcat/Netty 业务线程在执行这个方法。当一个请求进来时,会占用 1 个工作线程去处理业务(读数据库、调外部接口),处理完后线程释放。如果当前有 10 个线程正在同步执行该业务,第 11 个请求进来时,发现“座位满了”,就会被立刻拦截拒绝。它完全不关心一秒钟内到底进来了多少个请求。如果接口响应极快(比如 1ms 完工),10 个座位(线程)在 1 秒内可以轮转交替接待成千上万个请求;如果接口极其卡顿,10 个座位被死死占满,QPS 就会瞬间跌到接近于 0。
    • 大热点突发洪峰(必须用QPS限流):抢购场景或遭遇恶意刷单,某个查询秒杀商品的接口平时 QPS 是 10,突然飙升到 20000。这个接口没有任何远程调用,全是纯内存操作,响应极其迅猛(只需 0.5ms)。如果你配了线程数限流(设为 20),因为接口响应太快(0.5ms),1 个线程在一秒内就能处理 1000 / 0.5 = 2000 个请求。20 个线程一秒能疯狂放行 40000 个请求,结果由于流量总量完全失控,极高强度的计算让本地服务器的 CPU 瞬间飙到 100% 爆表,或者瞬间把底层的 Redis 数据库连接池活活冲垮。所以,如果你配了 QPS 限流(设为 1000),不管你响应多快、动用多少线程,1 秒钟只要数到第 1001 个请求,无条件大闸拉下,后面 19000 个请求全部被挡在门外。
    • 微服务遭遇下游假死(必须用线程数限流):假设你的订单服务去调用第三方支付网关,平时响应只需 10ms。现在第三方支付网关突然卡死(假死),每个请求需要 10 秒才会超时返回。如果你配了 QPS 限流(设为 100),在这一秒内进来了 50 个请求,没有超过 100 的门槛,QPS 拦截器放行。但这 50 个请求进入系统后全部卡死死等。下一秒又进来 50 个,又放行,又卡死…… 结果Tomcat 的几百个核心线程在短短几秒内被全量占满并活活拖死,导致你整个订单服务的所有其他健康接口(如查看购物车、查看物流)全部跟着瘫痪。这就是典型的服务雪崩。所以如果配线程数 限流(假设为 10),前 10 个请求进来,占满了 10 个工作线程。第 11 个请求进来时,Sentinel 发现执行该方法的并发线程数已经是 10,立刻秒回失败。
  • 冷启动预热(Warm Up):专门对付 “低热度系统突迎洪峰”。当微服务刚启动或长期处于空闲状态时,系统并发承载力处于低谷。Sentinel 会在设定的预热时间(如 10 秒)内,让通过的流量上限缓慢、平滑地递增到峰值阈值,给底层的数据库连接池、JVM 堆内存留出充裕的 “热身唤醒” 时间。
  • 排队等待(匀速流控):利用漏桶算法。对于瞬间涌入的脉冲流量(如整点秒杀),多余的请求不直接丢弃,而是塞进队列里,让请求以固定的间隔(如每 10ms 通过一个)匀速通过。只要等待时间没超过设定的超时时间,请求就能被平滑消化,完美实现 “削峰填谷”。

第二梯队:热点防控(热点参数限流)

核心思想在于精准打击,只拦截 “罪魁祸首”。在实际生产中,流量往往不是均匀分布的,而是高度集中的。例如 99% 的请求都在疯狂抢购某一个小鲜肉的单品(热点商品 ID),而其他商品无人问津。Sentinel 会动态统计方法的入参。如果发现请求的参数中,某个特定的 String 或 Long 值的并发量高得离谱,Sentinel 会单独对这个热点参数值进行降维限流,而携带其他参数值的健康请求依然可以 100% 正常通行。避免了因为极个别爆款商品的流量飙升,导致整个平台的普通交易也跟着一起陪葬。

第三梯队:物理防爆(隔离机制)

当流量已经进入系统内部,并且某个底层依赖(如第三方接口、慢 SQL 数据库)开始变慢、卡顿时,我们必须防止当前服务的全局线程池被耗尽。Sentinel 会使用信号量隔离(基于并发线程数控制)技术,在当前统一线程池内部通过计数器(信号量)进行总数控制。比如规定 “查询用户头像” 接口最多只能同时占用 10 个线程。一旦该接口卡死、10 个线程全部处于阻塞状态,第 11 个请求过来时会瞬间被拒绝。暂时牺牲了这 1 个非核心接口,成功死死保住了全局 Tomcat 线程池剩下的几百个可用线程去跑核心交易。

第四梯队:熔断断路(熔断与降级)

当故障已经在下游彻底爆发(如局部宕机或数据库超时)时,上游服务必须展现出最高级别的 “生存自愈能力”。

  • 熔断(Circuit Breaking):Sentinel 会实时监控调用链的健康度。
    • 慢调用比例熔断:如果发现过去一段时间内,响应时间(RT)超过 500ms 的慢调用比例达到了 50%,熔断器瞬间开启(Open)。
    • 异常比例 / 异常数熔断:如果接口调用的报错率、或者报错绝对数量越过红线,同样触发熔断。
    • 熔断效果:在熔断期内,上游请求不再去调卡死的下游,而是直接在入口处拦截。
  • 降级(Fallback):熔断触发后(或者限流触发后),项目必须执行优雅的降级。通过预先写好的 Fallback 方法,给前端返回本地缓存、默认静态 JSON 或温馨的用户提示(如“前方拥堵,请稍后再试”),从而实现服务虽然局部不可用,但整体体验不断档的闭环。

总结起来,实际中 Sentinel 的防护剧本一般是这样的:激增流量来了,先通过【网关流控与整形】过滤掉不健康的脉冲; 进入微服务后,通过【热点流控】按参数精准拆弹; 内部调用时,通过【信号量隔离】确保全局线程池不被打满; 下游一旦发生超时,通过【熔断】及时切断灾难蔓延链条,并用【降级】吐出友好提示。


Sentinel 基础使用案例

我们在之前 演示项目 的基础上新建一个干净的 zdemo-sentinel 模块来测试 Sentinel 的各种功能。

依赖配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zdemo.scloud</groupId>
<artifactId>zdemo-scloud-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>zdemo-sentinel</artifactId>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>


配置文件

src/main/resources/application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 8401

spring:
application:
name: zdemo-sentinel

cloud:
sentinel:
transport:
# 开启控制台的连接地址
dashboard: 127.0.0.1:8080
# 与控制台交互的本地心跳端口(默认8719,如果被占用会自动+1扫描)
port: 8719


启动类

1
2
3
4
5
6
@SpringBootApplication
public class SentinelTestApp {
public static void main(String[] args) {
SpringApplication.run(SentinelTestApp.class, args);
}
}


业务接口

OrderTestController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.zdemo.scloud.block.OrderBlockHandler;
import com.zdemo.scloud.fallback.OrderFallbackHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderTestController {

/**
* value代表资源名称(Sentinel控制台里认这个名字)
* 如果同时配置并触发了 block 和 fallback,那么 block 的优先级要高于 fallback
*/
@GetMapping("/order/create")
@SentinelResource(value = "createOrderResource",
// 挂载流控/熔断拦截器(防外部流量)
blockHandlerClass = OrderBlockHandler.class, blockHandler = "handleOrderBlock",
// 挂载业务异常兜底器(防内部代码Bug/中间件猝死)
fallbackClass = OrderFallbackHandler.class, fallback = "handleOrderFallback"
)
public String createOrder(@RequestParam("id") String id) {
// 故意制造一个运行期异常,模拟代码有 Bug 或是数据库挂了
if ("500".equals(id)) {
int i = 1 / 0;
}
return "【订单服务】-> 订单生成成功!订单编号: " + id;
}
}

UserTestController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
public class UserTestController {

/**
* 类内部的默认业务兜底(不建议写在业务类中,强烈建议统一写到统一的 BlockHandler)
*/
public String handleCustomBlock(Long id, BlockException e) {
return "【局部精准流控】-> 访问用户详情太快了!id=" + id;
}

@GetMapping("/user/get")
@SentinelResource(value = "getUser", blockHandler = "handleCustomBlock")
public String getUser(@RequestParam("id") Long id) {
return "用户详情数据";
}

@GetMapping("/user/list")
@SentinelResource(value = "listUsers")
public String listUsers() { // 走默认的全局 fallback
throw new RuntimeException("数据库连接超时!");
}
}


业务级别的 BlockHandler 和 FallbackHandler

OrderBlockHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;

/**
* 订单统一流控/降级异常特殊处理器
*
* 规范警示:严禁在 Controller 业务类里直接写 blockHandler 方法!如果每个接口的兜底逻辑都和业务代码塞在同一个类里,会导致代码迅速滑向“屎山”深渊。
* 标准做法:将所有的流控、降级兜底逻辑,统一抽离到独立的 handler 包中,并通过 static 静态方法提供服务。
*/
public class OrderBlockHandler {

/**
* 注意事项:
* 1. 方法必须是 public static
* 2. 返回值类型必须与原业务接口一致
* 3. 参数列表最后必须紧跟 BlockException 证明身份
*/
public static String handleOrderBlock(String id, BlockException exception) {
if (exception instanceof FlowException) {
return "【Sentinel 阻击成功】-> 触发流控规则:当前下单流量过大,请稍后再试!单号: " + id;
}
if (exception instanceof DegradeException) {
return "【Sentinel 熔断保护】-> 触发降级规则:下游依赖接口响应超时,服务已熔断!";
}
return "【Sentinel 异常拦截】-> 系统处于自我保护状态。";
}
}

OrderFallbackHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 订单统一业务代码异常/故障兜底处理器
*
* 规范警示:严禁在 Controller 业务类里直接写 fallbackHandler 方法!
* 标准做法:将所有的 fallback 逻辑,统一抽离到独立的 handler 包中,并通过 static 静态方法提供服务。
*/
public class OrderFallbackHandler {

/**
* 注意事项:
* 1. 方法必须是 public static
* 2. 返回值类型必须与原业务接口一致
* 3. 参数列表最后必须紧跟 Throwable t 承接运行期异常
*/
public static String handleOrderFallback(String id, Throwable t) {
// 生产规范:这里通常要记录核心异常日志,或者进行告警
System.err.println("【业务层级联崩溃】-> 订单服务内部发生致命未知异常: " + t.getMessage());
return "【Fallback 业务自愈】-> 抱歉,当前订单系统内部出现技术故障(如空指针/DB异常)," +
"我们已启动微服务应急保障方案!单号: " + id;
}
}


全局默认的 BlockHandler 和 FallbackHandler

对于 BlockHandler 这里需要特别注意,在 Spring Cloud Alibaba 中,Sentinel 收集和拦截流量其实是通过两条完全独立、互不相通的流水线来实现的(在 sentinel 控制台的 “实时监控” 也会看到对应的两条资源名称不同,但曲线一样的监控):

  • 流水线 A:全局 WebMVC 拦截器(网关/HTTP 链路)。当你引入 spring-cloud-starter-alibaba-sentinel 后,系统会自动拉起一个针对所有 HTTP 请求的 SentinelWebInterceptor。它在 Sentinel 控制台里注册的资源名称,是你的请求路径(URI),比如 /order/create。代码里实现的全局的 GlobalHttpBlockExceptionHandler 接口,百分之百只为这条流水线服务。
  • 流水线 B:@SentinelResource 注解切面(Aspect 链路)。是基于 AOP 切面技术实现的,是你手工指定的自定义别名,即 createOrderResource。它只认你在注解里写的 blockHandler。如果你在注解里没有写 blockHandler,当触发流控时,切面内部会直接抛出 BlockException,然后由于它发现你没配局部的解耦处理器,它就会越过全局处理器,直接原地炸开,将异常原封不动往上抛,甚至直接被框架降级成了普通的 500 或者默认文本。

所以,全局 GlobalHttpBlockExceptionHandler 只能拦截形如资源名叫 /order/create 的流控事件,对于 @SentinelResource 设置的资源名 createOrderResource 的流控规则,它压根就不认识。所以如果想要使用 GlobalHttpBlockExceptionHandler 进行兜底,推荐把 Controller 上的 @SentinelResource 阉割掉,只保留 fallback 抓 Bug,把流控的权力完全上交给全局大管家。

注意,BlockExceptionHandler 是专门用来拦截原生 HTTP Web 请求的,对于 Dubbo RPC ,它们走的是完全不同的两套交通网络。(整合 dubbo 会有其他不同的处理方法,下面会介绍)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;

/**
* 全应用所有 HTTP 接口的统一默认流控/降级处理器
*
* 优先级排位:接口独立的 blockHandler > 全局 BlockExceptionHandler 实现类
* 如果你在某个高价值接口上单独手写了 @SentinelResource,它依然会优先听从你手写的独立指挥;
* 而那些没写的普通接口,则会全量自动滑入 GlobalHttpBlockExceptionHandler 这张安全防护大网中。
*/
@Slf4j
@Component
public class GlobalHttpBlockExceptionHandler implements BlockExceptionHandler {

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, String resourceName, BlockException e) throws Exception {
log.info("blockException rule:{}", e.getRule());

// 1. 设置标准的 JSON 返回响应头和状态码(生产规范:触发流控通常返回 429 Too Many Requests)
response.setStatus(429);
response.setContentType("application/json;charset=utf-8");

// 2. 精准研判异常类型,给前端吐出极其规范的工业级错误码
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("path", request.getRequestURI());

if (e instanceof FlowException) {
errorResult.put("code", 1001);
errorResult.put("msg", "【全局流控】服务器当前排队人数过多,请稍后再试!");
} else if (e instanceof DegradeException) {
errorResult.put("code", 1002);
errorResult.put("msg", "【全局熔断】下游微服务链路出现卡顿崩溃,服务已紧急降级!");
} else if (e instanceof AuthorityException) {
errorResult.put("code", 1003);
errorResult.put("msg", "【权限拒绝】您的 IP 或黑白名单校验未通过,拒绝访问!");
} else {
errorResult.put("code", 1999);
errorResult.put("msg", "【防护拦截】触发了系统自适应保护限制!");
}

// 3. 将 Map 转换成标准的 JSON 字符串吐给前端
new ObjectMapper().writeValue(response.getWriter(), errorResult);
}
}


// 删掉 value 属性,这意味着当前接口的流控资源名将回归它本来的面目:"/order/create"(真正的流控资源名)
@GetMapping("/order/create"
@SentinelResource(
fallbackClass = OrderFallbackHandler.class,
fallback = "handleOrderFallback" // 只保留 fallback 用于防止代码 Bug 崩溃
)
public String createOrder(@RequestParam("id") String id) {...}

GlobalHttpFallbackExceptionHandler:对于普通的 Controller 接口来说,业务抛出的未捕获异常最终都会上浮到 Spring MVC 层。我们完全可以脱离 Sentinel 的注解,直接利用 Spring 生态原生的 @RestControllerAdvice 来做全应用的兜底。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import java.util.HashMap;
import java.util.Map;

/**
* 全应用所有 HTTP 接口的默认业务异常(Fallback)处理器
*/
@RestControllerAdvice
public class GlobalHttpFallbackExceptionHandler {

/**
* 精准拦截:拦截 Spring Boot 3.x 找不到静态资源(如 favicon.ico)的特殊 404 异常
* 将其提前在第一道防线消耗掉,绝不触发底层的 500 严重业务坍塌报警!
*/
@ExceptionHandler(NoResourceFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Map<String, Object> handleStaticResourceNotFound(NoResourceFoundException e) {
// 悄悄静默处理,打印一句轻量级的 debug 或 info 即可,绝对不报红、不触发警报
System.out.println("【静态资源未找到放行】用户请求了不存在的资源: " + e.getResourcePath());

Map<String, Object> result = new HashMap<>();
result.put("code", 4004);
result.put("msg", "您访问的静态资源或小图标不存在:" + e.getResourcePath());
return result;
}

/**
* 终极兜底:拦截全盘所有未捕获的致命业务异常(真正的线上业务塌陷)
*/
@ExceptionHandler(Throwable.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 返回标准的 500 状态码
public Map<String, Object> handleDefaultException(Throwable t) {
// 只有走到这里的,才算真正的致命错误,触发钉钉/企业微信报警!
System.err.println("【线上业务全面塌陷兜底】原因: " + t.getMessage());
t.printStackTrace(); // 打印堆栈方便排查

Map<String, Object> errorResult = new HashMap<>();
errorResult.put("code", 5003);
errorResult.put("msg", "【系统级默认自愈】很抱歉,业务大管家开小差了,底层服务由于未知异常发生假死,正在紧急抢修中!");
errorResult.put("error", t.getMessage());
return errorResult;
}
}


效果验证

启动 Sentinel Dashboard:

1
$ java -jar sentinel-dashboard.jar

由于 Sentinel 采用懒加载机制,启动后直接看控制台是一片空白的。你需要率先在浏览器里手动访问一次接口以唤醒心跳。

1
$ curl http://127.0.0.1:8401/order/create?id=1001

此时刷新 Sentinel 控制台,你会发现左侧导航栏神奇地出现了 zdemo-sentinel 的名字。依次点击 “族谱/流控规则” -> “新增流控规则”:

  • 资源名: createOrderResource (必须和代码里定义的value完全一致)
  • 单机QPS阈值: 1 (一秒钟只允许通过1个请求),保存。

现在回到浏览器,把你的手速拉满,疯狂刷新 /order/create 接口:

  • 第一下点击(处于安全水位):【订单服务】-> 订单生成成功!订单编号: 1001
  • 第二下闪电点击(并发越过红线):页面瞬间被拦截,并被 OrderBlockHandler 处理返回。


实际 Sentinel Deshboard 玩法

直接启动的风险

在实际生产环境中,Sentinel Dashboard绝对不会像我们在本地开发那样,简单地执行一句 java -jar 挂在后台就完事了。因为这样执行会导致:

  • 内存流失(配置乱丢):官方原版的控制台,所有流控、熔断规则全部保存在微服务的内存(Memory)中。只要微服务一重启,或者控制台一挂,你在网页上辛辛苦苦配的所有规则瞬间全部灰飞烟灭!
  • 裸奔风险:默认的用户名和密码都是 sentinel,这在生产的内网或外网环境下等同于给黑客开门。

实际中,我们需要修改 Sentinel 配置,利用 Nacos 作为 “持久化配置中心”,实现规则的持久化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌─────────────────────────┐
│ Sentinel Dashboard │
└────────────┬────────────┘
│ 1. 网页上修改规则
▼ (通过 Nacos Config API 写入)
┌─────────────────┐
│ Nacos Config │ ◄─── (终身持久化在 DB 中)
└────────┬────────┘

│ 2. 配置发生变更,实时广播推送 (Config Change)

┌───────────────────────┐
│ zdemo-sentinel 微服务 │ (内存中即时生效)
└───────────────────────┘


实际的玩法

服务端配置

步骤 1:下载官方基础 Jar 包

1
2
$ mkdir -p /usr/local/sentinel && cd /usr/local/sentinel
$ wget https://github.com/alibaba/Sentinel/releases/download/1.8.8/sentinel-dashboard-1.8.8.jar

步骤 2:启动命令(包含鉴权与 JVM 调优)。严禁裸奔启动!在生产中,我们必须强制修改默认的密码、定制通信端口,并给予其足够的堆内存支撑滑动窗口算法的内存统计。

在服务器上创建启动脚本 start.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash

JAVA_OPT="-server -Xms512m -Xmx512m -Xmn256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m"
JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:MaxGCPauseMillis=200"

# 1. 指定控制台自身的 HTTP 端口(默认 8080,生产通常建议修改以防冲突)
SERVER_PORT=8858

# 2. 生产保命:强制修改默认登录用户名和密码
AUTH_USER=owlias
AUTH_PASS=owlias

# 3. 执行后台无感启动并输出日志
nohup java ${JAVA_OPT} \
-Dserver.port=${SERVER_PORT} \
-Dsentinel.dashboard.auth.username=${AUTH_USER} \
-Dsentinel.dashboard.auth.password=${AUTH_PASS} \
-jar sentinel-dashboard-1.8.8.jar > sentinel.log 2>&1 &

echo "Sentinel Dashboard 正在后台启动,端口: ${SERVER_PORT}..."

执行 chmod +x start.sh && ./start.sh 开启大闸。

注意:启动 sentinel 之前,需要保证其主机上的时间和微服务上的时间保证是同步的。如果不同步,很有可能 sentinel 的实时监控是不可用的!


客户端配置

控制台安装好后,为了实现上面提到的 Nacos 规则持久化,我们的微服务代码和配置必须全面升级,引入 Nacos 数据源(DataSource)依赖。

升级子模块 pom.xml:引入 Sentinel 扩展数据源:

1
2
3
4
5
6
7
8
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

升级 application.yml:配置持久化监听大网。我们要告诉微服务,不要再死等控制台发指令了,直接去 Nacos 上监听一个叫 zdemo-sentinel-flow-rules 的流控配置文件!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
server:
port: 8401

spring:
application:
name: zdemo-sentinel

cloud:
sentinel:
# 强迫它在启动时就完成跟控制台的生死握手。
# eager: true
transport:
# 开启控制台的连接地址
dashboard: 192.168.1.149:8858
# 与控制台交互的本地心跳端口(默认8719,如果被占用会自动+1扫描)
# 启动后当你在微服务本地执行 netstat -an | grep 8719,并不会看到 8719 监听端口被拉起
# 只有当微服务第一次接收到外部 HTTP 业务请求时,Spring Cloud 才会真正触发 Sentinel 初始化切面
# 这时候它才会轰然苏醒,把底层的 Netty 服务器拉起来,去监听 8719 端口,并向远端控制台报到。
port: 8719
# 生产级救命参数:强行指定当前微服务对外的真实局域网 IP,确保 sentinel 那台机器能够回弹访问到你这个 IP 的 8719 端口
client-ip: 192.168.1.2
# 配置持久化数据源
datasource:
# 1. 自定义流控规则数据源名称
flow-rules:
nacos:
server-addr: 192.168.1.149:8848 # Nacos 的大本营地址
username: nacos
password: nacos
dataId: ${spring.application.name}-flow-rules.json # 监听的 Data ID
groupId: SENTINEL_GROUP # 约定的分组
namespace: prod-zdemo # 约定的命名空间
data-type: json # 配置内容格式
rule-type: flow # 明确这是【流控规则】

在 Nacos 上配置并发放流控规则配置。由于我们将数据源锚定在了 Nacos 上,现在我们直接前往 Nacos 控制台网页(8848),点击 “新建配置”:

  • Data ID: zdemo-sentinel-flow-rules.json
  • Group: SENTINEL_GROUP
  • 配置格式: JSON

配置内容如下:

1
2
3
4
5
6
7
8
9
10
11
[
{
"resource": "createOrderResource",
"limitApp": "default",
"grade": 1,
"count": 2,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
  • resource: 你的核心眼线!必须对应 Controller 里的 @SentinelResource(value = “createOrderResource”)
  • grade: 阈值类型。1 代表通过 QPS 进行限流,0 代表通过并发线程数限流。
  • count: 限制数量。2 代表每秒钟最多允许通过 2 个请求。
  • controlBehavior: 流控效果。0 代表直接快速失败(抛出 BlockException)。


查看效果

  • 唤醒服务:启动你的 zdemo-sentinel 微服务,在浏览器访问一次:http://127.0.0.1:8401/order/create?id=1001
  • 登录新控制台:打开 http://192.168.1.149:8858 ,登录 owlias / owlias。
  • 登录进去后,点击 zdemo-sentinel 的 “流控规则” 菜单(在访问一次 /order/create 接口之后)。你会发现在压根没有在 Sentinel 网页上配过任何规则的情况下,里面已经存在着一条针对 createOrderResource、QPS 为 2 的流控限制,这条规则正是 Sentinel 自动从 Nacos 里面拽过来的。


可以优化的地方

需要注意的是,上述流控规则的数据是单向流动的,只能通过配置 Nacos 同步到 Sentinel。你在 Sentinel 页面设置的规则是无法自动同步到 Nacos 的!Sentinel 控制台底层的默认行为是通过 Sentinel 客户端暴露的 8719 端口,直接把规则强行塞进微服务的内存里,此时,这条规则完全绕过了 Nacos。这就导致了两个极其致命的后果:

  • Nacos 控制台里一片空白,根本看不到你在 Sentinel 网页上配的规则。
  • 只要微服务一重启,微服务内存清空,它会重新去 Nacos 拉取配置。而 Nacos 里什么都没有,你在 Sentinel 网页上配的规则会瞬间人间蒸发。

在真正的商业生产环境中,大厂架构师绝对无法容忍 “配个规则还要手写 JSON 贴进 Nacos” 这种低效且易错的操作。我们必须实现:在 Sentinel Dashboard 页面修改 ──► 自动同步保存到 Nacos ──► 再广播给微服务。要做到这一点 improve 体验,业界有以下两种标准演进姿势:

  • 姿势一:去 Sentinel Dashboard 化,全量在 Nacos 中维护(最稳妥、成本最低)。严禁在 Sentinel 网页上点 新增或修改。所有的流控、熔断规则,一律由运维或开发人员在 Nacos 通过新建/修改 JSON 配置文件来下发。Sentinel 网页仅仅用来查看流量图表和监控数据。
  • 姿势二:魔改 Sentinel Dashboard 源码,官方的 Sentinel Dashboard 其实留了扩展接口(DynamicRuleProvider 和 DynamicRuleExporter)。大厂通常会下载 Sentinel 官方控制台的源码,进行微量魔改后重新打包部署。

在你没有完成姿势二的源码魔改之前,请牢记:在 Sentinel 控制台页面上配置的规则全是 “临时体验卡”,微服务重启即死;只有在 Nacos 配置文件里落地的 JSON 规则,才是 “永久的免死金牌”。那么有没有一个折中的方法,能够让我们方便配置 nacos 中的流控元数据呢?其实有的:

由于你目前使用的官方原版 Sentinel 控制台不支持自动同步,如果你觉得手写 JSON 容易错漏,完全可以先在 Sentinel Dashboard 网页上用图形化界面把规则配好,然后利用 Sentinel 提供的隐藏 HTTP 接口将规则全量导出成 JSON,最后粘进 Nacos。Sentinel 控制台本身没有提供“导出”按钮,我们需要利用 Sentinel 客户端(即你的本地微服务)内置的 Actuator / Sentinel 监控端点来直接提取内存中的规则。

1
$ curl http://127.0.0.1:8401/actuator/sentinel

如果你的 Spring Boot 3.3.0 项目没有全量暴露 Actuator,也可以直接访问 Sentinel 核心监听端口(默认 8719)的原生接口(推荐,更干净):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 普通流控规则
$ curl http://127.0.0.1:8719/getRules?type=flow

# 热点规则
$ curl "http://127.0.0.1:8719/getParamFlowRules"

# 熔断规则
$ curl http://127.0.0.1:8719/getRules?type=degrade

# 授权规则
$ curl http://127.0.0.1:8719/getRules?type=authority

# 系统设置规则
$ curl http://127.0.0.1:8719/getRules?type=system


降级规则的玩法

上述 Sentinel 安全防护我们都是通过流控规则进行演示的。在实际中,降级(Degrade,通常指熔断降级)是比流控更高一级的防线。流控是 “防外部流量暴击”,而降级则是 “防下游依赖猝死”。当微服务调用的下游接口(如数据库、第三方支付网关、兄弟微服务)出现假死、严重卡顿或频繁报错时,熔断器会果断切断调用链路(合闸变红灯),直接就地返回兜底数据,避免当前服务被下游活活拖死,从而阻止雪崩。

Sentinel 支持三种熔断策略:慢调用比例 (RT)异常比例异常数。在生产中,慢调用比例(根据响应时间熔断)是用得最多、最经典的场景。下面我们来写一个完整的生产级熔断防护案例。


编写测试接口

我们新写一个接口来模拟下游系统响应极其缓慢(比如平时只需 10ms,现在因为死锁需要 500ms)的惨状。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 生产规范:
* 为了让代码最纯净,我们不在此处通过注解配置任何 degradeHandler。
* 流控和熔断的拦截统一走上面我们写好的全局 GlobalHttpBlockExceptionHandler
*/
@GetMapping("/order/degrade")
public String degradeTest(@RequestParam("mockType") String mockType) {
// 模拟下游服务卡顿:当参数为 "slow" 时,强行让线程睡 500 毫秒
if ("slow".equals(mockType)) {
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return "【订单内部检查】下游链路一切正常!";
}


在Nacos编写熔断规则

根据我们前面打通的持久化链路,我们直接去 Nacos 中配置熔断规则。由于我们在 application.yml 里可以配置多个规则数据源,大厂规范通常会将流控和降级拆开监听。为了演示方便,你可以直接在 application.yml 中追加一个降级数据源,或者直接写在同一个大网里。这里我们直接在 Nacos 的 prod-zdemo 命名空间下,为降级规则创建一个标准的持久化文件。

升级 application.yml 引入降级规则监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spring:
cloud:
sentinel:
datasource:
# 保留原有的 flow-rules ...
# 新增:降级规则数据源名称
degrade-rules:
nacos:
server-addr: 192.168.1.149:8848
username: nacos
password: nacos
dataId: ${spring.application.name}-degrade-rules.json # 监听降级配置文件
groupId: SENTINEL_GROUP
namespace: prod-zdemo
data-type: json
rule-type: degrade # 明确声明这是【降级规则】

在 Nacos 上新建配置:

  • Data ID: zdemo-sentinel-degrade-rules.json
  • Group: SENTINEL_GROUP
  • 配置格式: JSON

配置内容(生产标准:慢调用熔断配置):

1
2
3
4
5
6
7
8
9
10
11
[
{
"resource": "/order/degrade",
"grade": 0,
"count": 200,
"timeWindow": 10,
"minRequestAmount": 5,
"slowRatioThreshold": 0.6,
"statIntervalMs": 1000
}
]

  • grade: 熔断策略。0 代表慢调用比例,1 代表异常比例,2 代表异常数。
  • count: 慢调用临界点(RT 阈值)。这里填 200,意味着只要响应时间超 200 毫秒,就被定义为一次慢调用。
  • slowRatioThreshold: 慢调用比例阈值。这里填 0.6(即 60%)。
  • minRequestAmount: 熔断触发的最小请求数。在一秒内,必须至少有 5 个请求进来,才会开始计算比例。防止只有 1 个请求慢就误判熔断。
  • statIntervalMs: 统计时长。默认 1000 毫秒(1秒)。
  • timeWindow: 熔断时长(合闸持续时间)。这里填 10 秒。

在 1 秒钟之内,如果访问 /order/degrade 的请求达到了 5 个以上,并且其中响应时间超过 200ms 的慢调用请求占到了 60% 以上,Sentinel 会轰然拉闸!在接下来的 10 秒钟(timeWindow) 内,任何进来的请求都不会再去调用 Controller 内部的代码,而是直接原地斩断,抛出 DegradeException。


效果验证

  • 正常访问(水位安全):启动微服务,在浏览器访问:http://127.0.0.1:8401/order/degrade?mockType=normal 。无论怎么刷,接口都在几毫秒内秒回正常结果。

  • 疯狂注入卡顿流量(触发熔断):现在我们利用工具疯狂请求:

    1
    2
    $ for i in {1..10}; do curl -s "http://127.0.0.1:8401/order/degrade?mockType=slow" & done
    $ for i in {1..30}; do curl -s "http://192.168.1.2:8401/order/create?id=1001" && sleep 0.2; done
    • 前几次点击,由于线程被 sleep(500),页面会转圈圈卡顿 0.5 秒才返回。
    • 当你在 1 秒内连续点了 5 次以上时,Sentinel 的滑动窗口瞬间捕捉到了“慢调用已经超过 60%”的致命危险,熔断器瞬间开启!
    • 如果过了10秒以上的一段时间,熔断器会进入 半开(Half-Open) 状态。放行一次请求,如果该请求依然卡顿,则继续熔断 10 秒;如果该请求恢复正常,则闭合闸门(绿灯放行),全盘恢复正常生产!


其他高级的流控模式

在真实的工业级高并发场景中,很多流量并不是孤立存在的。它们之间要么存在资源争抢,要么存在调用链交织。除了最常用的默认的直接模式外,Sentinel 还提供了极其强悍、能够应对复杂业务纽带的高级流控模式:关联模式(Associated) 和 链路模式(Link)。


关联模式

典型生产案例:在电商大促时,“下订单(Write)” 和 “修改购物车(Write)” 都会疯狂地争抢和高频读写底层的数据库连接池或同一张用户订单核心表。下订单是直接产生 GMV(交易额)的黄金链路,而修改购物车属于次要链路。如果订单系统快撑不住了,我们应该限制 “修改购物车”的流量,把所有的硬件资源(数据库、CPU)腾出来全量喂给“下订单”。关联模式的精髓:当关联资源(下订单)达到设定的阈值时,限流当前资源(修改购物车)。 也就是典型的 “丢车保帅”。下面我们来写一个典型的案例 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
public class CartAndOrderController {

/**
* 资源 A:修改购物车(次要业务)
* 它是被限制的对象!
*/
@GetMapping("/cart/update")
public String updateCart() {
return "购物车修改成功!";
}

/**
* 资源 B:下订单(黄金核心业务)
* 它是监控的源头!
*/
@GetMapping("/order/save")
public String saveOrder() {
return "订单创建成功,黄金链路畅通!";
}
}

Nacos 持久化规则 JSON 配置:

1
2
3
4
5
6
7
8
9
10
11
12
[
{
"resource": "/cart/update", # 被流控的资源
"limitApp": "default",
"grade": 1,
"count": 5,
"strategy": 1, # 1 代表【关联模式】
"refResource": "/order/save", # 被关联,关联的资源名叫下订单
"controlBehavior": 0,
"clusterMode": false
}
]

配置完成之后,你会发现,当你疯狂刷单的时候,修改购物车就会被限流:

1
2
# 疯狂刷单,浏览器访问修改购物车就会被限流
$ for i in {1..30}; do curl -s "http://192.168.1.2:8401/order/save" && sleep 0.2; done

你会注意到,流控规则设置的时候,有一个 “是否集群” 的选项。它决定了你的限流大闸到底是 “各自为政、单兵作战”,还是 “全局统筹、联合作战”。不勾选(单机模式,默认)阈值是针对 单台服务器(单个实例)的。勾选(集群模式)阈值是针对整个微服务集群(所有实例的总和)的。生产环境的建议:

  • 普通的无状态接口、流量巨大且均匀,只选择单机模式。
  • 网关层、防刷恶意流量,只选择单机模式。网关通常多实例部署且流量极高,单机限流足够应对。
  • 秒杀倒计时、总量必须极其精确,需要选择集群模式。单机限流无法卡死绝对总量,只有集群限流能做到 “说放 300 个,就绝对不会漏进来第 301 个”。
  • 下游依赖的第三方接口有严格的 QPS 计费/频次限制,必须开启集群模式。比如调用某第三方征信接口,对方限制我方全网每秒只能调 50 次。此时必须用集群流控锁死 50,否则多调一次就会面临高额罚款或被对方拉黑。


链路模式

典型生产案例:在微服务内部,Service 层有一个公共的方法 getUserInfo()(标记为了 Sentinel 资源)。现在有两个上游 Controller 都在调用它,一个是 “核心秒杀接口”,另一个是 “日常后台导出报表接口”。如果有一天,后台导出报表接口疯狂调用 getUserInfo() 导致该公共底层快要超负荷了,我们希望能只限制 “报表接口 -> getUserInfo” 这条链路,而绝对不能影响 “秒杀接口 -> getUserInfo” 这条链路。链路模式的精髓:只针对从指定 “入口资源(Context)” 进来的流量进行精准限流,其他入口进来的流量网开一面。

为了让 Sentinel 能够识别出 Service 层的调用链路,我们需要在 application.yml 中关闭新版 Sentinel 默认的 “入口收敛” 行为,否则所有链路都会被强行捏合在一起:

1
2
3
4
5
spring:
cloud:
sentinel:
# 强制开启链路展开配置:必须显式设置为 false,否则链路模式绝对无法生效!
web-context-unify: false

编写 Controller 层与共同的 Service 层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@RestController
public class BusinessController {
@Autowired
private UserService userService;

// 入口一:高贵的秒杀链路
@GetMapping("/api/seckill")
public String seckill() {
return "秒杀通过 -> " + userService.getUserInfo();
}

// 入口二:低优先级的报表链路
@GetMapping("/api/report")
public String report() {
return "报表导出 -> " + userService.getUserInfo();
}
}


@Service
public class UserService {
/**
* 将底层的公共方法标记为核心受保护资源
* 上述我们已经介绍,一旦使用 @SentinelResource,就不会使用全局默认的 block 处理了,需要自己指定。
*/
@SentinelResource(value = "commonGetUserInfo", blockHandler = "getUserInfoBlockHandler")
public String getUserInfo() {
return "[UserInfo] 拿到核心用户信息";
}

public String getUserInfoBlockHandler(BlockException e) {
return "[UserInfo] getUserInfo 触发 BlockHandler,rule: " + e.getRule();
}
}

Nacos 持久化规则 JSON 配置:

1
2
3
4
5
6
7
8
9
10
11
12
[
{
"resource": "commonGetUserInfo", // 对哪个资源设置流控规则
"limitApp": "default",
"grade": 1,
"count": 2,
"strategy": 2, // 2 代表【链路模式】
"refResource": "/api/report", // 死死限定入口必须是报表接口
"controlBehavior": 0,
"clusterMode": false
}
]

现在再来进行测试,发现:

  • 无论你用多高的并发去轰炸 /api/seckill,底层的 commonGetUserInfo 绝对不会触发限流。
  • 当你以超过每秒 2 次的速度去刷新 /api/report 时,由于它命中了限流大网,接口会瞬间触发限流!

这就是链路模式的精妙之处。它实现了微服务内部深处、细粒度到方法级别的局部定向精准定点清除,既保护了底层公共方法,又做到了对核心高优业务的“绝对零打扰”!


其他高级流控效果

除了默认的 “快速失败”(直接拒绝)外,Sentinel 还提供了两个极其高雅、充满弹性哲学的高级流控效果:暖起步模式(Warm Up) 和 排队等待模式(Rate Limiter)。在生产环境中,普通的快速失败就像一堵生硬的混凝土墙,流量超标直接撞碎;而这两个高级效果更像是变速箱和蓄水池,用更平滑的手段化解高并发的物理冲击。


Warm Up 模式

典型生产案例:秒杀刚刚开启,或大批新实例刚刚加入集群(主要针对洪峰流量的冲击)。线上某核心接口平时冷冷清清(QPS 只有个位数),系统底层的各类缓存(Redis、JVM本地缓存)大面积处于空虚或冷态。大促秒杀时间一到,几万 QPS 的洪峰在 0.5 毫秒内瞬间拉满砸下来。虽然你给接口设的单机大闸是 1000 QPS,但由于底层缓存全冷,这 1000 个并发会毫无阻拦地直接击穿到数据库上。数据库由于连接池还没扩容、索引还没热,瞬间被活活憋死,导致整个微服务在刚开局的几秒钟内直接瘫痪。

暖起步的精髓就是拒绝一蹴而就! 当流量突然飙升时,系统会让通过的 QPS 限额从一个很低的值(默认是 阈值 / 3)开始,在指定的预热时长内,像高架桥汽车起步一样,慢慢平滑地拉升到设定的最大阈值。

作为测试,我们对 /order/seckill 接口配置一个最大 60 QPS 的大闸,并给它 10 秒钟的 “苏醒时间”。

1
2
3
4
5
6
7
8
9
10
11
[
{
"resource": "/api/seckill",
"limitApp": "default",
"grade": 1,
"count": 60, // 最终达到的最大预热阈值:60 QPS
"strategy": 0,
"controlBehavior": 1, // 修改:1 代表【Warm Up / 预热效果】
"warmUpPeriodSec": 10 // 修改:预热时长设为 10 秒
}
]

测试发现:

  • 利用 ab 压测工具突然以 100 QPS 的并发轰炸该接口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # CentOS / RHEL / CentOS Stream (你的 host01 服务器)
    yum install -y httpd-tools
    # Ubuntu / Debian
    sudo apt-get update && sudo apt-get install apache2-utils

    # 每隔 1 秒,自动化砸出一次 1000 次请求的暴击,连续轰炸 20 次(即 20 秒)
    for i in {1..20}; do
    ab -n 1000 -c 50 "http://192.168.1.2:8401/api/seckill"
    sleep 1
    done
  • 在刚启动的第 1~3 秒,由于默认冷启动因子是 3,系统大闸其实被死死压在 60 / 3 = 20​ QPS。超过 20 的请求全部被限流拒绝。

  • 随着时间推移(第 4~8 秒),大闸慢慢从 20 提到 30、45……

  • 到第 10 秒之后,系统彻底被 “暖热”了,缓存建立完毕,连接池拉饱,此时大闸完全放开,平稳维持在 60 QPS。这前几秒的 “残忍拒绝”,给微服务底层赢得了宝贵的建立缓存、预热连接池的黄金缓冲期。


排队等待模式

典型生产案例:极其沉重的 “秒杀写订单” 或 “高德地图海量打卡上报”(主要针对脉冲流量的削峰填谷)。在促销开启的一瞬间,瞬间涌入 500 个写订单的 HTTP 请求。这些请求如果按照默认的快速失败,除了最前面的 20 个被执行外,剩下的 480 个用户会看到冰冷的报错网页,用户体验极差。写订单是一个典型的 IO 密集型重度操作。后台服务器和数据库绝对无法承受在 1 毫秒内并发处理 500 笔写库。但是,服务器的性能虽然没法“瞬间爆发”,却可以“细水长流”。

排队等待的精髓就是把脉冲流量整形成匀速直线! 它是基于漏桶算法实现的。让进来的请求在队列里规规矩矩地排队,严格按照指定的间隔时间(例如每 20ms 放行一个)匀速通过。如果请求在队列里排队的时间超过了指定的超时时间,才会被踢除拒绝。

作为测试,我们基于 /order/submit 接口,严格按照 10 QPS(即每 100 毫秒只能过去一个)的速率匀速通过。如果排队超过 2 秒还没轮到,再放弃。

1
2
3
4
5
6
@GetMapping("/order/submit")
public String submit(@RequestParam("id") String id) throws InterruptedException {
log.info("【订单服务】-> 订单提交进来了:{}", id);
TimeUnit.MILLISECONDS.sleep(100);
return "【订单服务】-> 订单提交成功!订单编号: " + id;
}

Nacos 持久化规则 JSON 配置:

1
2
3
4
5
6
7
8
9
10
11
[
{
"resource": "/order/submit",
"limitApp": "default",
"grade": 1,
"count": 10, // 匀速通过的期望值:10 QPS(意味着每 100ms 匀速放行 1 个)
"strategy": 0,
"controlBehavior": 2, // 修改:2 代表【Rate Limiter / 匀速排队效果】
"maxQueueingTimeMs": 2000 // 修改:最大排队等待超时时间:2000 毫秒(2秒)
}
]

测试发现:

  • 在 1 毫秒内,通过脚本同时发出 15 个请求访问 /order/submit。
  • 第 1 个请求瞬间通过。
  • 第 2~15 个请求没有被粗暴限流!而是全部进入内存队列开始排队。控制台或日志会以极其恐怖的精准度,每隔整整 100ms 吐出一个处理成功的日志(第 100ms、200ms、300ms…),15 个请求在 1.5 秒内全部被妥善、优雅地处理完毕,没有任何一个用户看到报错网页!
  • 如果突然进来了 50 个请求,排队到最后那几个预计等待时间会超过 maxQueueingTimeMs: 2000(2秒),Sentinel 才会把这几个真正超时的请求拉出来执行限流阻断。


专门针对热点的流控规则

在传统的流控中,我们只能针对整个接口(比如 /api/goods)一刀切地设个 100 QPS 的总闸。但现实中,流量往往是严重倾斜的——二八定律在这里体现得淋漓尽致。这里我们以电商大促、明星热搜中最经典的秒杀热点商品保护案例,来演示热点流控规则的设置和使用。

第一步,编写实际的业务接口。 在 Spring Boot 中,我们必须使用 @SentinelResource 来修饰包含热点数据的接口资源,以便捕获传入的特殊变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;

@RestController
public class GoodsController {

/**
* 查看商品详情接口
* @RequestParam("goodsId") 将作为 Sentinel 热点参数的核心研判依据
*/
@GetMapping("/api/goods/detail") // 必须用 @SentinelResource 声明,并且必须指定 blockHandler
@SentinelResource(value = "goods_detail_hotspot", blockHandler = "handleGoodsHotspotBlock")
public Map<String, Object> getGoodsDetail(@RequestParam("goodsId") Long goodsId) {
Map<String, Object> result = new HashMap<>();
result.put("code", 2000);
result.put("data", "正在查看商品ID为 [" + goodsId + "] 的精美详情页...");
return result;
}

/**
* 热点流控专用的兜底降级处理器 (Fallback/Block)
* 注意:参数列表必须和原方法完全一致,且最后必须加上 BlockException
*/
public Map<String, Object> handleGoodsHotspotBlock(Long goodsId, BlockException ex) {
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("code", 4029);
errorResult.put("msg", "【热点突发限流】当前抢购商品 [" + goodsId + "] 的人数过多,服务器正在排队抓取,请稍后再试!");
return errorResult;
}
}

第二步:修改核心配置文件。在 Sentinel 的源码架构中,普通流控规则和热点参数规则是由两个完全不同的组件、用不同的内存 Map 来管理的。分别是 FlowRule 和 ParamFlowRule。由于两者的规则字段、解析器(Converter)和对应的 Java 实体类完全不同,在 Spring Cloud Alibaba 体系中,不能把它们揉在同一个 Nacos 文件里。如果强行塞进同一个 Data ID,Nacos 的 JSON 解析器在反射生成对象时会直接抛出属性不匹配异常,导致所有规则一起瘫痪。标准的做法是在 Nacos 中创建两套独立的 Data ID:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
spring:
application:
name: zdemo-sentinel
cloud:
sentinel:
# 强迫它在启动时就完成跟控制台的生死握手。
eager: true
# 强制开启链路展开配置:必须显式设置为 false,否则链路模式绝对无法生效!
web-context-unify: true
transport:
# 开启控制台的连接地址
dashboard: 192.168.1.149:8858
# 与控制台交互的本地心跳端口(默认8719,如果被占用会自动+1扫描)
# 启动后当你在微服务本地执行 netstat -an | grep 8719,并不会看到 8719 监听端口被拉起
# 只有当微服务第一次接收到外部 HTTP 业务请求时,Spring Cloud 才会真正触发 Sentinel 初始化切面
# 这时候它才会轰然苏醒,把底层的 Netty 服务器拉起来,去监听 8719 端口,并向远端控制台报到。
port: 8719
# 生产级救命参数:强行指定当前微服务对外的真实局域网 IP,确保 sentinel 那台机器能够回弹访问到你这个 IP 的 8719 端口
client-ip: 192.168.1.2
# 配置持久化数据源
datasource:
# 1. 自定义流控规则数据源名称
flow-rules:
nacos:
server-addr: 192.168.1.149:8848 # Nacos 的大本营地址
username: nacos
password: nacos
dataId: ${spring.application.name}-flow-rules.json # 监听的 Data ID
groupId: SENTINEL_GROUP # 约定的分组
namespace: prod-zdemo # 约定的命名空间
data-type: json # 配置内容格式
rule-type: flow # 明确这是【流控规则】
# 2. 自定义降级规则数据源名称
degrade-rules:
nacos:
server-addr: 192.168.1.149:8848
username: nacos
password: nacos
dataId: ${spring.application.name}-degrade-rules.json # 监听降级配置文件
groupId: SENTINEL_GROUP
namespace: prod-zdemo
data-type: json
rule-type: degrade # 明确声明这是【降级规则】
# 3. 自定义热点规则数据源名称
param-rules:
nacos:
server-addr: 192.168.1.149:8848
username: nacos
password: nacos
dataId: ${spring.application.name}-param-rules.json # 监听降级配置文件
groupId: SENTINEL_GROUP
namespace: prod-zdemo
data-type: json
rule-type: param-flow # 明确声明这是热点参数流控!

第三步:Nacos 持久化规则 JSON 配置。

首先新建热点流控配置文件 zdemo-sentinel-param-rules.json(和普通的流控配置文件 zdemo-sentinel-flow-rule.json 相区别),内容如下:

  • 基础规矩:对于普通商品,每个商品 ID 单机每秒只能访问 2 次(大闸拉得极低)。
  • 特权例外(热点例外项):如果传入的第 0 个参数值是 999(爆款商品 ID),则破格将它的阈值单独拔高到 60 QPS。

注意:Nacos 里的配置结构和普通流控不同,热点规则的类路径是 com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule。且 classType 必须写得极其精准,由于我们代码里是 Long goodsId,这里就必须匹配 java.lang.Long,写错一个字母例外项直接失效!

1
2
# 先在 sentinel 中配置好,请求以下接口获取 nacos 配置数据
$ curl "http://192.168.1.2:8719/getParamFlowRules"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[
{
"clusterMode": false,
"resource": "goods_detail_hotspot",
"grade": 1, // 1 代表 QPS 模式
"count": 2, // 基础阈值:普通商品每个 ID 每秒只能点 2 次
"paramIdx": 0, // 核心:对应方法里第 0 个参数(即 goodsId)
"controlBehavior": 0, // 0 代表默认快速失败
"durationInSec": 1,
"limitApp": "default",
"maxQueueingTimeMs": 0,
"regex": false,
"burstCount": 0,
"paramFlowItemList": [
{
"classType": "long", // 参数的完整类名类型
"count": 60, // 特权阈值:破格提升到 60 QPS
"object": "999" // 当 goodsId == 999 时
}
],
"clusterConfig": {
"fallbackToLocalWhenFail": true,
"sampleCount": 10,
"thresholdType": 0,
"windowIntervalMs": 1000
}
}
]

测试发现:

  • 测试 A:轰炸普通商品(goodsId=111),由于命中基础阈值(QPS=2),你会发现返回的报告里绝大多数请求返回了 4029 错误码。

    1
    ab -n 100 -c 10 "http://192.168.1.2:8401/api/goods/detail?goodsId=111"
  • 测试 B:轰炸超级爆款(goodsId=999),由于触发了特权例外项配置,系统的通过 QPS 会在一瞬间蹦到 60 左右!直到多余的 40 个请求超标后才开始报 4029。

    1
    ab -n 100 -c 10 "http://192.168.1.2:8401/api/api/goods/detail?goodsId=999"


其他流控规则

系统规则

SystemRule 不针对某个具体资源/接口,而是从整个 JVM 应用维度监控系统指标,任一指标超标即对全部入口流量(EntryType.IN)触发限流/拒绝,起到”系统级熔断器”的作用,防止高负载导致整机宕机。需要注意的是:

  • 全局唯一:一般只配一条规则,是整机的最后一道防线。
  • 生产推荐组合:CPU 使用率 + 平均 RT + 最大线程数,Load 和 QPS 按需叠加。
  • 触发时抛出 SystemBlockException,可被 @SentinelResource(blockHandler=…) 或者全局 GlobalHttpBlockExceptionHandler 捕获并返回友好提示。


授权规则

按调用方来源(origin)对指定资源做黑白名单访问控制,常用于:

  • 只允许内部网关/指定微服务调用某接口(白名单)
  • 封禁特定调用方(黑名单)

核心配置项:

  • resource:受保护资源名,如 order:query
  • limitApp:调用方 origin 列表,逗号分隔,如 gateway-service,admin-service
  • strategy:
    • RuleConstant.AUTHORITY_WHITE(白名单,仅允许列表内)
    • RuleConstant.AUTHORITY_BLACK(黑名单,拒绝列表内)


集群流控

普通 FlowRule 一般是单机维度限流——每个实例只看自己本地的统计窗口,多实例部署时有明显短板:

  • 总量不精确:10 台 × 单机 10 QPS ≠ 精确全局 100 QPS,扩缩容要手动调阈值
  • 流量倾斜问题:负载不均衡时,某台实例先触顶被限流,但集群总 QPS 还远未达上限

集群流控就是来解决这个问题的——引入一个中心化的 Token Server,统一统计某资源在全集群的调用总量并发放令牌(Token),实现跨实例的精确流量控制。它的工作原理:

1
2
3
4
┌──────────┐    请求 Token    ┌───────────────┐
│ Instance │ ──────────────▶ │ Token Server │ ← 集中统计集群总调用量
│ (Client) │ ◀────────────── │ (Sentinel) │
└──────────┘ pass / block └───────────────┘
  • Token Client:应用实例向 Token Server 申请令牌
  • Token Server:按集群规则(总体阈值 / 均摊阈值)判断是否发放
  • 失败降级:Client 连不上 Server 时可自动退化成本地单机限流(fallbackToLocalWhenFail=true)
  • 部署模式:嵌入模式(某实例兼做 Server)或独立部署模式

实际中建议:集群规则控全局总量,单机规则做各实例兜底保护,互不影响。


Sentinel 整合 OpenFeign

整合案例介绍

我们在原来 Spring Cloud Alibaba 基础案例 的基础上,将 Sentinel 和 OpenFeign 整合起来。整合 Sentinel 的本质就是:在原本处于“不设防状态”的分布式网络调用中,强行插入一个全自动的“切面拦截器”与“本地流量蓄水池”。我们可以从物理链路和技术机制两个维度来透视它:

  • 物理链路的本质:控制权由“远端”收回到“本地”。
    • 在没有引入 Sentinel 之前,user 服务调用 order 服务,命运完全掌握在 order 手里(如果 order 宕机,user 只能干等直到超时崩溃)。
    • 而整合 Sentinel 之后,本质上是在 zdemo-scloud-user 的 JVM 内存里,为远程的 GET:http://zdemo-scloud-order/order/details 资源建立了一个本地计数器(滑动时间窗口)。
    • 流量能不能发出去,不再由网络决定,而是先由 user 本地内存里的计数器说了算。
  • 技术实现的本质:OpenFeign 动态代理的增强。我们在 user 的 application.yml 里配置的 feign.sentinel.enabled: true,在 Spring 容器启动时会触发一个魔幻的行为:
    • Spring 会偷偷把 Feign 原生的客户端,替换为 Sentinel 定制的代理对象 SentinelInvocationHandler。
    • 当你的业务代码调用 orderFeignService.getUserOrderDetails() 时,流量并没有立刻出网,而是先进入了 Sentinel 的环绕通知(Around Advice)。

下面我们来具体完成这个整合的案例。项目新加入的依赖如下:

1
2
3
4
zdemo-scloud-api (公共模块)  ── 弱引入 Sentinel (provided 或 optional,仅用于编译)

├── zdemo-scloud-order (服务提供方) ── 强引入 Sentinel
└── zdemo-scloud-user (服务调用方) ── 强引入 Sentinel & Nacos 数据源


具体实现

公共 api 模块

zdemo-scloud-api:公共模块只负责声明契约,不引入任何多余的运行期依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--使用 open feign 接口定义所需要的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>

<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<scope>provided</scope><!--谁需要sentinel机制,谁需要引入依赖-->
</dependency>

编写通用响应基类与 DTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
public class BaseResponse {
private Integer code = 2000; // 默认成功码
private String msg = "success";
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderDTO extends BaseResponse implements Serializable {
private String orderNo;
private BigDecimal amount;
private String productName;
}

声明 Feign 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package zdemo.scloud.api.service.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import zdemo.scloud.api.dto.OrderDTO;

/**
* value 指定目标服务在 Nacos 中的 application.name
* path 指定 Controller 类上定义的 RequestMapping 路径
* fallbackFactory 指定降级处理类
*/
@FeignClient(value = "zdemo-scloud-order", path = "/order", fallbackFactory = OrderFallbackFactory.class)
public interface OrderFeignService {

/**
* 映射路径必须与 order 服务暴露的实际 HTTP 接口一模一样
* @RequestParam@PathVariable 等参数必须显式指定 ("orderNo")
*/
@GetMapping("/details")
OrderDTO getUserOrderDetails(@RequestParam("orderNo") String orderNo);
}

声明 Feign 降级工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package zdemo.scloud.api.service.feign;

import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
import zdemo.scloud.api.dto.OrderDTO;

/**
* 服务降级工厂:
* 也可以使用 @Component OrderFeignServiceFallback implements OrderFeignService 的方式,但还是强烈推荐工厂模式!
*/
@Slf4j
@Component // 必须注入到 Spring 容器中
public class OrderFallbackFactory implements FallbackFactory<OrderFeignService> {

@Override
public OrderFeignService create(Throwable cause) {
log.error("[OrderFeignService] 远程调用异常:", cause);
return new OrderFeignServiceFallback(cause); // 将异常作为构造参数传递,代码结构瞬间变清爽
}

/**
* 内部静态类:专门负责编写降级业务逻辑,职责分离
*/
private static class OrderFeignServiceFallback implements OrderFeignService {
private final Throwable cause;

public OrderFeignServiceFallback(Throwable cause) {
this.cause = cause;
}

@Override
public OrderDTO getUserOrderDetails(String orderNo) {
OrderDTO dto = new OrderDTO();
dto.setCode(5003);

// 通过明确判定异常类型,来组装有温度的、精准的降级文案
String blockReason = "未知网络抖动";
if (cause instanceof DegradeException) {
// 熔断异常
blockReason = "链路正处于熔断隔离期(降级保护)";
} else if (cause instanceof FlowException) {
// 限流异常
blockReason = "接口触发客户端限流(QPS超标)";
} else if (cause != null) {
// 正常的物理报错,比如远程服务真宕机了、或者是网络超时 TimeoutException
// 此时可以通过 cause.getClass().getSimpleName() 拿到真实的异常名字
blockReason = "远程调用物理崩溃,报错类型: " + cause.getClass().getSimpleName();
}
dto.setMsg("【服务降级】订单中心触发熔断保护,单号 [" + orderNo + "] 的详情获取失败!原因: " + blockReason);
return dto;
}
}
}


服务提供方 order

依赖配置

1
2
3
4
5
6
7
8
9
10
11
12
<!--引入 API 公共模块-->
<dependency>
<groupId>com.zdemo.scloud</groupId>
<artifactId>zdemo-scloud-api</artifactId>
<version>${project.version}</version>
</dependency>

<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

编写业务 Controller 实现(还是原来的接口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 通过 http 暴露接口
*/
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {

@Value("${server.port}")
private Integer port;

@GetMapping("/details")
public OrderDTO getUserOrderDetails(@RequestParam String orderNo) {
log.info("收到客户端订单:{}", orderNo);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return new OrderDTO(orderNo, new BigDecimal("200.00"), "Owlias教程:" + port);
}
}


服务调用方 user

流控熔断是客户端行为!核心战场在调用方。

依赖配置:调用方需要真正从 Nacos 拉取熔断规则,所以必须引入 sentinel-datasource-nacos 扩展包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--公共模块-->
<dependency>
<groupId>com.zdemo.scloud</groupId>
<artifactId>zdemo-scloud-api</artifactId>
<version>${project.version}</version>
</dependency>

<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

application.yml 核心配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
server:
port: 8180

spring:
application:
name: zdemo-scloud-user
cloud:
nacos:
discovery:
server-addr: 192.168.1.149:8848
username: nacos
password: nacos
namespace: prod-zdemo
cluster-name: DEFAULT
config:
server-addr: 192.168.1.149:8848
username: nacos
password: nacos
file-extension: yml
namespace: prod-zdemo
cluster-name: DEFAULT
loadbalancer:
cache:
enabled: false
nacos:
enabled: true
openfeign:
client:
config:
# 全局默认配置(对所有微服务生效)
default:
logger-level: none
connect-timeout: 5000
read-timeout: 5000
# 特定微服务生效
zdemo-scloud-order:
logger-level: full
connect-timeout: 5000 # 连接超时时间(默认2s)
read-timeout: 5000 # 请求处理超时时间(默认5s)
request-interceptors:
- zdemo.scloud.user.filter.CustomOpenFeignInterceptor
# ☞ 🌟 整合 sentinel
sentinel:
eager: true
web-context-unify: true
transport:
dashboard: 192.168.1.149:8858
port: 8719
client-ip: 192.168.1.2
# 配置持久化数据源
datasource:
# 1. 自定义流控规则数据源名称
flow-rules:
nacos:
server-addr: 192.168.1.149:8848 # Nacos 的大本营地址
username: nacos
password: nacos
data-id: ${spring.application.name}-flow-rules.json
groupId: SENTINEL_GROUP
namespace: prod-zdemo
data-type: json
rule-type: flow
# 2. 自定义降级规则数据源名称
degrade-rules:
nacos:
server-addr: 192.168.1.149:8848
username: nacos
password: nacos
data-id: ${spring.application.name}-degrade-rules.json
groupId: SENTINEL_GROUP
namespace: prod-zdemo
data-type: json
rule-type: degrade
# 3. 自定义热点规则数据源名称
param-rules:
nacos:
server-addr: 192.168.1.149:8848
username: nacos
password: nacos
data-id: ${spring.application.name}-param-rules.json
groupId: SENTINEL_GROUP
namespace: prod-zdemo
data-type: json
rule-type: param-flow
config:
import:
- optional:nacos:${spring.application.name}.${spring.cloud.nacos.config.file-extension}

feign:
sentinel:
# ☞ 🌟 强制开启 Feign 的 Sentinel 切面拦截
enabled: true

logging:
level:
zdemo.scloud.api.service.feign: DEBUG

启动类并注入 API 公共组件:由于 Feign 接口和降级工厂写在公共模块 zdemo-scloud-api 里,用户服务的启动类必须显式扩大包扫描范围,否则 Spring 找不到它们!

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableDiscoveryClient
@ComponentScan(basePackages = {"zdemo.scloud.user", "zdemo.scloud.api.service.feign"}) // 🌟 必须手动将公共包的 fallback 工厂扫描进来
@EnableFeignClients(basePackages = {"zdemo.scloud.api"}) // 🌟 开启 Feign 客户端扫描器并指定接口扫描路径
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}

编写用户服务自身的对外测试入口(按原来不变)

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@RestController
public class UserController {

@Autowired
private OrderFeignService orderFeignService; // 注入公共包里的 Feign 接口

@GetMapping("/feign/user/order")
public OrderDTO getUserOrderDetailsUsingOpenFeign(@RequestParam String orderNo) {
log.info("使用 feign 发起 RPC 请求,订单号: {}", orderNo);
return orderFeignService.getUserOrderDetails(orderNo);
}
}

事先在 Nacos 中新建以下配置文件(与 user application.yml 对齐):

  • zdemo-scloud-user-degrade-rules.json
  • zdemo-scloud-user-flow-rules.json
  • zdemo-scloud-user-param-rules.json

在 zdemo-scloud-user-degrade-rules.json 中编写以下测试的降级规则。需要说明的是,当 Feign 整合成功后,Sentinel 默认生成的资源名称长这样:"GET:http://zdemo-scloud-order/order/details"。格式为:HTTP请求方式:http://微服务名/路径

1
2
3
4
5
6
7
8
9
10
11
[
{
"resource": "GET:http://zdemo-scloud-order/order/details",
"grade": 0, // 0 代表【慢调用比例】模式
"count": 500, // 慢调用临界阈值:RT 超过 500ms 算慢调用
"timeWindow": 5, // 熔断降级时间窗口:发生熔断后,持续熔断 5 秒
"statIntervalMs": 1000, // 统计时长:1秒内(1000ms)
"minRequestAmount": 2, // 核心:触发熔断的最小请求数。1秒内至少来5次才会评估
"slowRatioCount": 0.5 // 核心:慢调用比例阈值。达到 50% 立即熔断
}
]

在 nacos 编写完成,点击发布。陆续启动 order 和 user 子模块,转到 sentinel 控制台,查看 ”降级规则“ 是否已经同步过来了。一切ok,我们准备测试成果。


测试成果

正常状态:启动 order 和 user。访问 http://localhost:8180/feign/user/order?orderNo=11,两边通信通畅,返回 2000。

制造灾难:在 order 服务对应接口中,故意写一行 Thread.sleep(800); 或者直接抛出一个 RuntimeException 来模拟服务崩溃。使用 ab 工具对订单接口发起连续撞击:

1
$ ab -n 20 -c 15 "http://127.0.0.1:8180/feign/user/order?orderNo=121"
  • 在第 1~4 次请求时,由于没有达到 minRequestAmount=5 的底线,user 模块会卡死等待远程超时。
  • 从第 5 次开始,熔断机制瞬间爆发!user 服务不再等待远端响应,而是在 0 毫秒内瞬间返回我们在 Factory 里定制的熔断保护…

通过这一套配置,我们的 OpenFeign 客户端正式具备了分布式自愈能力,彻底告别了 “下游服务一死,全家跟着陪葬” 的连环雪崩悲剧!


Sentinel 整合 Dubbo

整合介绍

在微服务架构的演进中,Dubbo(三层协议/Triple)往往承载着企业内部核心链路的 RPC 通信。与 OpenFeign 这种基于 HTTP 的客户端熔断不同,Sentinel 整合 Dubbo,要求在服务调用方 和 服务提供方双向筑起防线。

  • 提供方(Order)的生死防线:线程池限流(防止大流量直接把自己的本地线程池或数据库砸挂)。
  • 调用方(User)的优雅兜底:网络/超时熔断(防止被下游的慢调用活活拖死,并提供 Fallback)。

Dubbo 3 的 Sentinel 适配完全基于 Dubbo 的 Filter(过滤器)机制。当你在两端引入依赖后,Sentinel 会自动在 Dubbo 的 RPC 调用拦截链中注入 DubboAdapterProviderFilter(服务端)和 DubboAdapterConsumerFilter(消费端)。

我们还是在上述 zdemo-scloud-api、zdemo-scloud-order、zdemo-scloud-user 的基础上进行整合。由于 api 公共模块只需要保持纯净,定义好 DTO 和接口即可,不需要像 Feign 那样用 FallbackFactory 强耦合,Dubbo 的 降级机制天然就是完全解耦的!


服务提供方 order

引入需要的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--引入 API 公共模块-->
<dependency>
<groupId>com.zdemo.scloud</groupId>
<artifactId>zdemo-scloud-api</artifactId>
<version>${project.version}</version>
</dependency>

<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-apache-dubbo3-adapter</artifactId>
</dependency>

配置文件 application.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
server:
port: 8081

spring:
application:
name: zdemo-scloud-order
cloud:
nacos:
discovery:
server-addr: 192.168.1.149:8848
username: nacos
password: nacos
namespace: prod-zdemo
cluster-name: DEFAULT
config:
server-addr: 192.168.1.149:8848
username: nacos
password: nacos
file-extension: yml
namespace: prod-zdemo
cluster-name: DEFAULT
# 🌟 dubbo 整合 sentinel
sentinel:
eager: true
web-context-unify: true
transport:
dashboard: 192.168.1.149:8858
port: 8719
client-ip: 192.168.1.2
# 配置持久化数据源
datasource:
# 🌟 dubbo 服务提供方需要做限流,保护自己不被大流量(高QPS)砸死。(注意引入 sentinel-datasource-nacos 依赖)
flow-rules:
nacos:
server-addr: 192.168.1.149:8848 # Nacos 的大本营地址
username: nacos
password: nacos
data-id: ${spring.application.name}-flow-rules.json
group-id: SENTINEL_GROUP
namespace: prod-zdemo
data-type: json
rule-type: flow
config:
import:
- optional:nacos:${spring.application.name}.${spring.cloud.nacos.config.file-extension}

dubbo:
application:
register-mode: INSTANCE # 🌟 全局默认:我们全面拥抱新时代的“应用级注册”
name: ${spring.application.name}-rpc
qos-enable: true
qos-port: 22222
registry:
register-mode: INSTANCE # 🌟 局部调整:它用于覆盖全局配置,精准控制某一个特定注册中心的注册行为。
address: nacos://192.168.1.149:8848?namespace=prod-zdemo
username: nacos
password: nacos
protocol:
name: tri
port: 20881
serialization: fastjson2
prefer-serialization: fastjson2

logging:
level:
org.apache.dubbo: INFO
org.apache.dubbo.rpc.filter: ERROR
org.apache.dubbo.config.deploy.DefaultMetricsServiceExporter: ERROR

dubbo 接口实现(不用改变):

1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@DubboService // 使用 Dubbo 的 @DubboService 注解暴露出该服务
public class OrderServiceImpl implements OrderService {

@Override
public OrderDTO getOrderDetails(String orderNo) {
log.info("收到 RPC 请求,订单号: {}", orderNo);
try { Thread.sleep(800); } catch (InterruptedException e) {} // 测试用
return new OrderDTO(orderNo, new BigDecimal("199.00"), "Owlias教程");
}
}


服务调用方 user

引入需要的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--引入 API 公共模块-->
<dependency>
<groupId>com.zdemo.scloud</groupId>
<artifactId>zdemo-scloud-api</artifactId>
<version>${project.version}</version>
</dependency>

<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-apache-dubbo3-adapter</artifactId>
</dependency>

服务调用者的 application.yml 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
server:
port: 8180

spring:
application:
name: zdemo-scloud-user
cloud:
nacos:
discovery:
server-addr: 192.168.1.149:8848
username: nacos
password: nacos
namespace: prod-zdemo
cluster-name: DEFAULT
config:
server-addr: 192.168.1.149:8848
username: nacos
password: nacos
file-extension: yml
namespace: prod-zdemo
cluster-name: DEFAULT
loadbalancer:
cache:
enabled: false
nacos:
enabled: true
# ☞ 🌟 整合 sentinel
sentinel:
eager: true
web-context-unify: true
transport:
dashboard: 192.168.1.149:8858
port: 8719
client-ip: 192.168.1.2
# 配置持久化数据源
datasource:
# 🌟 自定义降级规则数据源名称,保护自己不被下游的慢 Dubbo 或 Feign 调用拖死。
degrade-rules:
nacos:
server-addr: 192.168.1.149:8848
username: nacos
password: nacos
data-id: ${spring.application.name}-degrade-rules.json
groupId: SENTINEL_GROUP
namespace: prod-zdemo
data-type: json
rule-type: degrade
config:
import:
- optional:nacos:${spring.application.name}.${spring.cloud.nacos.config.file-extension}

# Dubbo 独立注册中心配置(与 Order 端无缝通信的关键)
dubbo:
application:
# 注意和 spring cloud application 暴露的应用名明确区分划清界限,否则 LoadBalancer 根本无法分辨谁是走 HTTP 的、谁是走 Dubbo 的!
name: ${spring.application.name}-rpc
qos-enable: true
qos-port: 22233
registry:
address: nacos://192.168.1.149:8848?namespace=prod-zdemo
username: nacos
password: nacos

logging:
level:
org.apache.dubbo: INFO
org.apache.dubbo.rpc.filter: ERROR
org.apache.dubbo.config.deploy.DefaultMetricsServiceExporter: ERROR

调用者调用服务提供者 dubbo 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@RestController
public class UserController {

@DubboReference(loadbalance = "roundrobin")
private OrderService orderService;

@GetMapping("/dubbo/user/order")
public OrderDTO getUserOrderDetailsUsingDubbo(@RequestParam String orderNo) {
log.info("使用 dubbo 发起 RPC 请求,订单号: {}", orderNo);
return orderService.getOrderDetails(orderNo);
}
}

编写服务降级处理(可以处理服务提供端的流控规则,也可以处理服务调用端的熔断规则),这里的 GlobalHttpBlockExceptionHandler 就是复用的之前的用于拦截 Http Web 请求的那个 BlockExceptionHandler。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package zdemo.scloud.user.controller;

import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import zdemo.scloud.user.block.GlobalHttpBlockExceptionHandler;
import java.util.Optional;

@RestControllerAdvice
public class GlobalRuntimeExceptionHandler {

@Resource
private GlobalHttpBlockExceptionHandler globalHttpBlockExceptionHandler;

/**
* 🌟 拦截所有从 Controller 内部(包括 Dubbo 链路)弹飞出来的 RuntimeException
*/
@ExceptionHandler(RuntimeException.class)
public void handleRuntimeException(RuntimeException e, HttpServletRequest request, HttpServletResponse response) throws Exception {

// 1. 研判这个 RuntimeException 是不是由 Sentinel 熔断/限流引起的变质异常
if (e.getMessage() != null && e.getMessage().contains("SentinelBlockException")) {

// 2. 人肉还原出对应的 BlockException 子类,灌入你写好的全局处理器中
BlockException blockException;
if (e.getMessage().contains("DegradeException")) {
blockException = new DegradeException("服务调用端降级被触发");
} else if (e.getMessage().contains("FlowException")) {
blockException = new FlowException("服务提供端限流被触发");
} else {
blockException = new FlowException("Sentinel Blocked");
}

// 3. 完美借刀杀人:直接调用你的组件,让它吐出你写好的 1001、1002 规范 JSON!
globalHttpBlockExceptionHandler.handle(request, response, Optional.of(request.getRequestURI()).orElse(""), blockException);
return;
}

// 如果是普通的其他代码崩溃(如空指针),继续扔给底层的 500 页面
throw e;
}
}


在 Nacos 上配置规则

资源名称说明

当 Sentinel 拦截到 Dubbo 的资源时,默认生成的资源名称格式有极其严格的规范:

  • 服务提供方资源名规范接口全限定名:方法名(参数类型列表)
  • 服务消费端资源名规范接口全限定名:方法名(参数类型列表)
  • 多个参数之间必须使用标准的英文逗号进行分隔,逗号前后绝对不能包含任何空格!
  • 原生基本类型(如 boolean、int)直接写关键字,而包装类(如 Long)必须写成 java.lang.Long。
  • 对于泛型集合(如 List \<Long>),Sentinel 在生成资源名时会发生泛型擦除,它不认里面的 \<Long>,只认最外层的接口类型 java.util.List。
  • 自己团队编写的业务对象,同样要写全路径:zdemo.scloud.api.dto.OrderUpdateDTO。

一些典型 Sentinel 默认生成的资源名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface OrderService {
// zdemo.scloud.api.service.dubbo.OrderService:getOrderList()
OrderDTO getOrderList();

// zdemo.scloud.api.service.dubbo.OrderService:getOrderDetails(java.lang.String)
OrderDTO getOrderDetails(String orderNo);
}

public interface UserService {
// zdemo.scloud.api.service.dubbo.UserService:login(java.lang.String,java.lang.Long,boolean)
UserDTO login(String username, Long tenantId, boolean rememberMe);
}

public interface InventoryService {
// zdemo.scloud.api.service.dubbo.InventoryService:batchUpdateStock(java.util.List,zdemo.scloud.api.dto.StockUpdateDTO)
boolean batchUpdateStock(List<Long> skuIds, StockUpdateDTO updateDto);
}

新版 Sentinel 的 Dubbo3 适配器中,提供方和消费端的默认资源名是完全一样且对齐的


具体的规则配置

针对我们的接口,资源名精确对齐为:zdemo.scloud.api.service.dubbo.OrderService:getOrderDetails(java.lang.String)。

在 Nacos 中为提供方(Order)配限流规则:

  • Data ID: zdemo-scloud-order-flow-rules.json
  • group-id:SENTINEL_GROUP
  • namespace:prod-zdemo
1
2
3
4
5
6
7
8
[
{
"resource": "zdemo.scloud.api.service.dubbo.OrderService:getOrderDetails(java.lang.String)",
"grade": 1,
"count": 2,
"limitApp": "default"
}
]

在 Nacos 中为调用方(User)配熔断规则:

  • Data ID: zdemo-scloud-user-degrade-rules.json
  • group-id:SENTINEL_GROUP
  • namespace:prod-zdemo

1秒内请求超过5次,且慢调用(超过500ms)比例达到30%时,启动熔断隔离 5 秒。

1
2
3
4
5
6
7
8
9
10
11
[
{
"resource": "zdemo.scloud.api.service.dubbo.OrderService:getOrderDetails(java.lang.String)",
"grade": 0, // 0: 慢调用比例
"count": 500, // 慢调用临界 RT: 500ms
"timeWindow": 5, // 熔断隔离 5 秒
"statIntervalMs": 1000,
"minRequestAmount": 5,
"slowRatioCount": 0.3 // 30% 慢调用则熔断
}
]


效果测试

  1. 正常通信:启动 Nacos、Order 服务和 User 服务。访问 user 的 xxx:8180/dubbo/user/order?orderNo=111,数据正常由 Order 返回,code 为 2000。

  2. 测试提供方流控(限流保护)

    1
    $ ab -n 20 -c 15 "http://192.168.1.2:8180/dubbo/user/order?orderNo=111"

    因为触发了服务端的限流规则,Order 端的 Dubbo 拦截器会抛出 SentinelRpcException。此时 User 端的 OrderServiceMock 会瞬间接盘,前端直接拿到:

    1
    2
    3
    4
    5
    {
    "msg": "【全局流控】服务器当前排队人数过多,请稍后再试!",
    "path": "/dubbo/user/order",
    "code": 1001
    }
  3. 测试调用方熔断(故障隔离)

    在 Order 服务的实现类里写一行 Thread.sleep(800) 模拟慢调用,或者将 Order 的限流上限调的很大。然后使用 ab 工具发起 1 秒 10 次的饱和压测。User 服务在检测到大面积慢调用后,本地的熔断器瞬间切换为 OPEN。在接下来的 5 秒钟内,User 服务甚至不再向 Dubbo 线程网络发出任何请求,在本地 0 毫秒内直接返回:

    1
    2
    3
    4
    5
    {
    "msg": "【全局熔断】下游微服务链路出现卡顿崩溃,服务已紧急降级!",
    "path": "/dubbo/user/order",
    "code": 1002
    }

通过这一套完整的 Dubbo3 + Sentinel 组合拳,你在公共模块保持了接口的零污染,同时完美达成了内部高性能 RPC 链路的过载限流与客户端自愈隔离的生产级双重保障。