Commit b172e9c8336daeb14ac638c2f58e264089df089e
1 parent
a86b269e
feat(infra): 增加可选的 Sentinel 限流熔断集成材料
Showing
6 changed files
with
245 additions
and
0 deletions
script/sentinel/README.md
0 → 100644
| 1 | +# 限流熔断(可选接入) | ||
| 2 | + | ||
| 3 | +基于 [Sentinel](https://sentinelguard.io/zh-cn/docs/introduction.html) 1.8.10 的可选接入材料,演示接口(资源)级别的限流、熔断。项目默认**不启用**:本目录不是 Maven 模块,不参与默认构建,主工程的依赖、配置、业务代码都不受影响。 | ||
| 4 | + | ||
| 5 | +项目的 `yudao-spring-boot-starter-protection` 组件本身**不包含** Sentinel,它提供的是基于 Redis(Redisson)的 `@RateLimiter` 请求限流、`@Idempotent` 幂等、Lock4j 分布式锁,接入本材料后保持不变,可以和 Sentinel 同时使用。 | ||
| 6 | + | ||
| 7 | +接入后,只有添加了 `@SentinelResource` 注解、并且配置了规则的资源,才会被限流、熔断,其它接口不受影响。示例的规则从 classpath 中的 JSON 文件加载,只保存在 JVM 内存中;单机阈值,多个实例不共享计数。 | ||
| 8 | + | ||
| 9 | +接入步骤、验证方式及使用限制,请阅读项目文档《限流熔断》,本文只列出材料及分支适用范围: | ||
| 10 | + | ||
| 11 | +- 单体项目 ruoyi-vue-pro:<https://doc.iocoder.cn/server-protection/> | ||
| 12 | +- 微服务项目 yudao-cloud:<https://cloud.iocoder.cn/server-protection/> | ||
| 13 | + | ||
| 14 | +## 1. 材料清单 | ||
| 15 | + | ||
| 16 | +| 文件 | 说明 | 目标位置 | | ||
| 17 | +| --- | --- | --- | | ||
| 18 | +| `dependencies.xml` | Maven 依赖片段:第 1 段 Sentinel 核心及注解切面(必须),第 2 段与 Dashboard 通信的模块(可选) | `yudao-module-infra/pom.xml` | | ||
| 19 | +| `src/main/resources/sentinel/demo-flow-rules.json` | 限流规则:资源 `demo-sentinel-flow`,QPS 2 | `yudao-module-infra/src/main/resources/sentinel/` | | ||
| 20 | +| `src/main/resources/sentinel/demo-degrade-rules.json` | 熔断规则:资源 `demo-sentinel-degrade`,慢调用比例 | `yudao-module-infra/src/main/resources/sentinel/` | | ||
| 21 | +| `src/main/java/.../infra/sentineldemo/DemoSentinelConfiguration.java` | 注册 `SentinelResourceAspect` 切面,启动时加载上面两个规则文件 | `yudao-module-infra/src/main/java` 的同名包 | | ||
| 22 | +| `src/main/java/.../infra/sentineldemo/controller/admin/DemoSentinelController.java` | 示例接口 `/admin-api/infra/sentinel-demo/flow`、`/admin-api/infra/sentinel-demo/degrade`,及被阻断时的 `blockHandler` | `yudao-module-infra/src/main/java` 的同名包 | | ||
| 23 | + | ||
| 24 | +示例的包名在 `yudao.info.base-package`(`cn.iocoder.yudao`)下,才能被 Spring 扫描到;Controller 放在 `controller.admin` 子包下,才会添加 `/admin-api` 前缀。规则 JSON 是严格的 JSON,不能包含注释,字段说明见项目文档。 | ||
| 25 | + | ||
| 26 | +## 2. 分支适用范围 | ||
| 27 | + | ||
| 28 | +目前只有 `master-jdk17` 分支提供本目录的材料;`master`、`master-jdk25` 分支请先从 `master-jdk17` 分支的 `script/sentinel/` 获取,同步到对应分支后,也可以直接从当前分支获取。 | ||
| 29 | + | ||
| 30 | +本目录的材料按 `master`(JDK 8 + Spring Boot 2.7.18)、`master-jdk17`(Spring Boot 3.5.15)、`master-jdk25`(Spring Boot 4.1.2)三个分支的 API 编写,接入步骤相同,兼容要点如下: | ||
| 31 | + | ||
| 32 | +| 要点 | 说明 | | ||
| 33 | +| --- | --- | | ||
| 34 | +| Sentinel 坐标 | 显式指定 `com.alibaba.csp:*:1.8.10`,三个依赖的版本保持一致。单体项目没有 Spring Cloud Alibaba 的 BOM,不要为了 Sentinel 引入 Spring Cloud 相关的 BOM 或 starter | | ||
| 35 | +| JDK 8 | Sentinel 1.8.10 的常规类按 Java 8 字节码发布(仅 `module-info.class` 面向 Java 9+),`master` 分支可以使用 | | ||
| 36 | +| AOP | `@SentinelResource` 依赖 Spring AOP,项目的 Security 组件已经引入(`master`、`master-jdk17` 是 `spring-boot-starter-aop`,`master-jdk25` 是 `spring-boot-starter-aspectj`),不需要额外添加 | | ||
| 37 | +| 示例代码 | 只使用 Java 8 语法,不引用 `javax`、`jakarta` 包;规则通过项目的 `JsonUtils` 解析,不区分 Jackson 2、Jackson 3,三个分支无需修改 | | ||
| 38 | +| Web 适配模块 | 示例没有使用自动统计所有 URL 的 Web 适配模块:它在 `master` 分支是 `sentinel-spring-webmvc-adapter`(`javax`),`master-jdk17` 分支是 `sentinel-spring-webmvc-v6x-adapter`(`jakarta`),两者的包名、`BlockExceptionHandler` 方法签名都不同,需要按分支自行接入 | | ||
| 39 | +| `master-jdk25` | Sentinel 官方没有声明对 Spring Boot 4、Spring Framework 7 的适配,接入后以实际的启动、请求验证为准 | |
script/sentinel/dependencies.xml
0 → 100644
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | +<!-- | ||
| 3 | + 限流熔断(Sentinel 1.8.10)的 Maven 依赖片段,不是 Maven 工程,不参与项目构建。 | ||
| 4 | + | ||
| 5 | + 使用时,将下面的 <dependency> 复制到【包含 @SentinelResource 等 Sentinel 代码】的模块的 pom.xml 的 <dependencies> 中: | ||
| 6 | + - 示例代码在 yudao-module-infra 中,添加到 yudao-module-infra/pom.xml; | ||
| 7 | + yudao-server 已经依赖 yudao-module-infra,会传递获得这些依赖。只加在 yudao-server/pom.xml 时,yudao-module-infra 编译失败 | ||
| 8 | + | ||
| 9 | + 三个分支(master、master-jdk17、master-jdk25)复制同样的内容,不需要按分支选择。 | ||
| 10 | + 单体项目没有 Spring Cloud Alibaba 的 BOM,所以显式指定 Sentinel 的版本;不要为了 Sentinel 引入 Spring Cloud、Spring Cloud Alibaba 的 BOM 或 starter。 | ||
| 11 | + 三个依赖的版本必须一致。 | ||
| 12 | +--> | ||
| 13 | +<dependencies> | ||
| 14 | + | ||
| 15 | + <!-- ========== 1. Sentinel 核心 + @SentinelResource 注解切面:必须 ========== --> | ||
| 16 | + <!-- | ||
| 17 | + sentinel-annotation-aspectj 会传递依赖 aspectjweaver,版本由项目的 Spring Boot BOM 统一管理,不会改变项目原有的版本。 | ||
| 18 | + 示例不使用 sentinel-spring-webmvc-adapter 等 Web 适配模块,不区分 javax、jakarta。 | ||
| 19 | + --> | ||
| 20 | + <dependency> | ||
| 21 | + <groupId>com.alibaba.csp</groupId> | ||
| 22 | + <artifactId>sentinel-core</artifactId> | ||
| 23 | + <version>1.8.10</version> | ||
| 24 | + </dependency> | ||
| 25 | + <dependency> | ||
| 26 | + <groupId>com.alibaba.csp</groupId> | ||
| 27 | + <artifactId>sentinel-annotation-aspectj</artifactId> | ||
| 28 | + <version>1.8.10</version> | ||
| 29 | + </dependency> | ||
| 30 | + | ||
| 31 | + <!-- ========== 2. 与 Sentinel Dashboard 通信:可选,不使用 Dashboard 时不需要添加 ========== --> | ||
| 32 | + <!-- | ||
| 33 | + 添加后,应用会在本机开启 Sentinel 的命令端口(默认 8719,被占用时自动递增),Dashboard 通过它查询监控数据、推送规则。 | ||
| 34 | + 该端口没有鉴权,只能在内网访问,不要对公网开放。 | ||
| 35 | + 它会传递依赖 fastjson(1.2.83_noneautotype,Sentinel 内部的命令处理使用),不会替换项目接口使用的 Jackson。 | ||
| 36 | + --> | ||
| 37 | + <dependency> | ||
| 38 | + <groupId>com.alibaba.csp</groupId> | ||
| 39 | + <artifactId>sentinel-transport-simple-http</artifactId> | ||
| 40 | + <version>1.8.10</version> | ||
| 41 | + </dependency> | ||
| 42 | + | ||
| 43 | +</dependencies> |
script/sentinel/src/main/java/cn/iocoder/yudao/module/infra/sentineldemo/DemoSentinelConfiguration.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.infra.sentineldemo; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.io.resource.ResourceUtil; | ||
| 4 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 5 | +import com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect; | ||
| 6 | +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; | ||
| 7 | +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; | ||
| 8 | +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; | ||
| 9 | +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; | ||
| 10 | +import lombok.extern.slf4j.Slf4j; | ||
| 11 | +import org.springframework.beans.factory.SmartInitializingSingleton; | ||
| 12 | +import org.springframework.context.annotation.Bean; | ||
| 13 | +import org.springframework.context.annotation.Configuration; | ||
| 14 | + | ||
| 15 | +import java.util.List; | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * Sentinel 限流熔断示例的配置类 | ||
| 19 | + * | ||
| 20 | + * 1. 注册 {@link SentinelResourceAspect} 切面,让 @SentinelResource 注解生效。单体项目没有引入 Spring Cloud Alibaba,需要自己注册 | ||
| 21 | + * 2. 启动时,从 classpath 的 sentinel 目录加载示例的限流、熔断规则 | ||
| 22 | + * | ||
| 23 | + * @author 芋道源码 | ||
| 24 | + */ | ||
| 25 | +@Configuration(value = "demoSentinelConfiguration", proxyBeanMethods = false) | ||
| 26 | +@Slf4j | ||
| 27 | +public class DemoSentinelConfiguration { | ||
| 28 | + | ||
| 29 | + /** | ||
| 30 | + * 限流规则的 JSON 文件,资源名与 DemoSentinelController 中 @SentinelResource 的 value 一致 | ||
| 31 | + */ | ||
| 32 | + private static final String FLOW_RULES_PATH = "sentinel/demo-flow-rules.json"; | ||
| 33 | + /** | ||
| 34 | + * 熔断规则的 JSON 文件 | ||
| 35 | + */ | ||
| 36 | + private static final String DEGRADE_RULES_PATH = "sentinel/demo-degrade-rules.json"; | ||
| 37 | + | ||
| 38 | + @Bean | ||
| 39 | + public SentinelResourceAspect sentinelResourceAspect() { | ||
| 40 | + return new SentinelResourceAspect(); | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + /** | ||
| 44 | + * 加载示例的规则:在所有单例 Bean 初始化之后、Web 服务器开始接收请求之前执行 | ||
| 45 | + * | ||
| 46 | + * 注意,规则只保存在当前 JVM 的内存中: | ||
| 47 | + * 1. loadRules 会替换同一类型的全部规则,自己的业务有其它规则来源时,需要合并后再加载 | ||
| 48 | + * 2. 通过 Sentinel Dashboard 修改的规则,重启后会恢复成 JSON 文件中的规则 | ||
| 49 | + */ | ||
| 50 | + @Bean | ||
| 51 | + public SmartInitializingSingleton demoSentinelRuleLoader() { | ||
| 52 | + return () -> { | ||
| 53 | + FlowRuleManager.loadRules(parseRules(FLOW_RULES_PATH, FlowRule.class)); | ||
| 54 | + DegradeRuleManager.loadRules(parseRules(DEGRADE_RULES_PATH, DegradeRule.class)); | ||
| 55 | + // 打印实际生效的规则数:不合法的规则(例如说 count 小于 0)会被 Sentinel 忽略,此时数量会少于 JSON 文件中的 | ||
| 56 | + log.info("[demoSentinelRuleLoader][加载 Sentinel 规则完成,限流规则({}) 条,熔断规则({}) 条]", | ||
| 57 | + FlowRuleManager.getRules().size(), DegradeRuleManager.getRules().size()); | ||
| 58 | + }; | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + private static <T> List<T> parseRules(String path, Class<T> clazz) { | ||
| 62 | + return JsonUtils.parseArray(ResourceUtil.readUtf8Str(path), clazz); | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | +} |
script/sentinel/src/main/java/cn/iocoder/yudao/module/infra/sentineldemo/controller/admin/DemoSentinelController.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.infra.sentineldemo.controller.admin; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.thread.ThreadUtil; | ||
| 4 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | ||
| 5 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 6 | +import com.alibaba.csp.sentinel.annotation.SentinelResource; | ||
| 7 | +import com.alibaba.csp.sentinel.slots.block.BlockException; | ||
| 8 | +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException; | ||
| 9 | +import io.swagger.v3.oas.annotations.Operation; | ||
| 10 | +import io.swagger.v3.oas.annotations.Parameter; | ||
| 11 | +import io.swagger.v3.oas.annotations.tags.Tag; | ||
| 12 | +import org.springframework.http.HttpStatus; | ||
| 13 | +import org.springframework.web.bind.annotation.GetMapping; | ||
| 14 | +import org.springframework.web.bind.annotation.RequestMapping; | ||
| 15 | +import org.springframework.web.bind.annotation.RequestParam; | ||
| 16 | +import org.springframework.web.bind.annotation.RestController; | ||
| 17 | + | ||
| 18 | +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.TOO_MANY_REQUESTS; | ||
| 19 | +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | ||
| 20 | + | ||
| 21 | +/** | ||
| 22 | + * 管理后台 - Sentinel 限流熔断示例 | ||
| 23 | + * | ||
| 24 | + * 1. 每个接口通过 @SentinelResource 定义一个资源,资源名与 sentinel 目录下 JSON 规则中的 resource 一一对应 | ||
| 25 | + * 2. 被限流、熔断时,调用同一个类中的 blockHandler 方法,返回 CommonResult 错误码,与 @RateLimiter 被限流时的返回格式一致 | ||
| 26 | + * | ||
| 27 | + * 放在 controller.admin 包下,才会和项目其它管理后台接口一样,添加 /admin-api 前缀 | ||
| 28 | + * | ||
| 29 | + * @author 芋道源码 | ||
| 30 | + */ | ||
| 31 | +@Tag(name = "管理后台 - Sentinel 示例") | ||
| 32 | +@RestController | ||
| 33 | +@RequestMapping("/infra/sentinel-demo") | ||
| 34 | +public class DemoSentinelController { | ||
| 35 | + | ||
| 36 | + @GetMapping("/flow") | ||
| 37 | + @Operation(summary = "限流示例", description = "资源 demo-sentinel-flow,每秒最多通过 2 个请求") | ||
| 38 | + @SentinelResource(value = "demo-sentinel-flow", blockHandler = "flowBlockHandler") | ||
| 39 | + public CommonResult<String> flow() { | ||
| 40 | + return success("flow"); | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + @GetMapping("/degrade") | ||
| 44 | + @Operation(summary = "熔断示例", description = "资源 demo-sentinel-degrade,慢调用比例超过阈值时熔断 10 秒") | ||
| 45 | + @Parameter(name = "slow", description = "是否模拟慢调用", example = "false") | ||
| 46 | + @SentinelResource(value = "demo-sentinel-degrade", blockHandler = "degradeBlockHandler", | ||
| 47 | + exceptionsToIgnore = ServiceException.class) // 业务异常不计入异常比例、异常数的熔断统计,由全局异常处理器照常返回错误码 | ||
| 48 | + public CommonResult<String> degrade(@RequestParam(value = "slow", defaultValue = "false") Boolean slow) { | ||
| 49 | + if (Boolean.TRUE.equals(slow)) { | ||
| 50 | + ThreadUtil.sleep(500); // 模拟慢调用:超过熔断规则中 200 毫秒的最大响应时间 | ||
| 51 | + return success("slow"); | ||
| 52 | + } | ||
| 53 | + return success("ok"); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + // ========== blockHandler:被 Sentinel 阻断时调用。参数为原方法的参数 + BlockException,返回值类型与原方法相同 ========== | ||
| 57 | + | ||
| 58 | + public CommonResult<String> flowBlockHandler(BlockException ex) { | ||
| 59 | + return buildBlockResult(ex); | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + public CommonResult<String> degradeBlockHandler(Boolean slow, BlockException ex) { | ||
| 63 | + return buildBlockResult(ex); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + /** | ||
| 67 | + * 将 BlockException 转换成 CommonResult:熔断返回 503,限流等其它阻断返回 429 | ||
| 68 | + */ | ||
| 69 | + private static <T> CommonResult<T> buildBlockResult(BlockException ex) { | ||
| 70 | + if (ex instanceof DegradeException) { | ||
| 71 | + return CommonResult.error(HttpStatus.SERVICE_UNAVAILABLE.value(), "服务暂时不可用,请稍后重试"); | ||
| 72 | + } | ||
| 73 | + return CommonResult.error(TOO_MANY_REQUESTS); | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | +} |
script/sentinel/src/main/resources/sentinel/demo-degrade-rules.json
0 → 100644