限流熔断
项目默认没有接入 Sentinel。yudao-spring-boot-starter-protection (opens new window) 组件本身也不包含 Sentinel,它提供的是基于 Redis(Redisson)的 《请求限流(RateLimiter)》、《幂等性》、《分布式锁》。
需要按 QPS 限流、按慢调用或异常比例熔断,并且规则与代码分离时,可以基于 Sentinel (opens new window) 按需接入,实际使用的依赖版本见《3.1 添加依赖》小节:
- 只有添加了
@SentinelResource注解、并且配置了规则的资源,才会被限流、熔断,其它接口不受影响。 - 示例的规则从 classpath 中的 JSON 文件加载,只保存在 JVM 内存中,参见《4.4 规则持久化》小节。
- 单体项目不需要、也不要为了 Sentinel 引入 Spring Cloud、Spring Cloud Alibaba,只使用 Sentinel 自己的核心及注解切面。
友情提示:
微服务项目 yudao-cloud 在网关、服务、Feign 中接入 Sentinel,并将规则持久化到 Nacos,参见 《服务保障》 (opens new window) 文档。
# 1. 方案说明
# 1.1 与 @RateLimiter 的区别
两者解决的问题不同,可以同时使用:
@RateLimiter | Sentinel | |
|---|---|---|
| 计数位置 | Redis,多个实例共享同一个计数,是全局的配额 | 每个 JVM 进程的内存,阈值是单机的,参见《4.5 单机阈值》小节 |
| 限流维度 | 时间窗口内最多 N 次,可以按全局、用户、IP、服务节点、Spring EL 表达式分别计数 | 按资源的 QPS 或并发线程数,支持快速失败、预热、匀速排队 |
| 熔断降级 | 不支持 | 慢调用比例、异常比例、异常数 |
| 规则 | 注解的属性,修改后需要重新发布 | 与代码分离,示例从 JSON 文件加载,也可以接入 Nacos 等动态数据源 |
| 外部依赖 | Redis | 无,Dashboard 可选 |
| 被限制时 | 抛出 ServiceException,code = 429 | 调用 blockHandler,示例返回 CommonResult 错误码 |
简单来说:「每个用户每分钟只能发送 1 次短信」这类业务配额,使用 @RateLimiter;「保护服务实例不被流量压垮、依赖变慢时快速失败」这类流量防护,使用 Sentinel。
# 1.2 限流与熔断
① 限流(FlowRule):保护当前接口。单位时间内超过阈值的请求直接拒绝。
② 熔断(DegradeRule):保护调用链路。资源的慢调用比例、异常比例超过阈值时,在熔断时长内直接拒绝,不再等待它变慢或出错;熔断时长结束后,放行一个探测请求,成功则恢复,失败则继续熔断。通常加在调用数据库、第三方接口等依赖的方法上。
两者被触发时,都由 @SentinelResource 的 blockHandler 方法决定返回什么。
# 1.3 示例
示例的接口在 yudao-module-infra 模块中,需要登录:
| 接口 | 资源名 | 规则 | 被阻断时 |
|---|---|---|---|
GET /admin-api/infra/sentinel-demo/flow | demo-sentinel-flow | 限流:QPS 2 | code = 429,msg = "请求过于频繁,请稍后重试" |
GET /admin-api/infra/sentinel-demo/degrade?slow=false | demo-sentinel-degrade | 熔断:慢调用比例,slow=true 时等待 500 毫秒模拟慢调用 | code = 503,msg = "服务暂时不可用,请稍后重试" |
被阻断时,与 @RateLimiter 一样,返回 HTTP 200 + CommonResult 错误码,前端按业务错误码的方式提示 msg。
# 2. 获取集成材料
集成材料在项目的 script/sentinel/ (opens new window) 目录。目前只有 master-jdk17 分支提供这些材料,master、master-jdk25 分支请先从 master-jdk17 分支获取,同步到对应分支后,也可以直接从当前分支获取。材料按下面三个分支的 API 编写,接入步骤相同,兼容差异如下:
| 分支 | JDK | Spring Boot | 差异 |
|---|---|---|---|
master | 8 | 2.7.18 | 无。Sentinel 1.8.10 的常规类按 Java 8 字节码发布 |
master-jdk17 | 17 | 3.5.15 | 无 |
master-jdk25 | 25 | 4.1.2 | 无需修改材料。Sentinel 官方没有声明对 Spring Boot 4、Spring Framework 7 的适配,接入后以实际的启动、请求验证为准 |
示例代码只使用 Java 8 语法,不引用 javax、jakarta 包,规则通过项目的 JsonUtils 解析,不区分 Jackson 2、Jackson 3,所以三个分支无需修改。
目录中的文件及用途如下:
| 文件 | 说明 | 目标位置 |
|---|---|---|
dependencies.xml | Maven 依赖片段:第 1 段 Sentinel 核心及注解切面(必须),第 2 段与 Dashboard 通信的模块(可选) | yudao-module-infra/pom.xml |
src/main/resources/sentinel/demo-flow-rules.json | 限流规则 | yudao-module-infra/src/main/resources/sentinel/ |
src/main/resources/sentinel/demo-degrade-rules.json | 熔断规则 | yudao-module-infra/src/main/resources/sentinel/ |
src/main/java/.../infra/sentineldemo/DemoSentinelConfiguration.java | 注册 SentinelResourceAspect 切面,启动时加载上面两个规则文件 | yudao-module-infra/src/main/java/ 的同名包 |
src/main/java/.../infra/sentineldemo/controller/admin/DemoSentinelController.java | 示例接口,及被阻断时的 blockHandler | yudao-module-infra/src/main/java/ 的同名包 |
# 3. 接入步骤
# 3.1 添加依赖
将 dependencies.xml 第 1 段的依赖,复制到 yudao-module-infra/pom.xml 中:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.10</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
<version>1.8.10</version>
</dependency>
① 添加到包含 Sentinel 代码的模块,示例是 yudao-module-infra;yudao-server 已经依赖它,会传递获得。只加在 yudao-server/pom.xml 时,yudao-module-infra 编译失败。
② 单体项目没有 Spring Cloud Alibaba 的 BOM,所以显式指定版本,多个 Sentinel 依赖的版本必须一致。sentinel-annotation-aspectj 传递依赖的 aspectjweaver,版本由项目的 Spring Boot BOM 统一管理,不会改变项目原有的版本。
③ @SentinelResource 依赖 Spring AOP,项目的 Security 组件已经引入,不需要额外添加。
④ 需要使用 Sentinel Dashboard 时,再添加第 2 段的 sentinel-transport-simple-http,参见《3.3 启动 Sentinel Dashboard》小节。
# 3.2 复制规则与示例代码
① 将 src/main/resources/sentinel/ 目录,复制到 yudao-module-infra/src/main/resources/ 下。
demo-flow-rules.json:
[
{
"resource": "demo-sentinel-flow",
"limitApp": "default",
"grade": 1,
"count": 2,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
demo-degrade-rules.json:
[
{
"resource": "demo-sentinel-degrade",
"grade": 0,
"count": 200,
"slowRatioThreshold": 0.5,
"minRequestAmount": 3,
"statIntervalMs": 10000,
"timeWindow": 10
}
]
即:资源 demo-sentinel-flow 每秒最多通过 2 个请求;资源 demo-sentinel-degrade 在 10 秒内至少有 3 个请求、并且响应时间超过 200 毫秒的比例超过 50% 时,熔断 10 秒。规则文件必须是严格的 JSON,不能包含 // 注释,字段的含义参见《4.2 限流规则》、《4.3 熔断规则》小节。
② 将 src/main/java 下的 cn.iocoder.yudao.module.infra.sentineldemo 包,复制到 yudao-module-infra/src/main/java 的同名目录:
DemoSentinelConfiguration:注册SentinelResourceAspect切面,让@SentinelResource注解生效;通过SmartInitializingSingleton,在所有单例 Bean 初始化之后、Web 服务器开始接收请求之前,加载上面两个规则文件。controller/admin/DemoSentinelController:示例接口。放在controller.admin包下,才会和项目其它管理后台接口一样,添加/admin-api前缀。
示例接口的核心代码如下:
@GetMapping("/flow")
@SentinelResource(value = "demo-sentinel-flow", blockHandler = "flowBlockHandler")
public CommonResult<String> flow() {
return success("flow");
}
@GetMapping("/degrade")
@SentinelResource(value = "demo-sentinel-degrade", blockHandler = "degradeBlockHandler",
exceptionsToIgnore = ServiceException.class) // 业务异常不计入异常比例、异常数的熔断统计,由全局异常处理器照常返回错误码
public CommonResult<String> degrade(@RequestParam(value = "slow", defaultValue = "false") Boolean slow) {
if (Boolean.TRUE.equals(slow)) {
ThreadUtil.sleep(500); // 模拟慢调用:超过熔断规则中 200 毫秒的最大响应时间
return success("slow");
}
return success("ok");
}
// blockHandler:被 Sentinel 阻断时调用。参数为原方法的参数 + BlockException,返回值类型与原方法相同
public CommonResult<String> flowBlockHandler(BlockException ex) {
return buildBlockResult(ex);
}
public CommonResult<String> degradeBlockHandler(Boolean slow, BlockException ex) {
return buildBlockResult(ex);
}
private static <T> CommonResult<T> buildBlockResult(BlockException ex) {
if (ex instanceof DegradeException) { // 熔断
return CommonResult.error(HttpStatus.SERVICE_UNAVAILABLE.value(), "服务暂时不可用,请稍后重试");
}
return CommonResult.error(TOO_MANY_REQUESTS); // 限流等其它阻断
}
# 3.3 启动 Sentinel Dashboard(可选)
Sentinel Dashboard (opens new window) 用于查看实时的 QPS、阻断次数,以及资源名,不是必须的:不启动 Dashboard,规则照常生效。
① 从 Sentinel 的 Releases (opens new window) 下载 sentinel-dashboard-1.8.10.jar,它是独立的 Spring Boot 应用,与项目使用的 JDK 版本无关。设置好 SENTINEL_DASHBOARD_PASSWORD 环境变量后启动:
java -Dserver.address=127.0.0.1 -Dserver.port=8858 \
-Dsentinel.dashboard.auth.username=sentinel \
-Dsentinel.dashboard.auth.password="${SENTINEL_DASHBOARD_PASSWORD}" \
-jar sentinel-dashboard-1.8.10.jar
-Dserver.address=127.0.0.1:只在本机监听,yudao-server与 Dashboard 运行在同一台机器上。-Dserver.port=8858:Dashboard 默认使用 8080 端口,改为 8858,避免与其它程序冲突。sentinel.dashboard.auth.*:Dashboard 的登录账号,默认是sentinel/sentinel,不要在共享环境中使用默认密码。
② 将 dependencies.xml 第 2 段的依赖,也添加到 yudao-module-infra/pom.xml 中:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.10</version>
</dependency>
③ 在 yudao-server 的启动参数(IDEA Run Configuration 的 VM options)中添加:
-Dcsp.sentinel.dashboard.server=127.0.0.1:8858 -Dproject.name=yudao-server -Dcsp.sentinel.api.port=8719
csp.sentinel.dashboard.server:Dashboard 的地址。project.name:在 Dashboard 中显示的应用名。csp.sentinel.api.port:Sentinel 在应用中开启的命令端口,Dashboard 通过它拉取监控数据、推送规则,被占用时自动递增。
命令端口没有鉴权
添加 sentinel-transport-simple-http 后,应用会在本机开启上面的命令端口,可以查询、修改规则,并且没有鉴权。只能在本机或内网访问,通过防火墙、安全组限制,不要对公网开放。它还会传递依赖 fastjson(Sentinel 内部的命令处理使用),不会替换项目接口使用的 Jackson。
# 3.4 启动验证
① 启动 yudao-server,日志中出现下面的内容,说明两条规则都已经加载:
[demoSentinelRuleLoader][加载 Sentinel 规则完成,限流规则(1) 条,熔断规则(1) 条]
数量少于 JSON 文件中的规则数时,说明有规则不合法、被 Sentinel 忽略了,例如说 count 小于 0。
② 下面的请求中,Authorization: Bearer test1 是项目的模拟登录:local 环境默认开启 yudao.security.mock-enable,test + 用户编号 1 即可模拟登录,其它环境需要使用登录接口返回的真实访问令牌;tenant-id: 1 是租户编号。
限流:连续请求 5 次,每秒只有 2 个请求通过:
for i in 1 2 3 4 5; do
curl -s -w '\n' -H 'Authorization: Bearer test1' -H 'tenant-id: 1' \
'http://127.0.0.1:48080/admin-api/infra/sentinel-demo/flow'
done
{"code":0,"msg":"","data":"flow"}
{"code":0,"msg":"","data":"flow"}
{"code":429,"msg":"请求过于频繁,请稍后重试"}
{"code":429,"msg":"请求过于频繁,请稍后重试"}
{"code":429,"msg":"请求过于频繁,请稍后重试"}
输出仅供参考,字段顺序、是否包含 "data":null 以实际的 JSON 配置为准。限流按 1 秒的滑动窗口统计,第一个请求较慢时,可能有 3 个请求通过,稍等 1 秒再执行一次即可。
③ 熔断:
URL='http://127.0.0.1:48080/admin-api/infra/sentinel-demo/degrade'
H1='Authorization: Bearer test1'; H2='tenant-id: 1'
# 1. 连续 3 次慢调用,每次约 0.5 秒:慢调用比例 100%,超过 50%,熔断 10 秒
for i in 1 2 3; do curl -s -w '\n' -H "$H1" -H "$H2" "$URL?slow=true"; done
# 2. 熔断期间,即使不是慢调用,也直接返回 blockHandler 的错误码
curl -s -w '\n' -H "$H1" -H "$H2" "$URL"
# 3. 10 秒后,第一个请求作为探测放行,成功且不慢,熔断恢复
sleep 10; curl -s -w '\n' -H "$H1" -H "$H2" "$URL"
| 步骤 | 结果 |
|---|---|
| 1 | 3 次都成功:code = 0,data = "slow" |
| 2 | code = 503,msg = "服务暂时不可用,请稍后重试" |
| 3 | code = 0,data = "ok" |
④(可选)Dashboard:浏览器访问 http://127.0.0.1:8858 (opens new window) 登录。Sentinel 在第一次进入资源时才初始化,请求一次示例接口后,Dashboard 中才会出现 yudao-server 应用;「簇点链路」中列出了 demo-sentinel-flow、demo-sentinel-degrade 两个资源,「实时监控」中可以看到通过、拒绝的 QPS。添加了第 2 段依赖时,也可以通过命令端口查看内存中的规则:
curl -s 'http://127.0.0.1:8719/getRules?type=flow'
curl -s 'http://127.0.0.1:8719/getRules?type=degrade'
验证完成后,可以删除 DemoSentinelController 及两个示例规则,DemoSentinelConfiguration 中加载规则的方式可以保留,改为加载自己业务的规则。
# 4. 使用说明
# 4.1 资源与 @SentinelResource
资源是 Sentinel 统计、保护的单位,规则通过 resource 字段与资源一一对应。@SentinelResource 可以加在 Controller、Service 等 Spring Bean 的方法上:
| 属性 | 说明 |
|---|---|
value | 资源名,必填,与规则的 resource 一致。建议使用「模块-业务-操作」这类稳定的名字,不要包含参数 |
blockHandler | 被限流、熔断时调用的方法:必须是 public,参数为原方法的参数 + BlockException,返回值类型与原方法相同 |
blockHandlerClass | blockHandler 方法在其它类中时指定,此时该方法必须是 static 的,可以用于多个资源共用 |
exceptionsToIgnore | 不计入熔断统计、直接抛出的异常,建议排除 ServiceException 业务异常 |
fallback、defaultFallback | 业务方法抛出异常时的降级方法。没有配置时,异常照常抛出,由全局异常处理器处理;配置时,注意不要吞掉异常、伪造成功的结果 |
注意事项:
- 和
@Transactional一样,基于 Spring AOP 实现:通过this.xxx()自调用、private方法、非 Spring Bean 上的注解不会生效。 - 没有配置
blockHandler时,被阻断会抛出BlockException。它是受检异常,方法没有声明时会变成UndeclaredThrowableException,由全局异常处理器返回「系统异常」,所以一定要配置blockHandler。 - 也可以不使用注解,通过
SphU.entry("资源名")+try-with-resources的方式定义资源,参见 Sentinel 的 《基本使用》 (opens new window) 文档。
# 4.2 限流规则
demo-flow-rules.json 的字段:
| 字段 | 说明 |
|---|---|
resource | 资源名 |
limitApp | 调用来源,default 表示不区分 |
grade | 阈值类型:1 QPS,0 并发线程数 |
count | 阈值,单个实例的 |
strategy | 流控模式:0 直接,1 关联,2 链路 |
controlBehavior | 流控效果:0 快速失败,1 预热(配合 warmUpPeriodSec,单位:秒),2 匀速排队(配合 maxQueueingTimeMs,单位:毫秒) |
clusterMode | 是否集群限流,保持 false,参见《4.5 单机阈值》小节 |
# 4.3 熔断规则
demo-degrade-rules.json 的字段:
| 字段 | 说明 |
|---|---|
resource | 资源名 |
grade | 熔断策略:0 慢调用比例,1 异常比例,2 异常数 |
count | 慢调用比例时,是最大响应时间(单位:毫秒),超过即为慢调用;异常比例时,是比例阈值(0.0 ~ 1.0);异常数时,是异常的个数 |
slowRatioThreshold | 慢调用比例的阈值(0.0 ~ 1.0),只在慢调用比例时有效 |
minRequestAmount | 统计时长内的最小请求数,请求数不足时不会熔断 |
statIntervalMs | 统计时长,单位:毫秒 |
timeWindow | 熔断时长,单位:秒。到期后放行一个探测请求,成功则恢复,失败则继续熔断 |
按异常比例、异常数熔断时,业务方法抛出、并且没有被 exceptionsToIgnore 排除的异常才会被统计。所以,要通过 exceptionsToIgnore = ServiceException.class 排除参数错误、余额不足等业务异常,否则用户的错误操作也可能触发熔断。
# 4.4 规则持久化
示例的规则来自 classpath 中的 JSON 文件,只保存在 JVM 内存中:
- 修改规则:修改 JSON 文件,重新发布或重启。
FlowRuleManager.loadRules、DegradeRuleManager.loadRules会整体替换同一类型的规则,自己的业务有多个规则来源时,需要合并后再加载。- 通过 Dashboard 修改的规则,只推送到当前实例的内存中,不会写回 JSON 文件,重启后恢复为 JSON 文件中的规则;有多个实例时,也只修改了其中一台。
需要运行时动态修改、多个实例统一生效时,可以将规则放到 Nacos、Apollo 等配置中心,通过 Sentinel 的数据源扩展(例如说 sentinel-datasource-nacos)注册到 FlowRuleManager.register2Property,替换示例中加载 JSON 文件的方式,参见 Sentinel 的 《动态规则扩展》 (opens new window) 文档,本文不提供。
# 4.5 单机阈值
规则中的 count 是单个实例的阈值,Sentinel 在每个实例的内存中分别计数。yudao-server 部署 3 个实例时,count = 2 的限流规则,整体每秒最多可以通过约 6 个请求。
需要全局、精确的配额时(例如说每个用户每分钟只能调用 N 次),使用基于 Redis 的 《请求限流(RateLimiter)》。Sentinel 的集群流控(Token Server)需要额外的部署和运维,本文不使用。
# 4.6 自动统计所有接口(可选)
示例只保护添加了 @SentinelResource 的接口。如果希望 Sentinel 自动为每个 Spring MVC 接口创建资源(资源名为接口的路径),需要引入 Sentinel 的 Web 适配模块,并注册它的拦截器。它与 Servlet 的包名有关,三个分支不同,本文不提供,只列出差异:
| 分支 | 模块 | BlockExceptionHandler |
|---|---|---|
master | sentinel-spring-webmvc-adapter(javax) | com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler,handle(request, response, ex) |
master-jdk17 | sentinel-spring-webmvc-v6x-adapter(jakarta) | com.alibaba.csp.sentinel.adapter.spring.webmvc_v6x.callback.BlockExceptionHandler,handle(request, response, resourceName, ex) |
master-jdk25 | 同 master-jdk17 | 同 master-jdk17,它面向 Spring Framework 6,在 Spring Framework 7 中需要自行验证 |
接入时,在 WebMvcConfigurer#addInterceptors 中注册 SentinelWebInterceptor,并通过 SentinelWebMvcConfig#setBlockExceptionHandler 设置返回 CommonResult 的处理器,否则被阻断时返回的是 Sentinel 默认的 Blocked by Sentinel 文本。
# 4.7 安全注意事项
- 命令端口:参见《3.3 启动 Sentinel Dashboard》小节,只能在本机或内网访问。
- Dashboard:修改默认的账号密码,只在内网访问。
- 本文不使用 Sentinel 的授权规则(黑白名单):它依据的调用来源,通常取自客户端可以伪造的请求头,不能替代 Spring Security 的认证、鉴权。
# 5. 常见问题
# 5.1 请求没有被限流、熔断
- 启动日志中,加载的规则数是否符合预期,参见《3.4 启动验证》小节。
- 规则的
resource与@SentinelResource的value是否完全一致。 @SentinelResource所在的类,是否是 Spring Bean,是否是通过this.xxx()自调用。- 是否部署了多个实例,请求被负载均衡到了不同的实例上,参见《4.5 单机阈值》小节。
# 5.2 被限流时,返回「系统异常」
没有配置 blockHandler,或者 blockHandler 方法的参数、返回值与原方法不匹配,Sentinel 找不到它,BlockException 变成了 UndeclaredThrowableException。参见《4.1 资源与 @SentinelResource》小节检查方法签名。
# 5.3 报错 JsonParseException 等规则解析失败
规则文件不是严格的 JSON,例如说包含 // 注释、结尾多余的逗号。可以先用 JSON 格式化工具校验。
# 5.4 Dashboard 中看不到应用
- 是否添加了
sentinel-transport-simple-http依赖,以及《3.3 启动 Sentinel Dashboard》小节的启动参数。 - 是否请求过一次示例接口:Sentinel 在第一次进入资源时才初始化、连接 Dashboard。
- Dashboard 需要反过来访问应用的命令端口:应用在 Docker 等环境中时,通过
-Dcsp.sentinel.heartbeat.client.ip设置 Dashboard 能够访问到的 IP,并开放对应的端口。