文章目录

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。

对比项HystrixSentinel
常见体系Spring Cloud NetflixSpring 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编程工具,助力开发者即刻编程。

更多推荐