文章目录

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 章关注的是“保护服务不被打垮”。

下一章将关注的是“提高热点数据访问性能,降低数据库压力”。


Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐