Sentinel 扩展实战:自定义 ProcessorSlot 实现开关降级功能

图片[1]-Sentinel 扩展实战:自定义 ProcessorSlot 实现开关降级功能 - 速优课-速优课

本文导读

在电商项目中,开关降级是一个非常实用的功能。尤其在大促活动、每日流量高峰、主播带货等场景下,我们经常需要临时关闭一些非核心功能,降低数据库和服务的压力,保障核心业务的稳定运行。

开关降级的实现方式有很多种,比如:

  • 使用 Spring AOP + 注解实现
  • 使用配置中心管理开关状态
  • 使用 Redis 存储开关状态

本文将从实际业务场景出发,介绍两种开关降级的实现方式:

  1. Spring AOP 方式:简单直接,通过注解快速实现
  2. 自定义 ProcessorSlot 方式:融入 Sentinel 体系,与限流熔断协同工作

通过本文的学习,你不仅能掌握开关降级的实现方法,还能深入理解 Sentinel 的 SPI 扩展机制,学会如何自定义 Slot 来扩展 Sentinel 的功能。


一、为什么需要开关降级

1.1 什么是开关降级

开关降级,顾名思义,就是通过一个”开关”来控制某些功能是否可用。当开关打开时,对应的功能被降级(直接拒绝或返回默认值);当开关关闭时,功能正常使用。

在电商项目中,开关降级是每个微服务几乎都需要支持的功能。典型的应用场景包括:

场景 目的 被降级的功能举例
大促活动期间 保障核心交易链路 个性化推荐、用户积分查询、评价列表
流量高峰时段 降低数据库压力 非核心查询接口、日志上报接口
主播带货场景 防止瞬间流量冲垮系统 商品详情页的非核心信息、推荐商品
服务出问题时 快速止损 依赖下游不稳定服务的接口

1.2 开关降级 vs 熔断降级

开关降级和熔断降级虽然都是降级手段,但有本质区别:

对比维度 开关降级 熔断降级
触发方式 人工/自动配置 自动触发(基于指标)
触发条件 手动开启或定时开启 异常率、响应时间等指标达标
恢复方式 手动关闭 时间窗口后自动恢复
粒度 较粗(一组接口) 较细(单个资源)
适用场景 预知的流量高峰、活动期间 突发的服务故障、下游不稳定

简单来说:

  • 熔断降级是”被动防御”,出了问题才触发
  • 开关降级是”主动出击”,在问题发生前就主动降级

两者结合使用,才能构建更完善的服务保护体系。


二、方式一:使用 Spring AOP 实现开关降级

2.1 实现思路

Spring AOP 是实现开关降级最简单直接的方式。以 Redis 缓存开关为例,整体思路如下:

  1. 定义一个开关降级注解 @SwitchDegrade
  2. 实现切面 SwitchDegradeAspect,拦截带注解的方法
  3. 在切面中从 Redis 读取开关状态,决定是否降级
  4. 降级时抛出异常,由全局异常处理器统一处理

整个过程只需要三步,非常简单。

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 实现开关降级有以下优势:

  1. 配置化管理:和限流、熔断一样,全部通过规则配置,不需要加注解
  2. 统一管理:和其他降级规则统一管理,便于监控和运维
  3. SPI 扩展:利用 Sentinel 的扩展机制,侵入性低
  4. 灵活的资源匹配:支持 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");
                }
            }
        }
    }
}

逻辑说明:

  1. SwitchRuleManager 获取所有开关规则
  2. 遍历每条规则,只处理状态为 open 的规则
  3. include 模式:如果当前资源在 include 列表中,抛出 SwitchException
  4. exclude 模式:如果当前资源不在 exclude 列表中,抛出 SwitchException
  5. 一个资源可以被多个开关规则控制(只要有一个命中就降级)

注意: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 链机制为我们提供了强大的扩展能力,只要你有想法,几乎可以在请求的任意环节插入自定义逻辑。

实践建议

  1. 开关降级 + 动态配置:结合配置中心(Nacos/Apollo)实现开关的动态调整
  2. 开关分级管理:按重要程度分级,不同级别开关由不同人员管理
  3. 开关操作审计:记录开关的开启/关闭操作,便于追溯
  4. 定时开关:支持按时间自动开启/关闭开关(比如流量高峰时段自动开启)
  5. 开关演练:定期进行开关降级演练,确保功能可用

开关降级是一个看似简单但非常实用的功能,设计得当可以大大提升系统的可用性和容错能力。下一篇文章我们将介绍 Sentinel 的动态数据源,看看如何实现规则的动态配置,敬请期待。

© 版权声明
THE END
喜欢就支持一下吧
点赞8
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容

温馨提示:
1、本内容转载于网络,版权归原作者所有!
2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!