专栏:《Java高级进阶》90天进阶系列 版本环境:Spring Boot 3.2.x + Spring Cloud Alibaba 2023.0.1.0 + Sentinel 1.8.6
一、开篇:大促那一夜,限流救了我们
有一年双 11,公司搞了一场优惠券秒杀。接口上线前压测明明过了,结果活动开始 3 分钟,数据库连接池打满,Redis 主节点被打到 CPU 飙到 95%,整个下单链路全崩。
事后看监控,羊毛党的脚本和正常用户混在一起,QPS 曲线像一把刀一样竖起来。我们说:"下次一定要做限流。" 但限流怎么做才不算"一刀切"?把所有请求都拒掉,正常用户也一起骂娘。
Sentinel 就是用来解决这个问题的。它不是简单的"超过阈值就 429",而是把流量当成一个立体系统来治理:入口限流、链路隔离、熔断降级、热点防护、系统自适应保护,一层一层把风险关在笼子里。
今天这一篇,把生产里用得最多的 4 个能力串起来,给你一套能直接落地的代码。
二、Sentinel 的设计精髓:一条 Slot 责任链
理解 Sentinel 最快的方式,不是先背概念,而是先看它的执行链。
Sentinel 把每一次资源访问都抽象成一条调用链,链上每个节点叫 Slot(槽),各司其职:
-
NodeSelectorSlot:按调用链路构建树,方便你做链路限流。
-
ClusterBuilderSlot:统计集群维度的总流量。
-
StatisticSlot:滑动窗口计数,所有限流/熔断的判断数据都来自这里。
-
FlowSlot:核心流控,支持 QPS、线程数、关联、链路等多种策略。
-
DegradeSlot:熔断降级,慢调用、异常比例、异常数三种触发条件。
-
SystemSlot:系统保护,根据 Load、CPU、RT、线程数做入口总控。
-
ParamFlowSlot:热点参数限流,针对某个参数值(比如用户 ID、商品 ID)做精细控制。
这条链的最大好处是:每一层只做一件事,组合起来就是一套完整的防御体系。
三、代码实战 1:流量控制,先学会"精准拒绝"
流控是 Sentinel 最基础也最常用的能力。生产里不要一上来就全局 QPS 限流,而是按资源粒度配规则。
下面是一个 Spring Boot 3.2 项目的完整最小可用示例。
3.1 Maven 依赖
java
<!-- Spring Boot 3.2.x 请使用 2023.0.1.0 版本 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2023.0.1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
</dependencies>
3.2 启动时加载流控规则
java
@Configuration
public class SentinelFlowConfig {
@PostConstruct
public void initRules() {
List<FlowRule> rules = new ArrayList<>();
// 资源名建议用 "类名:方法名" 或业务语义名,和 @SentinelResource 保持一致
FlowRule seckillRule = new FlowRule("seckill:submit");
seckillRule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 按 QPS 限流
seckillRule.setCount(1000); // 阈值 1000 QPS
seckillRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
seckillRule.setWarmUpPeriodSec(10); // 预热 10 秒,防止冷启动瞬间打崩
seckillRule.setLimitApp("default");
rules.add(seckillRule);
// 关联限流:当查询接口流量过高时,限制下单接口
FlowRule queryRelatedRule = new FlowRule("order:create");
queryRelatedRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
queryRelatedRule.setCount(500);
queryRelatedRule.setStrategy(RuleConstant.STRATEGY_RELATE);
queryRelatedRule.setRefResource("order:query"); // 关联 order:query 资源
queryRelatedRule.setRefThreshold(2000); // order:query 超过 2000 时触发
rules.add(queryRelatedRule);
FlowRuleManager.loadRules(rules);
}
}
3.3 Controller 与统一异常处理
java
@RestController
@RequestMapping("/seckill")
public class SeckillController {
@GetMapping("/submit")
@SentinelResource(value = "seckill:submit",
blockHandler = "submitBlockHandler",
fallback = "submitFallback")
public String submit(@RequestParam Long skuId, @RequestParam Long userId) {
// 真正的秒杀下单逻辑
return "下单成功:sku=" + skuId;
}
// blockHandler:仅处理 Sentinel 限流/降级抛出的 BlockException
public String submitBlockHandler(Long skuId, Long userId, BlockException ex) {
return "系统繁忙,请稍后再试";
}
// fallback:处理业务异常(如库存不足、RPC 超时)
public String submitFallback(Long skuId, Long userId, Throwable ex) {
return "下单失败:" + ex.getMessage();
}
}
@RestControllerAdvice
public class SentinelGlobalExceptionHandler {
@ExceptionHandler(BlockException.class)
public ResponseEntity<String> handleBlockException(BlockException e) {
return ResponseEntity.status(429).body("请求过于频繁,请稍后重试");
}
}
老梁提醒:
-
blockHandler只处理 Sentinel 规则触发;fallback处理业务异常。两者参数签名必须一致,且最后一个是BlockException或Throwable。 -
预热模式对秒杀场景非常有用,避免流量瞬间打到最大值。
四、代码实战 2:熔断降级,让故障"快速失败"
限流是"防冲",熔断是"止损"。下游服务变慢或者疯狂报错时,继续调用只会把故障放大。
Sentinel 熔断有三种触发策略:
| 策略 | 含义 | 适用场景 |
|---|---|---|
| 慢调用比例 | 单位时间内慢调用占比超过阈值 | 下游 RT 抖动、GC 频繁 |
| 异常比例 | 异常调用占比超过阈值 | 下游偶发 500、网络抖动 |
| 异常数 | 异常调用次数超过阈值 | 下游明确故障 |
下面以保护 AI 模型调用接口为例,演示慢调用比例熔断。
java
@Configuration
public class SentinelDegradeConfig {
@PostConstruct
public void initDegradeRules() {
List<DegradeRule> rules = new ArrayList<>();
DegradeRule aiRule = new DegradeRule("ai:chat");
aiRule.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType()); // 慢调用比例
aiRule.setCount(0.5); // 慢调用比例阈值 50%
aiRule.setSlowRatioThreshold(800); // 超过 800ms 算慢调用
aiRule.setTimeWindow(10); // 熔断后 10 秒内快速失败
aiRule.setMinRequestAmount(10); // 最小统计请求数,避免刚启动就熔断
aiRule.setStatIntervalMs(1000); // 统计窗口 1 秒
rules.add(aiRule);
DegradeRuleManager.loadRules(rules);
}
}
@Service
public class AiChatService {
private final ChatClient chatClient;
public AiChatService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
@SentinelResource(value = "ai:chat",
blockHandler = "chatBlock",
fallback = "chatFallback")
public String ask(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
public String chatBlock(String question, BlockException ex) {
// 熔断或限流时,直接返回本地兜底话术
return "AI 服务繁忙,已切换至离线模式。常见问题请查看帮助文档。";
}
public String chatFallback(String question, Throwable ex) {
// 大模型调用本身的异常
return "AI 响应异常:" + ex.getMessage();
}
}
这里的关键是:熔断时间窗口结束后,Sentinel 会进入半开状态,放少量请求试探,成功后关闭熔断。 不要自己写死"休眠 10 秒再恢复",那在波动环境里很容易误恢复。
五、代码 3:热点参数限流,针对"某个用户/某个商品"精准打击
全局 QPS 限流有一个问题:羊毛党用 10 个账号并发,每个账号请求量都不高,但总量能压垮系统。热点参数限流就是来解决这种"局部热点"的。
假设我们要保护一个 AI 文档摘要接口,针对 docId 做限流,同时给 VIP 用户单独配置阈值。
java
@Configuration
public class SentinelParamFlowConfig {
@PostConstruct
public void initParamRules() {
List<ParamFlowRule> rules = new ArrayList<>();
ParamFlowRule docRule = new ParamFlowRule("ai:summary");
docRule.setParamIdx(0); // 第 0 个参数是 docId
docRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
docRule.setCount(10); // 默认每个 docId 10 QPS
// 对特定热点值单独限流:docId=10086 的文档被疯狂点击
List<ParamFlowItem> items = new ArrayList<>();
items.add(new ParamFlowItem("10086", 2, String.class.getName()));
docRule.setParamFlowItemList(items);
rules.add(docRule);
ParamFlowRuleManager.loadRules(rules);
}
}
@RestController
@RequestMapping("/ai")
public class AiSummaryController {
@GetMapping("/summary")
@SentinelResource(value = "ai:summary",
blockHandler = "summaryBlock")
public String summary(@RequestParam String docId,
@RequestParam Long userId) {
// 调用大模型生成摘要
return "这是文档 " + docId + " 的 AI 摘要......";
}
public String summaryBlock(String docId, Long userId, BlockException ex) {
// 热点限流触发,返回缓存摘要或友好提示
return "该文档当前访问火爆,摘要生成排队中,请 5 秒后重试";
}
}
热点参数限流在 AI 场景特别好用:
-
同一个问题被反复问,缓存没命中时可以限流,保护 Token 费用。
-
某个用户疯狂刷接口,按
userId限流,不影响其他用户。
六、系统自适应保护:最后一道闸门
前面都是针对单个资源做规则。但如果机器本身已经不行了------CPU 被打满、Load 飙高、线程数暴涨------再精细的规则也救不了。
java
Sentinel 的 SystemSlot 提供了系统级保护,根据当前系统负载动态控制入口总流量。
@Configuration
public class SentinelSystemConfig {
@PostConstruct
public void initSystemRules() {
List<SystemRule> rules = new ArrayList<>();
SystemRule rule = new SystemRule();
rule.setHighestSystemLoad(8.0); // Linux Load1 超过 8 限流
rule.setHighestCpuUsage(0.8); // CPU 超过 80% 限流
rule.setMaxThread(500); // 入口线程数超过 500 限流
rule.setAvgRt(200); // 平均 RT 超过 200ms 限流
rule.setQps(5000); // 入口总 QPS 超过 5000 限流
rules.add(rule);
SystemRuleManager.loadRules(rules);
}
}
系统保护规则一般作为兜底,不要和流控规则重复叠加太多,否则容易出现"双重限流"导致正常流量被误杀。
七、生产环境里的三条实战建议
1. 规则必须持久化,否则重启就丢
Sentinel 默认规则放在内存里,服务重启就没了。生产里一定要接 Nacos、Apollo 或者 Redis 数据源。
java
spring:
cloud:
sentinel:
datasource:
flow:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-flow-rules
groupId: SENTINEL_GROUP
rule-type: flow
degrade:
nacos:
server-addr: localhost:8848
dataId: ${spring.application.name}-degrade-rules
groupId: SENTINEL_GROUP
rule-type: degrade
2. 限流提示要做业务语义化
不要直接抛 429 Too Many Requests 给前端。用户看不懂,产品也骂你。统一封装成:
java
{
"code": "RATE_LIMITED",
"message": "当前排队人数较多,预计等待 8 秒",
"retryAfter": 8
}
3. 先埋点,再配规则,阈值是压测出来的
很多团队一上来就凭感觉写 count=100。正确的做法是:
-
先用 Sentinel 控制台或者 Micrometer 暴露接口 QPS/RT 基线;
-
用 JMeter 压测找到拐点;
-
把阈值设在拐点 70%~80% 的位置,留出缓冲。
八、结尾:把"防御"做成系统能力
流量治理不是加几行代码的事,而是一种系统能力。
限流解决"谁该进",熔断解决"坏了怎么办",降级解决"还能给什么",系统保护解决"机器不行了怎么保命"。 四层叠加,才是真正能扛住生产风暴的架构。
下一篇 Day 46,我们聊一聊 Spring Cloud Gateway 网关实战:路由、过滤器、限流全攻略,看看怎么在网关层就把坏人拦在外面。