Spring Cloud 学习与实践(10):Sentinel 限流、熔断与降级
文章目录
- Spring Cloud 学习与实践(10):Sentinel 限流、熔断与降级
-
- 1、当前项目已经打通了什么
- 2、为什么需要 Sentinel
- 3、Sentinel 是什么
- 4、Sentinel 和 Hystrix 简单对比
- 5、启动 Sentinel Dashboard
- 6、cloud-product 接入 Sentinel
- 7、商品详情接口 QPS 限流
- 8、自定义商品服务 Sentinel 限流返回
- 9、cloud-order 接入 Sentinel
- 10、商品服务超时触发 Feign fallbackFactory
- 11、验证业务异常不会被 fallback 掩盖
- 12、Sentinel 慢调用比例熔断
- 13、热点参数限流
- 14、并发线程数限流
- 15、知识扩展
- 16、本章总结
- 17、下一章预告:Redis 缓存与高并发读优化
Spring Cloud 学习与实践(10):Sentinel 限流、熔断与降级
本章目标不是简单让 Sentinel 跑起来,而是理解:主链路打通之后,如何保护服务不被流量和异常拖垮。
1、当前项目已经打通了什么
在第 9 章结束时,项目已经完成统一入口、统一鉴权和用户身份透传。
当前主链路如下:
客户端
↓
Gateway 统一入口
↓
JWT 统一鉴权
↓
Gateway 写入 X-User-Id
↓
cloud-order 创建订单
↓
OpenFeign 调用 cloud-user
↓
OpenFeign 调用 cloud-product
↓
MySQL 保存业务数据
目前已经打通的能力包括:
1. Nacos 注册中心
2. Nacos 配置中心
3. Gateway 路由转发
4. JWT 统一鉴权
5. X-User-Id 身份透传
6. UserContext 保存当前用户身份
7. OpenFeign 服务调用
8. CircuitBreaker 线程切换下的用户身份修复
9. @Async 异步线程下的用户身份修复
但是这些只能说明:
服务之间能正常调用。
还不能说明:
服务在高并发、慢调用、下游异常时仍然稳定。
所以第 10 章进入新的主题:
服务保护
流量治理
限流
熔断
降级
2、为什么需要 Sentinel
2.1 主链路打通后还会有什么问题
现在订单链路已经可以跑通:
客户端
↓
Gateway
↓
cloud-order
↓
cloud-user
↓
cloud-product
但是如果出现下面几种情况,系统仍然可能出问题:
1. 商品详情接口突然被大量请求打爆
2. 下单接口短时间被大量并发请求访问
3. 商品服务响应很慢,订单服务一直等待
4. 商品服务不可用,订单服务不断重试或等待
5. 某个热点商品被疯狂访问,拖慢整个商品服务
6. 慢接口占满 Tomcat 工作线程,导致其它接口也无法响应
这些问题已经不属于“服务能不能调用成功”,而属于“服务能不能稳定运行”。
2.2 什么是服务雪崩
微服务之间往往是链式调用。
例如:
cloud-order
↓
cloud-user
↓
cloud-product
如果 cloud-product 响应非常慢,cloud-order 中等待商品服务返回的线程就会被占住。
当请求越来越多时,cloud-order 中越来越多线程被阻塞。
如果线程一直不释放,最终可能导致:
cloud-product 慢
↓
cloud-order 线程堆积
↓
cloud-order 也变慢或不可用
↓
Gateway 请求堆积
↓
整个链路被拖垮
这就是微服务中常说的服务雪崩。
2.3 服务保护的几种常见方案
服务保护常见有几类手段:
1. 超时处理
2. 线程隔离 / 舱壁模式
3. 熔断降级
4. 限流
| 方案 | 解决什么问题 | 本章是否演练 |
|---|---|---|
| 超时处理 | 下游太慢时,调用方不能无限等待 | 是 |
| 线程隔离 / 线程数限制 | 慢资源不能占满全部线程 | 是 |
| 熔断降级 | 下游持续异常或持续慢时,暂时阻断 | 是 |
| 限流 | 流量太大时,挡住一部分请求 | 是 |
可以简单理解为:
限流:流量太大,我先挡住一部分。
超时:等太久了,我不再继续等。
线程数限制:慢接口最多只能占用有限线程。
熔断:这个资源持续不稳定,我暂时不让请求继续进来。
降级:系统异常时,返回一个可控的兜底结果。
3、Sentinel 是什么
Sentinel 是 Alibaba 开源的流量治理组件。
它以“流量”为切入点,提供:
流量控制
并发线程数限制
熔断降级
系统自适应保护
热点参数限流
实时监控
规则管理
本章主要用它解决:
1. 商品详情接口被大量请求访问怎么办
2. 创建订单接口被大量并发访问怎么办
3. 商品服务变慢时如何熔断
4. Feign 调用超时时如何进入 fallback
5. 热点商品如何单独限流
6. 慢接口如何避免占满线程
3.1 Resource:资源
Sentinel 保护的对象叫资源。
资源可以是:
一个接口
一个方法
一段代码
一个 Feign 调用
一个 Gateway 路由
例如本章中会用到这些资源:
/products/{id}
productDetailById
/orders
其中:
/products/{id}
是 Web 接口资源。
productDetailById
是我们通过 @SentinelResource 主动标记的方法级资源。
3.2 Rule:规则
Sentinel 通过规则来保护资源。
常见规则有:
流控规则
热点参数规则
熔断规则
系统保护规则
授权规则
例如:
商品详情接口 QPS 最大为 2
表示:
1 秒内最多允许 2 个请求通过。
超过的请求会被 Sentinel 拦截。
4、Sentinel 和 Hystrix 简单对比
老一代 Spring Cloud Netflix 体系中,经常能看到 Hystrix。
现在 Spring Cloud Alibaba 体系中,更常见的是 Sentinel。
| 对比项 | Hystrix | Sentinel |
|---|---|---|
| 常见体系 | Spring Cloud Netflix | Spring Cloud Alibaba |
| 重点能力 | 熔断、隔离、降级 | 限流、热点参数、线程数限制、熔断降级、系统保护 |
| 隔离方式 | 常见线程池隔离 | 线程数限制,偏轻量信号量隔离 |
| 监控控制台 | 相对不完善 | Dashboard 更直观 |
| 流量整形 | 支持较弱 | 支持快速失败、Warm Up、排队等待 |
| 本课程是否采用 | 不采用 | 采用 |
这里要联系第 9 章踩过的坑:
线程池切换
↓
ThreadLocal 丢失
↓
UserContext 需要额外传播
Hystrix 常见线程池隔离方式会带来更明显的线程上下文切换问题。
Sentinel 的线程数限制更像轻量级信号量隔离,它不是给每个资源单独分配线程池,而是限制某个资源同时占用的线程数量。
5、启动 Sentinel Dashboard
5.1 为什么需要 Dashboard
Dashboard 是 Sentinel 的控制台。
它可以用来:
查看应用
查看资源
查看实时监控
配置限流规则
配置熔断规则
配置热点参数规则
注意:
应用接入 Sentinel 后,不是启动就一定能在 Dashboard 看到资源。
必须访问过资源,Dashboard 才能看到对应的资源链路。
5.2 启动 Dashboard
本项目使用:
sentinel-dashboard-1.8.6.jar
为了不和其它服务端口冲突,本章使用端口:
8858
启动命令:
java -Dserver.port=8858 ^
-Dcsp.sentinel.dashboard.server=localhost:8858 ^
-Dproject.name=sentinel-dashboard ^
-jar sentinel-dashboard-1.8.6.jar
PowerShell 或普通命令行也可以写成一行:
java -Dserver.port=8858 -Dcsp.sentinel.dashboard.server=localhost:8858 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.6.jar
浏览器访问:
http://localhost:8858
默认账号密码:
sentinel / sentinel
6、cloud-product 接入 Sentinel
6.1 为什么先接入 cloud-product
第一个演练服务选择 cloud-product。
原因是商品详情接口适合做限流验证:
1. 调用简单
2. 不会修改数据库
3. 可以反复访问
4. 适合用 JMeter 压测
5. 很容易观察 QPS 限流效果
先不要一上来就压测创建订单接口。
因为创建订单接口涉及:
Gateway
JWT
UserContext
cloud-order
Feign
cloud-user
cloud-product
MySQL
库存扣减
订单保存
链路太长,不适合作为 Sentinel 的第一个验证点。
6.2 引入 Sentinel 依赖:cloud-product/pom.xml
在 cloud-product/pom.xml 中增加:
<!-- Sentinel:用于接口限流、熔断降级和 Dashboard 监控 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
不需要写版本号。
因为父工程已经通过 Spring Cloud Alibaba BOM 管理版本。
6.3 配置 Dashboard 地址:cloud-product-dev.yaml
在 Nacos 中打开:
cloud-product-dev.yaml
补充 Sentinel 配置:
spring:
cloud:
sentinel:
transport:
dashboard: 127.0.0.1:8858
port: 8719
这里的:
dashboard: 127.0.0.1:8858
表示当前服务要连接哪个 Sentinel Dashboard。
这里的:
port: 8719
表示 Sentinel 客户端在本地启动一个通信端口,Dashboard 会通过它和当前应用交互。
注意:
如果当前配置文件里已经有 spring.cloud.nacos,
不要重复写多个 spring 根节点。
本项目中 Nacos 相关配置前面已经放在了 bootstrap.yml 或已有 Nacos 配置中,所以这里重点只补 Sentinel 配置即可。

6.4 启动并验证 cloud-product 出现在 Dashboard
启动顺序:
1. Nacos Server
2. Sentinel Dashboard
3. CloudProductApplication
4. CloudGatewayApplication
然后通过 Gateway 访问商品详情接口:
GET http://localhost:9000/api/product/products/1
Authorization: Bearer {{token}}
访问后回到:
http://localhost:8858
预期:
Dashboard 左侧出现 cloud-product
簇点链路中能看到商品详情相关资源

7、商品详情接口 QPS 限流
7.1 本轮目标
给商品详情接口配置:
QPS = 2
验证:
JMeter 一次发送 10 个请求
↓
大约只有 2 个请求通过
↓
其它请求被 Sentinel 拦截
7.2 为什么先做 QPS 限流
QPS 限流是 Sentinel 最容易理解的能力。
QPS 可以简单理解为:
每秒请求数
例如:
QPS = 2
表示:
1 秒内最多允许 2 个请求通过。
超过的请求会被 Sentinel 拦截。
7.3 Dashboard 配置流控规则
进入:
Sentinel Dashboard
↓
cloud-product
↓
簇点链路
↓
找到商品详情资源
↓
点击“流控”
配置:
资源名:商品详情接口对应资源
针对来源:default
阈值类型:QPS
单机阈值:2
流控模式:直接
流控效果:快速失败

7.4 使用 JMeter 验证
请求:
GET http://localhost:9000/api/product/products/1
Authorization: Bearer {{token}}
JMeter 快速发送:
10 个请求
本轮实际验证结果:
1. Dashboard 的 cloud-product 簇点链路中能看到商品详情资源
2. 成功给商品详情资源配置 QPS=2 流控规则
3. JMeter 请求 10 个,只有 2 个通过
4. Dashboard 实时监控能看到拒绝 QPS / blocked 数据
5. 被限流时返回:Blocked by Sentinel (flow limiting)
6. 无报错



这说明 QPS 流控规则已经生效。
8、自定义商品服务 Sentinel 限流返回
8.1 为什么要自定义返回
默认限流返回是:
Blocked by Sentinel (flow limiting)
这个结果虽然能说明 Sentinel 生效了,但有几个问题:
1. 不是项目统一 Result 格式
2. 前端不好统一处理
3. 用户看不懂英文提示
4. 不方便接口文档描述
所以要改成统一格式:
{
"code": 42900,
"message": "请求过于频繁,请稍后再试",
"data": null
}
HTTP 状态码也应该更合理地使用:
429 Too Many Requests
8.2 统一商品服务 Sentinel 拦截返回:ProductSentinelBlockExceptionHandler.java
这个类解决的问题是:
Sentinel 已经决定拦截请求,
这个类负责决定拦截后返回什么 JSON。
它放在:
cloud-product
└── src/main/java
└── com.example.cloud.product.config
└── ProductSentinelBlockExceptionHandler.java
完整代码:
package com.example.cloud.product.config;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.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.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.example.cloud.common.result.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
/**
* 商品服务 Sentinel 限流 / 熔断 / 系统保护统一返回处理器。
*
* 默认情况下,请求被 Sentinel 限流后,
* 可能直接返回:
*
* Blocked by Sentinel (flow limiting)
*
* 这个返回不符合项目统一 Result<T> 格式。
*
* 当前处理器的作用:
* 1. 捕获 Sentinel 抛出的 BlockException
* 2. 根据不同 BlockException 类型生成中文提示
* 3. 返回项目统一 JSON
*
* 注意:
* 当前类只作用于 cloud-product。
*/
@Component
public class ProductSentinelBlockExceptionHandler
implements BlockExceptionHandler {
private static final int TOO_MANY_REQUESTS_CODE = 42900;
private final ObjectMapper objectMapper;
public ProductSentinelBlockExceptionHandler(
ObjectMapper objectMapper
) {
this.objectMapper = objectMapper;
}
@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
BlockException exception
) throws Exception {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("application/json;charset=UTF-8");
Result<Object> result = Result.fail(
TOO_MANY_REQUESTS_CODE,
buildMessage(exception)
);
response.getWriter().write(
objectMapper.writeValueAsString(result)
);
}
private String buildMessage(BlockException exception) {
if (exception instanceof FlowException) {
return "请求过于频繁,请稍后再试";
}
if (exception instanceof DegradeException) {
return "服务暂时不可用,请稍后再试";
}
if (exception instanceof ParamFlowException) {
return "热点参数访问过于频繁,请稍后再试";
}
if (exception instanceof SystemBlockException) {
return "系统繁忙,请稍后再试";
}
if (exception instanceof AuthorityException) {
return "当前请求无权访问";
}
return "请求被 Sentinel 拦截";
}
}
8.3 再次使用 JMeter 验证
重启:
CloudProductApplication
Dashboard 里重新添加商品详情 QPS=2 流控规则。
注意:
重启后之前添加的规则消失了,这是正常现象。


再次使用 JMeter 发送 10 个请求。


本轮实际验证结果:
1. ProductSentinelBlockExceptionHandler 编译通过
2. cloud-product 重启成功
3. JMeter 再次请求 10 个,仍然大约只有 2 个通过
4. 被限流请求不再返回 Blocked by Sentinel
5. 被限流请求返回统一 Result JSON
6. HTTP 状态码为 429
7. Dashboard 实时监控能看到记录
8. 无报错
限流响应变为:
{
"code": 42900,
"message": "请求过于频繁,请稍后再试",
"data": null
}
8.4 注意:Dashboard 手动规则默认不会持久化
本轮测试时观察到:
重启 cloud-product 后,
之前在 Sentinel Dashboard 中手动添加的流控规则消失了。
这不是错误。
当前我们在 Dashboard 页面上手动添加的规则,默认是临时规则,主要用于学习、调试和验证。
应用重启后,下发到客户端内存中的规则会丢失,所以需要重新配置。
本章先不做规则持久化。
后续如果要生产化,一般需要把 Sentinel 规则持久化到:
Nacos
Apollo
ZooKeeper
本地文件
其它配置中心
9、cloud-order 接入 Sentinel
9.1 为什么订单服务也要接入 Sentinel
商品详情限流保护的是:
cloud-product 查询接口
订单接口限流保护的是:
cloud-order 下单入口
订单接口比商品详情更接近真实业务,因为它会继续调用:
cloud-user
cloud-product
MySQL
如果在订单入口直接限流,那么被拦截的请求不会继续压到下游服务。
9.2 引入 Sentinel 依赖:cloud-order/pom.xml
在 cloud-order/pom.xml 中增加:
<!-- Sentinel:用于订单接口限流、熔断降级和 Dashboard 监控 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

9.3 配置 Dashboard 地址:cloud-order-dev.yaml
在 Nacos 中打开:
cloud-order-dev.yaml
补充:
spring:
cloud:
sentinel:
transport:
dashboard: 127.0.0.1:8858
port: 8720

注意:这里没有重复写 nacos.discovery.server-addr。
原因是前面演练中,Nacos Discovery 的地址已经放在 bootstrap.yml 或已有配置中。
也就是说,最终运行环境中必须有:
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
但是它不一定非要写在 cloud-order-dev.yaml 里。
它可以来自:
bootstrap.yml
application.yml
Nacos 远程配置
启动参数
环境变量
只要最终服务能读取到 spring.cloud.nacos.discovery.server-addr,就能注册和发现服务。
本节只需要新增 Sentinel 配置即可。
9.4 统一订单服务 Sentinel 拦截返回:OrderSentinelBlockExceptionHandler.java
这个类解决的问题和商品服务类似。
它放在:
cloud-order
└── src/main/java
└── com.example.cloud.order.config
└── OrderSentinelBlockExceptionHandler.java
代码和 ProductSentinelBlockExceptionHandler 基本一致,只是类名和包名改为订单服务。
为了避免笔记过度重复,这里保留核心结构:
@Component
public class OrderSentinelBlockExceptionHandler
implements BlockExceptionHandler {
private static final int TOO_MANY_REQUESTS_CODE = 42900;
private final ObjectMapper objectMapper;
public OrderSentinelBlockExceptionHandler(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
BlockException exception
) throws Exception {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("application/json;charset=UTF-8");
Result<Object> result = Result.fail(
TOO_MANY_REQUESTS_CODE,
buildMessage(exception)
);
response.getWriter().write(objectMapper.writeValueAsString(result));
}
private String buildMessage(BlockException exception) {
if (exception instanceof FlowException) {
return "请求过于频繁,请稍后再试";
}
if (exception instanceof DegradeException) {
return "服务暂时不可用,请稍后再试";
}
if (exception instanceof ParamFlowException) {
return "热点参数访问过于频繁,请稍后再试";
}
if (exception instanceof SystemBlockException) {
return "系统繁忙,请稍后再试";
}
if (exception instanceof AuthorityException) {
return "当前请求无权访问";
}
return "请求被 Sentinel 拦截";
}
}
9.5 创建订单接口 QPS 限流
启动顺序:
1. Nacos Server
2. Sentinel Dashboard
3. CloudAuthApplication
4. CloudUserApplication
5. CloudProductApplication
6. CloudOrderApplication
7. CloudGatewayApplication
先访问一次创建订单接口,让 Dashboard 发现资源:
POST http://localhost:9000/api/order/orders
Authorization: Bearer {{token}}
Content-Type: application/json
{
"productId": 1,
"quantity": 1
}

然后进入:
Sentinel Dashboard
↓
cloud-order
↓
簇点链路
↓
找到创建订单资源
↓
点击“流控”
配置:
资源名:创建订单接口对应资源
针对来源:default
阈值类型:QPS
单机阈值:1
流控模式:直接
流控效果:快速失败
9.6 JMeter 验证订单接口限流
JMeter 快速请求创建订单接口。


本轮实际验证结果:
1. cloud-order 成功引入 Sentinel 依赖
2. cloud-order-dev.yaml 配置了 Sentinel Dashboard 和 8720 端口
3. cloud-order 重启成功
4. Dashboard 左侧出现 cloud-order
5. 簇点链路中能看到创建订单资源
6. 成功配置创建订单接口 QPS=1
7. JMeter 快速请求时,只有约 1 个请求通过
8. 被限流请求返回统一 Result JSON
9. HTTP 状态码为 429
10. 实时监控能看到拒绝记录
11. 无报错
这一轮说明:订单入口已经可以被 Sentinel 保护。
10、商品服务超时触发 Feign fallbackFactory
10.1 为什么要演练 Feign fallback
前面的限流发生在服务内部。
现在要观察另一种场景:
cloud-order 调用 cloud-product
↓
cloud-product 响应很慢
↓
cloud-order 等待超时
↓
进入 Feign fallbackFactory
这属于典型系统异常:
服务超时
服务不可用
网络异常
连接失败
这种异常进入 fallback 是合理的。
10.2 先删除 Dashboard 中的限流规则
为了避免干扰,先临时删除:
cloud-product 商品详情 QPS 规则
cloud-order 创建订单 QPS 规则
否则还没走到 Feign 超时,就可能先被 Sentinel 限流拦住。

10.3 确认 Feign 超时和 CircuitBreaker 配置
在 cloud-order-dev.yaml 中确认类似配置:
feign:
circuitbreaker:
enabled: true
client:
config:
default:
connectTimeout: 1000
readTimeout: 1000
或者针对 cloud-product 单独配置:
feign:
circuitbreaker:
enabled: true
client:
config:
cloud-product:
connectTimeout: 1000
readTimeout: 1000
其中:
connectTimeout:建立连接的超时时间。
readTimeout:连接建立后,等待服务返回响应的超时时间。
本轮为了容易触发超时,使用:
readTimeout = 1000ms
10.4 确认 ProductClient 使用 fallbackFactory
ProductClient 应使用:
@FeignClient(
name = "cloud-product",
fallbackFactory = ProductClientFallbackFactory.class
)
public interface ProductClient extends ProductApi {
}
重点是:
fallbackFactory = ProductClientFallbackFactory.class
不用普通 fallback 的原因是:
fallbackFactory 可以拿到真正触发 fallback 的异常 cause。
这对后面区分系统异常和业务异常很重要。
10.5 模拟商品服务超时:临时 sleep 3000ms
在 cloud-product 商品详情查询入口临时加:
/*
* Sentinel / Feign fallback 故障演练:
*
* 临时让商品详情接口睡眠 3 秒,
* 用来模拟商品服务响应很慢。
*
* cloud-order 中 Feign readTimeout 设置为 1000ms,
* 所以订单服务调用商品服务时会超时,
* 从而进入 ProductClientFallbackFactory。
*
* 演练结束后要删除这段代码。
*/
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

验证完成后必须删除。
10.6 创建订单并观察 fallback
通过 Gateway 创建订单。


关键日志:
ProductClientFallbackFactory : 调用 cloud-product 失败,进入降级逻辑
feign.RetryableException: Read timed out executing GET http://cloud-product/products/1
Caused by: java.net.SocketTimeoutException: Read timed out
这说明链路确实是:
cloud-order 调用 cloud-product
↓
cloud-product sleep 3000ms
↓
cloud-order 等待超过 readTimeout
↓
抛出 Read timed out
↓
进入 ProductClientFallbackFactory
↓
返回“商品服务暂时不可用,请稍后重试”
11、验证业务异常不会被 fallback 掩盖
11.1 为什么要验证这个问题
降级不是万能兜底。
fallback 应该处理:
服务超时
服务宕机
网络异常
连接失败
Sentinel 熔断
不应该把下面这些业务异常包装成“服务不可用”:
商品库存不足
商品不存在
用户不存在
参数错误
余额不足
否则用户明明是库存不足,却看到:
商品服务暂时不可用,请稍后重试
这就叫:降级掩盖真实业务异常。
11.2 制造库存不足场景
本轮采用:
把商品 id=1 的库存改为 0
SQL:
UPDATE t_product
SET stock = 0
WHERE id = 1;

然后创建订单。
11.3 实际验证结果


本轮结果:
1. 将库存改为 0
2. 创建订单返回:商品库存不足
3. cloud-product 打印库存不足相关日志
4. cloud-order 没有打印 ProductClientFallbackFactory 降级日志
5. 没有 fallback cause
6. 无报错
cloud-order 中只看到类似:
GlobalExceptionHandler : 业务异常:商品库存不足
没有看到:
ProductClientFallbackFactory : 调用 cloud-product 失败,进入降级逻辑
这说明库存不足没有进入 fallbackFactory。
实际链路是:
cloud-order 创建订单
↓
Feign 调用 cloud-product 扣减库存
↓
cloud-product 判断库存不足
↓
cloud-product 抛出 BizException:商品库存不足
↓
cloud-product 的 GlobalExceptionHandler 转成统一 Result
↓
cloud-order 收到业务失败结果
↓
cloud-order 自己抛 BizException:商品库存不足
↓
cloud-order 的 GlobalExceptionHandler 返回:商品库存不足
结论:当前项目没有出现“降级掩盖真实业务异常”的问题。
12、Sentinel 慢调用比例熔断
12.1 限流和熔断的区别
前面的 QPS 限流解决的是:
请求太多,挡住一部分。
慢调用熔断解决的是:
接口持续变慢,暂时不让请求继续进入。
它不是因为请求太多,而是因为资源持续不稳定。
12.2 本轮目标
验证:
商品详情接口持续变慢
↓
Sentinel 判断慢调用比例过高
↓
触发熔断
↓
后续请求不再进入 ProductController
↓
直接被 Sentinel 拦截
↓
返回“服务暂时不可用,请稍后再试”
12.3 临时让商品详情接口变慢
在商品详情方法中临时加:
/*
* Sentinel 熔断降级演练:
*
* 临时让商品详情接口睡眠 1200ms,
* 用来模拟商品服务响应变慢。
*
* 后面会在 Sentinel Dashboard 中配置:
* 最大 RT = 1000ms。
*
* 因此该接口每次执行都会被统计为“慢调用”。
*
* 演练结束后必须删除这段代码。
*/
try {
Thread.sleep(1200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

12.4 配置慢调用比例熔断规则
进入:
Sentinel Dashboard
↓
cloud-product
↓
簇点链路
↓
找到商品详情资源
↓
点击“熔断”或“降级”
配置:
熔断策略:慢调用比例
最大 RT:1000 ms
比例阈值:0.5
熔断时长:10 秒
最小请求数:5
统计时长:10000 ms

含义:
在统计窗口内,
至少有 5 个请求,
并且超过 50% 的请求响应时间大于 1000ms,
就触发熔断 10 秒。
12.5 JMeter 验证
请求:
GET http://localhost:9000/api/product/products/1
Authorization: Bearer {{token}}
JMeter 发送 10 个请求。


本轮实际验证结果:
1. 已删除之前的 QPS 流控规则
2. 已给商品详情接口临时加 sleep 1200ms
3. 成功配置慢调用比例熔断规则
4. JMeter 请求时,前几个请求等待约 1200ms
5. 触发熔断后,后续请求很快返回
6. 返回内容为:服务暂时不可用,请稍后再试
7. Dashboard 实时监控能看到拒绝记录
8. 已删除临时 sleep
9. 已删除熔断规则
10. 无报错
结论:慢调用比例熔断验证成功。
13、热点参数限流
13.1 为什么需要热点参数限流
普通 QPS 限流看的是整个资源的访问量。
例如:
/products/{id} 每秒最多 2 次
这会导致:
/products/1 被限流
/products/2 也被限流
/products/3 也被限流
但是很多场景中,真正热的是某一个参数值:
商品 1 是爆款
商品 2、3、4 访问量很少
这时更适合做热点参数限流:
productId=1 每秒最多 2 次
其它商品每秒最多 5 次
13.2 增加热点参数依赖:cloud-product/pom.xml
在 cloud-product/pom.xml 中增加:
<!-- Sentinel 热点参数限流 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-parameter-flow-control</artifactId>
</dependency>
13.3 标记商品详情热点资源:ProductController.java
注意:这里使用的是
blockHandler,它处理的是被 Sentinel 规则拦截的情况(如触发限流)。如果业务代码本身抛出异常,应使用fallback属性。本章只演练限流场景,因此只演示blockHandler。
关键代码:
package com.example.cloud.product.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.example.cloud.common.result.ErrorCode;
import com.example.cloud.common.result.Result;
import com.example.cloud.product.entity.Product;
import com.example.cloud.product.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 商品接口。
*/
@RestController
@RequestMapping("/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
/**
* 查询商品列表。
*
* 本章不重复演练分页插件。
* 分页机制已经在 cloud-user 中完整学习过。
*/
@GetMapping
public Result<List<Product>> list() {
return Result.success(productService.list());
}
/**
* 根据 ID 查询商品详情。
*
* 这里额外使用 @SentinelResource 标记一个资源名:
*
* productDetailById
*
* 为什么不直接只用 /products/{id}?
*
* 因为热点参数限流需要基于方法参数做统计。
* 使用 @SentinelResource 后,Sentinel 可以把方法参数 id
* 作为热点参数规则中的参数索引 0 进行统计。
*/
@GetMapping("/{id}")
@SentinelResource(
value = "productDetailById",
blockHandler = "productDetailBlockHandler"
)
public Result<Product> getById(@PathVariable Long id) {
Product product = productService.getById(id);
if (product == null) {
return Result.fail(
ErrorCode.NOT_FOUND,
"商品不存在"
);
}
return Result.success(product);
}
/**
* 商品详情热点参数限流后的兜底方法。
*
* 注意:
* 1. 返回值类型必须和原方法一致
* 2. 参数列表要包含原方法参数
* 3. 最后额外增加 BlockException 参数
*/
public Result<Product> productDetailBlockHandler(
Long id,
BlockException exception
) {
return Result.fail(
42900,
"商品访问过于频繁,请稍后再试"
);
}
/**
* 扣减商品库存。
*
* 示例:
* POST /products/1/deduct-stock?quantity=2
*/
@PostMapping("/{id}/deduct-stock")
public Result<Void> deductStock(
@PathVariable Long id,
@RequestParam Integer quantity
) {
productService.deductStock(id, quantity);
return Result.success();
}
}

13.4 配置热点参数规则
重启 cloud-product 后,先访问一次:
GET http://localhost:9000/api/product/products/1
Authorization: Bearer {{token}}

然后进入:
Sentinel Dashboard
↓
cloud-product
↓
簇点链路
↓
productDetailById
↓
热点
新增规则:
资源名:productDetailById
参数索引:0
单机阈值:5
统计窗口时长:1 秒
再配置参数例外项:
参数类型:long
参数值:1
限流阈值:2
含义:
普通商品:同一个商品 id 每秒最多 5 次。
商品 id = 1:每秒最多 2 次。

13.5 JMeter 验证热点参数限流
测试商品 1:
GET http://localhost:9000/api/product/products/1
Authorization: Bearer {{token}}
JMeter 发送 10 个请求。

实际结果:
/products/1 大约通过 2 个请求。
测试商品 2:
GET http://localhost:9000/api/product/products/2
Authorization: Bearer {{token}}
JMeter 发送 10 个请求。

实际结果:
/products/2 大约通过 5 个请求。
本轮完整验证结果:
1. sentinel-parameter-flow-control 依赖添加成功
2. @SentinelResource 编译通过
3. cloud-product 重启成功
4. Dashboard 能看到 productDetailById 资源
5. 成功配置热点参数规则
6. JMeter 请求 /products/1 时触发热点参数限流
7. /products/1 大约通过 2 个请求
8. /products/2 明显比 /products/1 更宽松
9. /products/2 大约通过 5 个请求
10. 无报错
结论:热点参数限流验证成功。
14、并发线程数限流
14.1 QPS 限流和线程数限流的区别
QPS 限流看的是:
1 秒内进来多少请求
线程数限流看的是:
同一时刻有多少线程正在处理这个资源
例如:
线程数阈值 = 1
表示:
同一时刻最多只允许 1 个线程执行这个资源。
如果第一个请求执行很慢,占住线程 2 秒,那么其它同时进来的请求会被 Sentinel 快速拒绝。
14.2 临时让商品详情接口变慢
在 ProductController#getById 方法开头临时加:
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
14.3 配置线程数限流规则
进入:
Sentinel Dashboard
↓
cloud-product
↓
簇点链路
↓
productDetailById
↓
流控
新增规则:
资源名:productDetailById
针对来源:default
阈值类型:线程数
单机阈值:1
流控模式:直接
流控效果:快速失败
含义:
同一时刻最多只允许 1 个线程正在执行 productDetailById。


14.4 JMeter 并发验证
JMeter 设置:
线程数:5
Ramp-Up:0 或 1 秒
循环次数:1
请求:
GET http://localhost:9000/api/product/products/1
Authorization: Bearer {{token}}
预期:
大约 1 个请求等待 2 秒后正常返回商品数据;
其它请求很快返回限流 JSON。

本轮实际验证结果:
1. 已删除其它干扰规则
2. 已给商品详情临时加 sleep 2000ms
3. 成功配置 productDetailById 线程数阈值 = 1
4. JMeter 使用并发请求测试
5. 大约 1 个请求等待 2 秒后正常返回
6. 其它请求快速返回限流 JSON
7. Dashboard 能看到拒绝记录
结论:并发线程数限流验证成功。
15、知识扩展
15.1 流控模式:直接、关联、链路
本章主要演练的是直接模式。
即:
当前资源自己超过阈值,就拦截当前资源。
Sentinel 还支持:
关联模式
链路模式
可以这样理解:
关联模式:
当关联资源压力过大时,限制当前资源。
例如写操作压力大时,限制查询操作,把资源让给写操作。
链路模式:
同一个资源从不同入口进入时,只限制某一条调用链。
本章不展开这两种模式,因为当前项目还没有特别适合的演练场景。
后续如果做查询订单、创建订单、支付订单、秒杀商品,可以再单独演练关联和链路模式。
15.2 流控效果:快速失败、Warm Up、排队等待
本章主要使用快速失败,也就是超过阈值后立即拒绝。
Sentinel 还支持:
Warm Up
排队等待
可以这样理解:
Warm Up:
系统刚启动或长期低水位后,不要一上来就打满流量,而是慢慢放量。
排队等待:
不是直接拒绝请求,而是让请求按稳定速率排队通过。
本章没有展开这两种效果,是因为当前学习目标是先掌握最核心的限流是否生效、被限流后如何统一返回。
15.3 授权规则
Sentinel 还有授权规则,可以根据请求来源决定是否允许访问。
不过当前项目已经在第 9 章通过 Gateway + JWT 做了统一鉴权,所以本章不把 Sentinel 授权规则作为主线。
如果后续需要区分调用来源,例如只允许 Gateway 调用业务服务、不允许浏览器直接调用 cloud-product,可以再单独研究 Sentinel 授权规则或网关层安全隔离。
15.4 规则持久化
本章已经观察到:
Dashboard 手动添加规则,应用重启后会消失。
这说明当前规则只是学习阶段的临时规则。
生产环境一般不会只靠 Dashboard 手动配置,而是会把规则持久化到配置中心,例如:
Nacos
Apollo
ZooKeeper
本地文件
当前章节先不做规则持久化。
后续如果要补 Sentinel 生产化,会单独加一章:
Sentinel 规则持久化到 Nacos
16、本章总结
本章完成了 Sentinel 服务保护的核心演练。
已经完成:
1. 启动 Sentinel Dashboard
2. cloud-product 接入 Sentinel
3. 商品详情接口 QPS 限流
4. 自定义商品服务 Sentinel 限流返回
5. cloud-order 接入 Sentinel
6. 创建订单接口 QPS 限流
7. 商品服务超时触发 Feign fallbackFactory
8. 库存不足业务异常不进入 fallback
9. 慢调用比例熔断
10. 热点参数限流
11. 并发线程数限流
12. Dashboard 临时规则不持久化现象验证
13. MySQL allowPublicKeyRetrieval 环境问题处理
几个关键点:
QPS 限流:
限制单位时间内请求数量。
线程数限流:
限制同一时刻正在处理资源的线程数量。
热点参数限流:
针对某个热点参数值单独限流。
慢调用熔断:
资源持续变慢时,暂时阻断后续请求。
fallbackFactory:
适合处理超时、连接失败、服务不可用这类系统异常。
业务异常:
例如库存不足,不能被降级逻辑掩盖。
第 9 章解决的是:
谁可以访问系统?
用户身份如何在服务之间传递?
第 10 章解决的是:
访问太多怎么办?
服务太慢怎么办?
下游异常怎么办?
业务异常能不能被降级掩盖?
至此,项目已经从“主链路打通”进入“服务稳定性治理”。
17、下一章预告:Redis 缓存与高并发读优化
Sentinel 解决的是:
流量进来后,如何保护服务。
下一章将继续进入另一个高频能力:
Redis 缓存
要解决的问题是:
热点数据能不能不每次都查 MySQL?
商品详情这种高频查询能不能走缓存?
缓存穿透、缓存击穿、缓存雪崩分别是什么?
缓存和数据库不一致怎么办?
也就是说,第 10 章关注的是“保护服务不被打垮”。
下一章将关注的是“提高热点数据访问性能,降低数据库压力”。
更多推荐

所有评论(0)