![图片[1]-Sentinel 扩展实战:自定义 ProcessorSlot 实现开关降级功能 - 速优课-速优课](https://www.suyouke.com/wp-content/uploads/2026/08/cover-2108.png)
本文导读
在电商项目中,开关降级是一个非常实用的功能。尤其在大促活动、每日流量高峰、主播带货等场景下,我们经常需要临时关闭一些非核心功能,降低数据库和服务的压力,保障核心业务的稳定运行。
开关降级的实现方式有很多种,比如:
- 使用 Spring AOP + 注解实现
- 使用配置中心管理开关状态
- 使用 Redis 存储开关状态
本文将从实际业务场景出发,介绍两种开关降级的实现方式:
- Spring AOP 方式:简单直接,通过注解快速实现
- 自定义 ProcessorSlot 方式:融入 Sentinel 体系,与限流熔断协同工作
通过本文的学习,你不仅能掌握开关降级的实现方法,还能深入理解 Sentinel 的 SPI 扩展机制,学会如何自定义 Slot 来扩展 Sentinel 的功能。
一、为什么需要开关降级
1.1 什么是开关降级
开关降级,顾名思义,就是通过一个”开关”来控制某些功能是否可用。当开关打开时,对应的功能被降级(直接拒绝或返回默认值);当开关关闭时,功能正常使用。
在电商项目中,开关降级是每个微服务几乎都需要支持的功能。典型的应用场景包括:
| 场景 | 目的 | 被降级的功能举例 |
| 大促活动期间 | 保障核心交易链路 | 个性化推荐、用户积分查询、评价列表 |
| 流量高峰时段 | 降低数据库压力 | 非核心查询接口、日志上报接口 |
| 主播带货场景 | 防止瞬间流量冲垮系统 | 商品详情页的非核心信息、推荐商品 |
| 服务出问题时 | 快速止损 | 依赖下游不稳定服务的接口 |
1.2 开关降级 vs 熔断降级
开关降级和熔断降级虽然都是降级手段,但有本质区别:
| 对比维度 | 开关降级 | 熔断降级 |
| 触发方式 | 人工/自动配置 | 自动触发(基于指标) |
| 触发条件 | 手动开启或定时开启 | 异常率、响应时间等指标达标 |
| 恢复方式 | 手动关闭 | 时间窗口后自动恢复 |
| 粒度 | 较粗(一组接口) | 较细(单个资源) |
| 适用场景 | 预知的流量高峰、活动期间 | 突发的服务故障、下游不稳定 |
简单来说:
- 熔断降级是”被动防御”,出了问题才触发
- 开关降级是”主动出击”,在问题发生前就主动降级
两者结合使用,才能构建更完善的服务保护体系。
二、方式一:使用 Spring AOP 实现开关降级
2.1 实现思路
Spring AOP 是实现开关降级最简单直接的方式。以 Redis 缓存开关为例,整体思路如下:
- 定义一个开关降级注解
@SwitchDegrade - 实现切面
SwitchDegradeAspect,拦截带注解的方法 - 在切面中从 Redis 读取开关状态,决定是否降级
- 降级时抛出异常,由全局异常处理器统一处理
整个过程只需要三步,非常简单。
2.2 第一步:定义开关降级注解
首先定义一个注解 @SwitchDegrade,用于标记需要被开关控制的方法:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface SwitchDegrade {
// 缓存 key
String key() default "";
}
实践建议:如果是在实际项目中使用,建议为注解添加一个前缀属性(比如 appName),限制同一个应用下的开关 key 都有统一前缀,避免多个应用之间的缓存 key 冲突。
2.3 第二步:实现开关降级切面
接下来实现切面 SwitchDegradeAspect,拦截目标方法的执行:
@Aspect
public class SwitchDegradeAspect {
// 定义切点:所有带 @SwitchDegrade 注解的方法
@Pointcut("@annotation(com.wujiuye.demo.common.sentinel.SwitchDegrade)")
public void degradePointCut() {
}
/
* 环绕通知:拦截请求,判断是否开启开关降级
*/
@Around("degradePointCut() && @annotation(switchDegrade)")
public Object around(ProceedingJoinPoint point, SwitchDegrade switchDegrade) throws Throwable {
String cacheKey = switchDegrade.key();
RedisTemplate redisTemplate = SpringContextUtils.getBean(RedisTemplate.class);
String value = redisTemplate.get(cacheKey);
// 开关打开则抛出降级异常
if ("true".equalsIgnoreCase(value)) {
throw new SwitchDegradeException(cacheKey, "开关降级打开");
}
// 开关关闭则正常执行
return point.proceed();
}
}
代码解读:
@Pointcut定义切点,匹配所有带@SwitchDegrade注解的方法@Around环绕通知,在方法执行前后做处理- 从注解中获取开关的 key,去 Redis 查询当前状态
- 如果开关为 true(打开),抛出
SwitchDegradeException异常 - 如果开关为 false(关闭),调用
proceed()正常执行方法
为什么选择抛出异常而不是直接返回?
因为并不是每个接口方法都有返回值,而且返回值类型也不固定。抛出异常由全局异常处理器统一处理,更加灵活。
2.4 第三步:全局异常处理器
在全局异常处理器中处理开关降级异常,返回统一的降级响应:
@ExceptionHandler(SwitchDegradeException.class)
public BaseResponse handleSwitchDegradeException(SwitchDegradeException ex) {
log.error("Switch Degrade! switch key is:{}, message:{}", ex.getSwitchKey(), ex.getMessage());
return new BaseResponse(ResultCode.SERVICE_DEGRADE, ex.getMessage());
}
提示:如果整合了 OpenFeign 且配置了 Fallback,也可以不配置全局异常,直接由 Feign 的 Fallback 处理。
2.5 使用方式
在需要被开关控制的接口方法上添加 @SwitchDegrade 注解即可:
@RestController
@RequestMapping("/v1/test")
public class DemoController {
@SwitchDegrade(key = "auth:switch")
@PostMapping("/demo")
public GenericResponse<Void> demo(@RequestBody Invocation<DemoFrom> invocation) {
// 业务逻辑...
}
}
2.6 AOP 方式的优缺点
优点:
- 实现简单,上手快
- 不依赖 Sentinel,可独立使用
- 注解方式使用方便
缺点:
- 必须在方法上添加注解,配置不够灵活
- 与 Sentinel 的其他降级功能割裂,无法统一管理
- 每个请求都读 Redis,有一定性能开销(可通过本地缓存优化)
三、方式二:基于 Sentinel 自定义 ProcessorSlot 实现
3.1 为什么选择 Sentinel 扩展
Sentinel 将降级功能抽象为处理器插槽(ProcessorSlot),由一个个 Slot 提供丰富的降级功能,并且通过 SPI 机制支持扩展。这相当于给我们提供了”插件化”的能力。
基于 Sentinel 实现开关降级有以下优势:
- 配置化管理:和限流、熔断一样,全部通过规则配置,不需要加注解
- 统一管理:和其他降级规则统一管理,便于监控和运维
- SPI 扩展:利用 Sentinel 的扩展机制,侵入性低
- 灵活的资源匹配:支持 include/exclude 等灵活的资源匹配方式
3.2 整体设计思路
参照 Sentinel 现有 Slot 的设计模式,我们的实现也遵循相同的套路:
| 组件 | 职责 | 命名规范 |
| 规则类 | 定义规则配置项 | XxxRule |
| 规则管理器 | 提供加载/获取规则的 API | XxxRuleManager |
| 规则检查器 | 实现具体的判断逻辑 | XxxRuleChecker |
| 自定义 Slot | 作为功能切入点,调用检查器 | XxxSlot |
| SlotChainBuilder | 将自定义 Slot 加入插槽链 | 自定义 |
| 异常类 | 降级时抛出的异常 | XxxException(继承 BlockException) |
具体到开关降级,就是:
SwitchRule→ 开关降级规则SwitchRuleManager→ 开关规则管理器SwitchRuleChecker→ 开关规则检查器SwitchSlot→ 开关降级切入点SwitchException→ 开关降级异常
3.3 第一步:定义开关降级规则
首先定义开关降级规则类 SwitchRule。
与熔断降级、限流降级不同,一个开关通常会控制多个接口,而不只是一个。所以我们设计的规则支持灵活的资源配置:
@Data
@ToString
public class SwitchRule {
public static final String SWITCH_KEY_OPEN = "open";
public static final String SWITCH_KEY_CLOSE = "close";
// 开关状态:open(打开,降级)/ close(关闭,正常)
private String status = SWITCH_KEY_OPEN;
// 开关控制的资源
private Resources resources;
@Data
@ToString
public static class Resources {
// 包含:只控制这些资源
private Set<String> include;
// 排除:除了这些资源,其他都控制
private Set<String> exclude;
}
}
资源配置的三种模式:
| 配置方式 | 效果 |
| resources 不配置 | 开关作用于全部资源 |
| 配置 include | 只作用于 include 指定的资源 |
| 配置 exclude(不配置 include) | 除了 exclude 指定的资源,其他都受控制 |
这种设计非常灵活,可以满足各种场景需求。
3.4 第二步:实现规则管理器
参照 Sentinel 的命名规范,提供 loadRules API 的类命名为 XxxRuleManager。我们定义 SwitchRuleManager:
public class SwitchRuleManager {
private static volatile Set<SwitchRule> switchRuleSet = new HashSet<>();
/
* 加载或更新开关降级规则
*/
public static void loadSwitchRules(Set<SwitchRule> rules) {
SwitchRuleManager.switchRuleSet = rules;
}
/**
* 获取当前生效的所有开关降级规则
*/
static Set<SwitchRule> getRules() {
return switchRuleSet;
}
}
非常简单,两个方法:
loadSwitchRules:加载/更新规则getRules:获取当前规则
使用
volatile修饰规则集合,保证多线程下的可见性。
3.5 第三步:实现规则检查器
接下来实现 SwitchRuleChecker,负责具体的检查逻辑:
public class SwitchRuleChecker {
public static void checkSwitch(ResourceWrapper resource, Context context) throws SwitchException {
Set<SwitchRule> switchRuleSet = SwitchRuleManager.getRules();
// 遍历所有开关规则
for (SwitchRule rule : switchRuleSet) {
// 开关未打开,跳过
if (!rule.getStatus().equalsIgnoreCase(SwitchRule.SWITCH_KEY_OPEN)) {
continue;
}
if (rule.getResources() == null) {
continue;
}
// include 模式:包含在列表中则降级
if (!CollectionUtils.isEmpty(rule.getResources().getInclude())) {
if (rule.getResources().getInclude().contains(resource.getName())) {
throw new SwitchException(resource.getName(), "switch");
}
}
// exclude 模式:不包含在列表中则降级
if (!CollectionUtils.isEmpty(rule.getResources().getExclude())) {
if (!rule.getResources().getExclude().contains(resource.getName())) {
throw new SwitchException(resource.getName(), "switch");
}
}
}
}
}
逻辑说明:
- 从
SwitchRuleManager获取所有开关规则 - 遍历每条规则,只处理状态为 open 的规则
- include 模式:如果当前资源在 include 列表中,抛出
SwitchException - exclude 模式:如果当前资源不在 exclude 列表中,抛出
SwitchException - 一个资源可以被多个开关规则控制(只要有一个命中就降级)
注意:
SwitchException必须继承BlockException,否则抛出的异常会被资源指标统计收集,影响熔断降级等功能的准确性。
虽然使用了 for 循环遍历规则,但实际项目中开关数量通常很少(几个到几十个),性能完全没问题。
3.6 第四步:实现自定义 Slot
现在实现开关降级的切入点 SwitchSlot,继承 AbstractLinkedProcessorSlot:
public class SwitchSlot extends AbstractLinkedProcessorSlot<Object> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object param, int count,
boolean prioritized, Object... args) throws Throwable {
// 检查开关
SwitchRuleChecker.checkSwitch(resourceWrapper, context);
// 传递给下一个 Slot
fireEntry(context, resourceWrapper, param, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
// 传递给下一个 Slot
fireExit(context, resourceWrapper, count, args);
}
}
非常简洁:
entry方法中调用SwitchRuleChecker#checkSwitch检查开关- 检查通过后调用
fireEntry传递给下一个 Slot exit方法直接调用fireExit传递即可
SwitchSlot 放在 Slot 链的哪个位置?
因为 SwitchSlot 不依赖统计指标数据,所以放在哪个位置都不会影响 Sentinel 的正常工作。我们可以选择:
- 放在最前面:快速失败,减少后续 Slot 的判断开销
- 放在最后面:不影响其他功能的统计
这里我们选择放在链表末尾,作为兜底降级。
3.7 第五步:自定义 SlotChainBuilder
要让自定义的 SwitchSlot 生效,还需要自定义 SlotChainBuilder,将 SwitchSlot 添加到插槽链中。
最简单的方式是继承 DefaultSlotChainBuilder,在默认构建的链表基础上添加我们的 Slot:
public class MySlotChainBuilder extends DefaultSlotChainBuilder {
@Override
public ProcessorSlotChain build() {
ProcessorSlotChain chain = super.build();
// 在链表末尾添加自定义的 SwitchSlot
chain.addLast(new SwitchSlot());
return chain;
}
}
继承 DefaultSlotChainBuilder 的好处是:
- 复用默认的 Slot 构建逻辑
- 只需要关注添加自己的 Slot
- 代码简洁,不易出错
3.8 第六步:注册 SPI
光有 MySlotChainBuilder 还不够,需要通过 Java SPI 机制注册它。
在项目的 resources/META-INF/services/ 目录下创建一个文件,文件名是接口的全限定名:
com.alibaba.csp.sentinel.slotchain.SlotChainBuilder
文件内容是自定义实现类的全限定名,例如:
com.wujiuye.demo.common.sentinel.MySlotChainBuilder
这样,Sentinel 在启动时就会通过 SPI 发现我们的自定义 SlotChainBuilder,并使用它来构建 Slot 链。
验证方式:可以在
MySlotChainBuilder#build方法中加断点,启动项目后看是否会停下来。
3.9 第七步:配置规则测试
最后,我们写一个配置类,加载开关降级规则进行测试:
@Configuration
public class SentinelRuleConfiguration {
static {
Set<SwitchRule> rules = new HashSet<>();
SwitchRule rule = new SwitchRule();
rule.setStatus(SwitchRule.SWITCH_KEY_OPEN);
SwitchRule.Resources resources = new SwitchRule.Resources();
Set<String> include = new HashSet<>();
include.add("/v1/test/demo");
resources.setInclude(include);
rule.setResources(resources);
rules.add(rule);
SwitchRuleManager.loadSwitchRules(rules);
}
}
上面的代码配置了一个开关降级规则:
- 开关状态:打开(open)
- 控制方式:include 模式
- 控制资源:
/v1/test/demo
这样,当访问 /v1/test/demo 接口时,就会被开关降级。
注意:这种硬编码配置只适用于本地测试。实际项目中应该通过动态配置实现,比如结合 Nacos、Apollo 等配置中心。
四、两种实现方式对比
| 对比维度 | Spring AOP 方式 | Sentinel 自定义 Slot 方式 |
| 实现复杂度 | 简单 | 稍复杂 |
| 配置方式 | 注解 | 规则配置 |
| 灵活性 | 一般(逐个方法加注解) | 高(include/exclude/全部) |
| 与 Sentinel 集成 | 独立 | 深度融合 |
| 统一管理 | 不支持 | 支持(和其他规则一起) |
| 性能 | 每次读 Redis | 内存判断,性能更好 |
| 适用场景 | 少量接口、快速实现 | 大量接口、统一管控 |
选择建议:
- 如果只是少量接口需要开关控制,AOP 方式足够
- 如果已经在使用 Sentinel,建议用自定义 Slot 方式,统一管理
- 如果后续需要动态配置规则,自定义 Slot 方式更方便对接动态数据源
总结与思考
本文详细介绍了两种开关降级的实现方式:Spring AOP 方式和 Sentinel 自定义 ProcessorSlot 方式。
核心要点回顾
1. 开关降级的价值
- 主动降级,在流量高峰前提前保护系统
- 灵活控制非核心功能,保障核心业务
- 可与熔断降级形成互补
2. Spring AOP 方式
- 三步实现:定义注解 → 实现切面 → 全局异常处理
- 优点是简单直接,缺点是配置不够灵活
- 适合快速实现、少量接口的场景
3. Sentinel 自定义 Slot 方式
- 遵循 Sentinel 的设计模式:Rule → RuleManager → RuleChecker → Slot
- 通过 SPI 机制注册自定义 SlotChainBuilder
- 支持 include/exclude/全部三种资源匹配模式
- 与 Sentinel 其他功能深度融合,便于统一管理
4. 扩展思路
学会自定义 ProcessorSlot 后,你可以做的事情还有很多:
- 自定义参数校验 Slot
- 自定义日志埋点 Slot
- 自定义权限校验 Slot
- 自定义灰度发布 Slot
Sentinel 的 Slot 链机制为我们提供了强大的扩展能力,只要你有想法,几乎可以在请求的任意环节插入自定义逻辑。
实践建议
- 开关降级 + 动态配置:结合配置中心(Nacos/Apollo)实现开关的动态调整
- 开关分级管理:按重要程度分级,不同级别开关由不同人员管理
- 开关操作审计:记录开关的开启/关闭操作,便于追溯
- 定时开关:支持按时间自动开启/关闭开关(比如流量高峰时段自动开启)
- 开关演练:定期进行开关降级演练,确保功能可用
开关降级是一个看似简单但非常实用的功能,设计得当可以大大提升系统的可用性和容错能力。下一篇文章我们将介绍 Sentinel 的动态数据源,看看如何实现规则的动态配置,敬请期待。












请登录后查看评论内容