1. 先分清:fallback 和 blockHandler 是两套完全不同的兜底机制
最开始接触 Sentinel 的时候,最容易让人上头的一对概念就是@SentinelResource注解里的fallback和blockHandler。网上很多文章把两个词并列着写,示例代码里也是你中有我、我中有你,导致不少同学误以为它们功能类似,随便配一个就行。我自己第一次做压测就吃过这个亏:接口明明配置了fallback,限流一触发,调用方却收到了一个冷冰冰的FlowException,当时还以为是规则配置错了,排查了半天才发现问题出在兜底方法选错了。
先说结论,fallback和blockHandler是两套互相独立的兜底机制,触发场景完全不一样。fallback管的是业务方法自身抛出来的异常,比如参数校验失败、下游调用超时、空指针这些;blockHandler管的是 Sentinel 规则被触发时抛出的BlockException,比如 QPS 超过阈值、被熔断降级、被系统保护拦截。换句话说,业务代码“正常进场”但“现场翻车”时,走的是fallback;业务代码“根本进不了场”时,走的是blockHandler。不理解这个前提,后面所有配置都可能白做。
为什么这对概念经常被搞混?主要原因是它们语法上长得太像:都是给一个方法做兜底,方法签名都是“原方法参数列表 + 一个异常参数”,返回值都要求与原方法一致。再加上有些项目为了省事,会把两个方法写在同一个Handler类里,不仔细看根本分不清谁是谁。后面我会把注解的每个属性、两个方法的具体签名、执行优先级、以及一个完整的订单接口示例拆开讲清楚,最后还会给出基于 Nacos 的动态限流配置样例,方便你直接抄作业。
2. @SentinelResource 注解全属性拆解:每个字段到底管什么
2.1 注解的声明式本质
@SentinelResource做的事情是“把某个方法声明为一个 Sentinel 资源”。它本身不创建任何限流规则,也不负责加载规则。真正的规则是通过FlowRuleManager、DegradeRuleManager等静态 API 加载,或者通过配置中心动态推送的。注解的作用,是让 Sentinel 在方法执行前后插入一段切面逻辑:进入方法前尝试申请资源,如果规则不允许进入,立即抛出对应的BlockException;如果允许进入,就执行业务方法,方法结束后上报本次调用的 QPS、RT、异常数等指标。
因此,看一个@SentinelResource是否生效,至少要确认三件事:第一,注解确实被切面拦截到了(Spring 环境下要求方法走代理对象);第二,规则里配置的resource名称与注解的value完全一致;第三,兜底方法的签名符合要求。很多人说“我配了注解但没生效”,九成是这三件事里至少有一件没做到。
2.2 value 与 entryType
value是资源名,没有默认值,必须手动指定。这个名称是后续规则关联的钥匙,规则配置里的resource字段和它精确匹配。这里建议用稳定的业务标识,比如order:create、pay:callback,而不是直接拼方法名。原因很简单:规则放在 Nacos 里,如果资源名跟着代码重构一起改,配置中心的规则很容易失联。
entryType用于标注资源的流量类型,取值是EntryType.IN或EntryType.OUT,默认IN。绝大多数场景都不需要动它,但如果你需要对 HttpClient、OpenFeign 这类出站调用做保护,可以把对应的@SentinelResource的entryType改为OUT,这样 Sentinel 在统计系统自适应限流等逻辑时会区分出入口流量,避免把内部调用当外部流量误伤。
2.3 兜底方法相关的五个属性
用表格梳理一下注解里的关键属性,对照着看会更清楚:
| 属性 | 作用 | 使用要点 |
|---|---|---|
value | 资源名,规则匹配的唯一标识 | 必填,建议使用稳定的业务语义字符串 |
entryType | 入口/出口流量类型 | 默认IN,出站调用可以配OUT |
blockHandler | 处理被 Sentinel 拦截时的逻辑 | 对应BlockException及子类 |
blockHandlerClass | blockHandler所在类 | 配置后要求方法必须为public static |
fallback | 处理业务方法抛出的异常 | 对应普通Throwable,不处理BlockException |
fallbackClass | fallback所在类 | 配置后要求方法必须为public static |
defaultFallback | 默认兜底,通用异常处理 | 优先级低于fallback,签名更灵活 |
exceptionsToIgnore | 忽略指定的异常类型 | 忽略后的异常不会被fallback接管 |
这里有一个容易忽略的细节:@SentinelResource没有提供defaultBlockHandler之类的属性。如果很多资源都需要被限流后统一提示“系统繁忙”,你只能给每个核心资源单独配置blockHandler,或者统一在全局异常处理器里捕获BlockException。我自己在项目里的做法是:非核心资源不做注解兜底,让BlockException直接向上抛,由外层ControllerAdvice统一转成友好提示;核心资源才单独配blockHandler,这样职责清晰,也不需要在每个方法上重复写一堆静态方法。
2.4 一个容易忽略的 exceptionsToIgnore
exceptionsToIgnore这个属性很多人没用过,但场景其实很实用。默认情况下,只要方法抛出异常,fallback就会接管。但有些异常你是不希望被吞掉的,比如参数校验异常,你更希望它直接抛给全局异常处理器,让接口返回 400 和明确的提示信息,而不是走业务兜底返回一个统一错误码。此时把IllegalArgumentException.class配置到exceptionsToIgnore,这个异常就会原样继续向上抛,不再进入fallback。
注意一下优先级:exceptionsToIgnore优先于fallback。如果同一个异常既在忽略列表里,又写了匹配的fallback,最终结果还是忽略,直接抛异常。这个优先级意味着你在设计异常兜底时,要先想清楚哪些异常是“可恢复的兜底场景”,哪些是“必须亮出真实错误给上层处理”的。
3. 两者的核心区别:触发条件、函数签名和优先级
3.1 触发条件对比:业务异常 vs 规则拦截
一个被@SentinelResource保护的方法,执行过程大致是:切面先检查 Sentinel 规则,如果流量通过,才真正调用业务方法;如果流量被拦截,业务方法根本不会执行,直接抛BlockException。所以这两个兜底机制的触发点在时间线上是明确分开的:
blockHandler:在业务方法执行之前触发。只要 Sentinel 判定当前请求需要被限流、降级、限热点参数、拒绝授权等,就会抛出BlockException的子类,此时由blockHandler接管。fallback:在业务方法执行过程中触发。业务代码抛出的任何非BlockException异常,都会被fallback接管。
有一个非常关键的结论:fallback不能处理BlockException。哪怕你只配了fallback,没配blockHandler,限流时也不会进入fallback,而是直接把BlockException抛给调用方。很多新手在这里踩坑,以为fallback是万能兜底,结果线上限流触发后看到一堆FlowException。记住,这两个兜底的分工是:blockHandler管“进不来”,fallback管“进来后挂了”。
3.2 函数签名要求对比
签名要求是另一个极易踩坑的点。fallback和blockHandler的返回值都必须与原方法一致,或至少能兼容转换;参数列表也必须和原方法保持“前缀一致”。
具体拆解一下:
fallback方法:参数列表 = 原方法全部参数 + 最后的Throwable参数。比如原方法Order createOrder(OrderRequest request),fallback 方法必须是Order createOrderFallback(OrderRequest request, Throwable t)。Throwable不能省略,这一点和blockHandler不一样。blockHandler方法:参数列表有两种写法。第一种是只保留原方法参数,不追加参数;第二种是原方法参数 + 最后的BlockException参数。例如同一个订单方法,可以写Order createOrderBlockHandler(OrderRequest request),也可以写Order createOrderBlockHandler(OrderRequest request, BlockException ex)。官方推荐带上BlockException,因为你需要知道具体是哪种拦截,流控、降级、还是热点参数限制。- 如果原方法有多个参数,比如
User getUser(String name, int age),那么fallback必须是User getUserFallback(String name, int age, Throwable t),blockHandler必须是User getUserBlockHandler(String name, int age, BlockException ex),顺序不能乱。
我在实际项目里统一要求:兜底方法的参数顺序必须严格照抄,且异常参数放在最后,不要自行调整顺序。否则 Sentinel 在反射查找方法时找不到匹配签名,会直接报“no such method”或者干脆忽略你配置的兜底方法。
3.3 优先级与默认兜底方法
当多个兜底属性同时存在时,Sentinel 的处理优先级是有一套明确顺序的:
exceptionsToIgnore配置的异常类型:最高优先级,直接忽略,不进入任何兜底。blockHandler:仅处理BlockException,优先级高于所有普通异常兜底。fallback:处理普通业务异常,优先级高于defaultFallback。defaultFallback:当没有匹配到具体的fallback,或者你希望统一处理某些异常时使用。
很多人会问“同时配置了 fallback 和 blockHandler,到底走哪个”,答案是看触发原因。被限制流时毫不犹豫走blockHandler,因为业务方法都没执行,根本轮不到fallback;业务方法内部抛出普通异常时,blockHandler也不会参与,因为这不是BlockException。两者不是二选一的关系,而是两条平行赛道。
defaultFallback的签名更灵活,它可以不带任何参数,也可以带一个Throwable参数,但不能带原方法参数。也就是说,如果原方法有一堆入参,你可以在兜底方法里不去管它们,只接收Throwable。这个特性很适合做统一异常兜底,但要注意优先级比具体fallback低,别期望它能覆盖具体fallback的逻辑。
3.4 核心区别汇总表
| 对比维度 | fallback | blockHandler |
|---|---|---|
| 触发原因 | 业务方法执行中抛出普通异常 | Sentinel 规则拦截,抛出BlockException |
| 业务方法是否执行 | 已经执行了一部分 | 完全没有执行 |
| 异常参数 | Throwable,必须有 | 可选的BlockException |
| 能否处理限流/降级 | 不能 | 能 |
| 建议位置 | 当前类实例方法或独立类静态方法 | 建议独立类静态方法 |
| 典型日志 | ERROR 级别,记录业务失败原因 | WARN 级别,记录被限流/降级 |
这张表我建议截图保存。每次配置@SentinelResource前先对着表问一句:我当前要兜底的是哪种异常?答案直接决定你应该写fallback还是blockHandler。
4. 实战:一个订单接口同时配置 fallback 和 blockHandler
4.1 业务场景和规则规划
假设你在做一个订单服务,核心接口是创建订单,资源名定为order:create。业务上有三个需求:
- 当请求参数不合法时,返回业务失败提示,而不是直接抛 500;
- 当接口 QPS 超过 5 时,返回“系统繁忙,请稍后重试”;
- 两条兜底逻辑需要分别记录不同级别的日志,方便后续排查。
对应到 Sentinel 上,第一个需求用fallback处理业务异常,第二个需求用blockHandler处理FlowException。我把两个兜底方法都放到一个独立的OrderDegradeHandler类里,声明为public static,理由后面会讲。
4.2 完整代码实现
先定义一个简单的订单请求对象和统一返回结构:
public class OrderRequest { private Long userId; private String skuId; // getter/setter 省略 } public class Order { private String orderId; private String status; // getter/setter 省略 } public class R<T> { private int code; private String msg; private T data; public static <T> R<T> success(T data) { R<T> r = new R<>(); r.code = 200; r.msg = "success"; r.data = data; return r; } public static <T> R<T> error(int code, String msg) { R<T> r = new R<>(); r.code = code; r.msg = msg; return r; } }然后是核心业务方法:
@Slf4j @Service public class OrderService { @SentinelResource( value = "order:create", blockHandler = "createOrderBlockHandler", blockHandlerClass = OrderDegradeHandler.class, fallback = "createOrderFallback", fallbackClass = OrderDegradeHandler.class ) public R<Order> createOrder(OrderRequest request) { if (request == null || request.getUserId() == null) { throw new IllegalArgumentException("userId不能为空"); } if (request.getSkuId() == null) { throw new RuntimeException("skuId不能为空"); } // 模拟正常业务处理 Order order = new Order(); order.setOrderId("ORD" + System.currentTimeMillis()); order.setStatus("CREATED"); return R.success(order); } }再写独立的兜底类:
@Slf4j public class OrderDegradeHandler { public static R<Order> createOrderBlockHandler(OrderRequest request, BlockException ex) { log.warn("[blockHandler] 订单接口被拦截,资源=order:create,类型={}", ex.getClass().getSimpleName()); return R.error(429, "系统繁忙,请稍后重试"); } public static R<Order> createOrderFallback(OrderRequest request, Throwable t) { log.error("[fallback] 订单接口业务异常", t); return R.error(500, "业务处理失败:" + t.getMessage()); } }这里有几个细节需要强调。第一,两个兜底方法都放在OrderDegradeHandler里,并配置了blockHandlerClass和fallbackClass,所以方法必须是public static,否则 Sentinel 在反射调用时会找不到可实例化的目标。第二,第一个参数OrderRequest request与原方法完全一致,最后分别追加BlockException和Throwable,这是硬性要求。第三,返回值类型都写成R<Order>,和原方法保持一致。
4.3 验证过程:分别制造异常和触发限流
代码写完不能直接上生产,先做一次本地验证。启动应用后用 Swagger 或者 curl 访问测试:
第一步,验证fallback。发送一个缺少userId的请求:
POST /order/create Content-Type: application/json { "skuId": "ABC123" }此时业务方法会在第一个判断处抛出IllegalArgumentException,Sentinel 的切面捕获到这个异常后,不会原样往上抛,而是进入createOrderFallback。接口返回:
{ "code": 500, "msg": "业务处理失败:userId不能为空" }同时本地日志会打印[fallback] 订单接口业务异常,说明走的是fallback分支。
第二步,验证blockHandler。先把 Nacos 里的限流规则调成一个很小的阈值,比如 QPS 为 1,然后快速并发调用同一个接口。这里要注意,限流规则需要提前配置到当前命名空间,资源名必须是order:create,线程数或者 QPS 阈值按 1 设置。并发两个请求后,第二个请求会被 Sentinel 拦截,业务方法根本没有执行,直接进入createOrderBlockHandler。该请求返回:
{ "code": 429, "msg": "系统繁忙,请稍后重试" }日志里打印的是[blockHandler] 订单接口被拦截,和fallback的日志完全不同。
4.4 验证结论
通过这个案例可以直观看到:同一个资源、同一个注解,同时配置两个兜底方法后,触发条件互不干扰。异常和限流是两条独立路径,谁触发谁接管,不存在“fallback 优先级更高所以 blockHandler 白配”的情况。实际业务中你甚至可以在blockHandler里做限流告警统计,在fallback里做业务异常日志上报,两个方法各司其职。
5. 结合 Nacos 实现限流规则的动态配置
5.1 为什么需要动态规则
上面的验证过程里,我用的是“先把规则配好再启动”的方式。但生产环境不可能每次都改代码重启来调整阈值,否则上线一个限流配置还要走发布流程,效率太低。正常情况下,我们会把 Sentinel 规则放到 Nacos 配置中心,通过控制台改配置,客户端自动监听并刷新规则,整个过程不用重启服务。
动态规则的好处不仅仅是省一次发布。压测时发现 QPS 阈值需要从 100 调到 50,直接在 Nacos 页面改一个数字就能生效;线上发生突发流量,也可以在几十秒内把阈值降下来。相比本地FlowRuleManager.loadRules(),这种方式更适合真实运维。
5.2 引入依赖
如果你用的是 Spring Cloud Alibaba,集成 Nacos 动态规则很简单,只需在pom.xml中增加两个依赖:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>sentinel-datasource-nacos是 Sentinel 官方提供的 Nacos 数据源扩展包,它负责监听 Nacos 配置变更,并把配置内容反序列化成对应的规则对象。注意,和普通的 Nacos 配置中心依赖不同,这里必须引入专门的数据源扩展,否则 Sentinel 不知道去哪里拉规则。
5.3 通过 Nacos 配置限流规则
登录 Nacos 控制台,新建一个配置:
Data ID:order-flow-rulesGroup:SENTINEL_GROUP- 配置格式:
JSON
配置内容如下:
[ { "resource": "order:create", "grade": 1, "count": 5, "limitApp": "default", "strategy": 0, "controlBehavior": 0, "clusterMode": false }, { "resource": "order:create", "grade": 0, "count": 10, "limitApp": "default", "strategy": 0, "controlBehavior": 2, "clusterMode": false } ]这里解释一下字段含义。grade是规则类型,0表示按线程数限制,1表示按 QPS 限制;count是阈值;strategy是流控策略,0直接、1关联、2链路;controlBehavior是流控效果,0快速失败、1预热、2排队等待。上面这个配置同时做了 QPS 为 5 的快速失败限制,以及线程数为 10 的排队等待限制,生产环境可以根据业务特性选一种即可。
如果你还需要降级规则,可以在另一个配置文件里配:
[ { "resource": "order:create", "grade": 0, "count": 0.5, "timeWindow": 10, "minRequestAmount": 5 } ]grade为0表示按异常比例降级,count为0.5表示 50% 的请求异常时触发,timeWindow是熔断时间窗口,单位秒。降级规则的数据源在 Spring 配置里要用rule-type=degrade。
5.4 Spring Boot 配置和验证
数据源配置写在application.yml里:
spring: application: name: order-service cloud: sentinel: transport: dashboard: 127.0.0.1:8080 datasource: ds-order-flow: nacos: server-addr: 127.0.0.1:8848 dataId: order-flow-rules groupId: SENTINEL_GROUP rule-type: flow ds-order-degrade: nacos: server-addr: 127.0.0.1:8848 dataId: order-degrade-rules groupId: SENTINEL_GROUP rule-type: degraderule-type是必填项,它告诉 Sentinel 这段 Nacos 配置对应哪类规则,常见取值有flow、degrade、param-flow、system、authority。配置好之后,启动应用,在 Nacos 页面修改order-flow-rules里的count值,保存后无需重启服务,规则很快会生效。验证方法也很简单:把阈值改成 1,然后连续访问接口,第二个请求就会触发blockHandler。
如果在非 Spring Cloud 环境,也可以手动注册数据源:
ReadableDataSource<String, List<FlowRule>> flowRuleDataSource = new NacosDataSource<>("127.0.0.1:8848", "SENTINEL_GROUP", "order-flow-rules", source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>() {})); FlowRuleManager.register2Property(flowRuleDataSource.getProperty());这种写法和 Spring Cloud 配置等价,核心都是把 Nacos 中配置的 JSON 转换成FlowRule列表,再注册到FlowRuleManager。我建议直接使用 Spring Cloud 配置方式,代码更少,也避免自己处理序列化问题。
5.5 动态规则推送的坑
Nacos 方案用起来挺爽,但有几个坑必须提前知道。第一,namespace不配置时默认是public,如果应用配置了自定义 namespace,一定要在数据源配置里同步指定,否则客户端监听的是 public 下的配置,你改了自定义 namespace 下的规则自然不生效。第二,Nacos 推送的规则会覆盖本地通过FlowRuleManager.loadRules()加载的规则,如果本地和服务端都配置了,会产生“你以为的规则不是实际规则”的问题。第三,JSON 格式解析失败时 Sentinel 通常不会直接报错,而是规则不加载,排查时要去看 Nacos 客户端的日志,确认有没有反序列化异常。
6. 常见问题排查与我的避坑经验
6.1 为什么 blockHandler 方法必须 static 以及怎么绕
前面提到,只要配置了blockHandlerClass或fallbackClass,对应方法必须声明为public static。原因是 Sentinel 的切面在反射调用兜底方法时,需要凭空实例化或者调用一个不依赖 Spring 容器的对象;静态方法可以直接通过类名调用,不需要构造对象,这对一个框架级组件来说是最稳妥的实现方式。
但这也带来一个问题:静态方法里不能直接访问 Spring Bean。如果你在blockHandler里想调用某个AlertService发送告警,你会发现@Resource注入不进去。我的做法是提供一个静态的SpringContextHolder,在兜底方法里手动从 Spring 容器中取 Bean:
public class SpringContextHolder implements ApplicationContextAware { private static ApplicationContext context; @Override public void setApplicationContext(ApplicationContext applicationContext) { context = applicationContext; } public static <T> T getBean(Class<T> clazz) { return context.getBean(clazz); } }然后在blockHandler里这样用:
public static R<Order> createOrderBlockHandler(OrderRequest request, BlockException ex) { SpringContextHolder.getBean(AlertService.class).sendAlert(ex.getClass().getSimpleName()); return R.error(429, "系统繁忙,请稍后重试"); }当然,如果你不想用静态方法,也可以不配置blockHandlerClass,把兜底方法直接写在原类中,用普通实例方法访问 Bean。但这种方式在 Sentinel 反射处理时偶尔会遇到方法查找不到的问题,不同版本表现还不一样。我个人为了减少不确定性,统一走独立静态类方案。
6.2 fallback/blockHandler 同时存在时到底走谁
直觉上有人会以为两个方法都配置了,优先级高的那个会“抢占”另一个。实际情况是,二者按触发原因分流,互不抢占。可以做一个极端实验:在业务方法第一行就抛异常,同时把 QPS 阈值设为 0。QPS 为 0 意味着任何请求都会被 Sentinel 拦截,业务方法根本不会执行,所以你看到的只会是blockHandler的日志,而不是fallback。反过来,把 QPS 阈值调大,让请求正常进入业务方法并抛出普通异常,走的一定是fallback。这个实验能很好帮助团队理解两条兜底路径的独立性。
6.3 规则不生效的 5 个原因
我排查了无数个“规则不生效”的问题,总结下来原因基本集中在下面五类:
- 资源名不一致。注解
value是order:create,规则里的resource写成了order_create,或者带上了方法前缀,根本不匹配。 - 自调用导致切面失效。在同类里使用
this.createOrder(request),绕过了 Spring 代理对象,@SentinelResource完全没机会执行。解决方法是注入自己,或者将调用拆分到另一个 Service。 - 私有方法无法被拦截。
@SentinelResource加在private方法上没有意义,AOP 无法拦截私有方法,必须改成public,且通过代理对象调用。 - Nacos 配置没推下来。检查
rule-type是否正确、dataId是否存在、应用是否引入了sentinel-datasource-nacos。 - 本地规则覆盖了远程规则。如果你在代码里调用了
FlowRuleManager.loadRules(),后面 Nacos 再推送时可能会出现互相覆盖的问题,尽量避免本地和服务端同时配置相同的规则。
你可以把这些问题整理成一份排查清单,每次遇到“限流不进 blockHandler”时按顺序过一遍,基本能定位九成问题。
6.4 日志中如何判断是被限流还是业务异常
Sentinel 抛出的BlockException有好几个子类,常见的包括FlowException(流控)、DegradeException(降级)、ParamFlowException(热点参数限流)、AuthorityException(授权规则)、SystemBlockException(系统保护)。如果blockHandler里打印了ex.getClass(),通过异常类名就能精确知道是哪种规则拦截的。
如果你没有配置blockHandler,线上日志里看到FlowException堆栈,不要误以为是代码 BUG。它是 Sentinel 在资源入口处主动抛出的,代表当前请求被流量规则挡住了。相反,如果看到的是NullPointerException、IllegalArgumentException这类业务异常,那才是fallback应该处理的场景。建议在日志里把兜底方法名打印出来,通过方法名直接区分blockHandler和fallback走了哪条路。
6.5 我沉淀下来的最佳实践
经历了多次线上故障后,我总结了几个使用@SentinelResource的固定规则,分享给你参考。
第一,核心接口必须同时配置blockHandler和fallback,不要嫌麻烦。blockHandler负责限流提示和告警,fallback负责业务异常兜底和日志记录,配置完整了才不容易出线上事故。第二,兜底方法集中在独立的Handler类中,统一命名规范,比如xxxBlockHandler、xxxFallback,避免散落在业务类里造成维护困难。第三,返回值尽量使用统一包装对象,这样兜底方法可以直接返回统一的错误码和提示语,不需要在业务类里重复定义返回结构。第四,所有规则通过 Nacos 下发,禁止在代码里硬编码阈值。规则变化是常态,动态调整才是正确的运维方式。
最后再说一个小技巧:在defaultFallback里只处理通用异常,具体业务异常还是交给各自的fallback方法。因为defaultFallback没有原方法参数,无法针对某个业务字段做精细化处理,如果用得太宽泛,容易掩盖问题的真实原因。我见过不少项目把defaultFallback当成万能兜底,结果线上所有异常都被统一转成了“系统繁忙”,排查问题反而变得更困难。兜底方法不是为了吞异常,而是为了在异常发生后给调用方一个合理的响应,同时把真实原因记录到日志里。理解了这一点,你在配置fallback和blockHandler时,思路就会清晰很多。