15-自定义ProcessorSlot实现开关降级

原创 吴就业 112 0 2020-09-22

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/63bc1e5acc8047eda6af4fcd7fe65732

作者:吴就业
链接:https://wujiuye.com/article/63bc1e5acc8047eda6af4fcd7fe65732
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

深入理解Sentinel

开关降级在我们公司的电商项目中是每个微服务都必须支持的一项功能,主要用于活动期间、每日流量高峰期间、主播带货期间关闭一些无关紧要功能,降低数据库的压力。

开关降级实现起来很简单,例如,我们可以使用Spring AOP或者动态代理模式拦截目标方法的执行,在方法执行之前,先根据key从Redis读取value,如果value是true,则不执行目标方法,直接响应服务降级。这种方式付出的性能损耗就只有一次redis的get操作,如果不想每个请求都读Redis缓存,也可以通过动态配置方式,使用配置中心去管理开关。

使用Spring AOP实现开关降级功能

以Redis缓存开关为例,使用切面实现开关降级只需要三步:定义注解、实现开关降级切面、在需要使用开关降级的接口方法上添加开关降级注解。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface SwitchDegrade {
    // 缓存key
    String key() default "";
}

提示:如果是应用在实际项目中,建议为@SwitchDegrade注解添加一个前缀属性,限制同一个应用下的开关key都是有同一个前缀,避免多个应用之间的缓存key冲突。

@Aspect
public class SwitchDegradeAspect {
    // 切点定义
    @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();
    }
}

如代码所示,SwitchDegradeAspect拦截目标方法的执行,先从方法上的@SwitchDegrade注解获取开关的缓存key,根据key从redis读取当前开关的状态,如果key存在且value为true,则抛出一个开关降级异常。

当开关打开时,SwitchDegradeAspect并不直接响应请求,而是抛出异常由全局异常处理器处理,这是因为并不是每个接口方法都会有返回值,且返回值类型也不固定。所以还需要在全局异常处理器处理开关降级抛出的异常,代码如下。

    @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,则全局异常可以不配置。

@RestController
@RequestMapping("/v1/test")
public class DemoController {
    @SwitchDegrade(key = "auth:switch")
    @PostMapping("/demo")
    public GenericResponse<Void> demo(@RequestBody Invocation<DemoFrom> invocation) {
       //.....
    }
}

这种方式虽然能满足需求,但也有一个缺点,就是必须要在方法上添加@SwitchDegrade注解,配置不够灵活,但也不失为一个好方法。

基于Sentinel自定义ProcessorSlot实现开关降级功能

Sentinel将降级功能的实现抽象为处理器插槽(ProcessorSlot),由一个个ProcessorSlot提供丰富的降级功能的实现,并且使用SPI机制提供扩展功能,使用者可通过自定义SlotChainBuilder自己构建ProcessorSlotChain,这相当于给我们提供插件的功能。因此,我们可以通过自定义ProcessorSlot为Sentinel添加开关降级功能。

与熔断降级、限流降级一样,我们也先定义开关降级规则类,实现loadRules API;然后提供一个Checker,由Checker判断开关是否打开,是否需要拒绝当前请求;最后自定义ProcessorSlot与SlotChainBuilder。

与使用切面实现开关降级有所不同,使用Sentinel实现开关降级我们不需要再在接口方法或者类上添加注解,我们想要实现的是与熔断降级、限流降级一样全部通过配置规则实现,这也是我们为什么选择基于Sentinel实现开关降级功能的原因。

通常,一个开关会控制很多的接口,而不仅仅只是一个,所以,一个开关对应一个降级规则,一个降级规则可配置多个资源。开关降级规则类SwitchRule实现代码如下:

@Data
@ToString
public class SwitchRule {
    public static final String SWITCH_KEY_OPEN = "open";
    public static final String SWITCH_KEY_CLOSE = "close";
    // 开关状态
    private String status = SWITCH_KEY_OPEN;
    // 开关控制的资源
    private Resources resources;
    @Data
    @ToString
    public static class Resources {
        // 包含
        private Set<String> include;
        // 排除
        private Set<String> exclude;
    }
}

灵活,不仅仅只是去掉注解的使用,更需要可以灵活指定开关控制某些资源,因此,配置开关控制的资源应支持这几种情况:指定该开关只控制哪些资源、除了某些资源外其它都受控制、控制全部。所以,SwitchRule的资源配置与Sentinel的熔断降级、限流降级规则配置不一样,SwitchRule支持三种资源配置方式:

接着实现loadRules API。在Sentinel中,提供loadRules API的类通常命名为XxxRuleManager,即Xxx规则管理器,所以我们定义的开关降级规则管理器命名为SwitchRuleManager。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;
    }
}

SwitchRuleManager提供两个接口:

同样的,在Sentinel中,一般会将检查规则是否达到触发降级的阈值由XxxRuleChecker完成,即Xxx规则检查员,所以我们定义的开关降级规则检查员命名为SwitchRuleChecker,由SwitchRuleChecker检查开关是否打开,如果开关打开则触发开关降级。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");
                }
            }
        }
    }

}

如代码所示,SwitchRuleChecker从SwitchRuleManager获取配置的开关降级规则,遍历开关降级规则,如果开关打开,且匹配到当前资源名称被该开关控制,则抛出SwitchException。

SwitchException需继承BlockException,抛出的SwitchException如果不被捕获,则由全局异常处理器处理。一定是要抛出BlockException的子类,否则抛出的异常会被资源指标数据统计收集,会影响到熔断降级等功能的准确性。

虽然SwitchRuleChecker使用了for循环遍历开关降级规则,但一个项目中的开关是很少的,一般就一个或者几个。

与熔断降级、限流降级一样,开关降级也支持一个资源被多个开关规则控制。

最后,还需要自定义实现开关降级功能的切入点SwitchSlot。SwitchSlot继承AbstractLinkedProcessorSlot,在entry方法中调用SwitchRuleChecker#checkSwitch方法检查当前资源是否已经降级。SwitchSlot的代码实现如下:

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);
        fireEntry(context, resourceWrapper, param, count, prioritized, args);
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        fireExit(context, resourceWrapper, count, args);
    }

}

自定义ProcessorSlotChain构建器MySlotChainBuilder,将自定义的SwitchSlot添加到ProcessorSlot链表的末尾。当然,可以添加到任何位置,因为SwitchSlot没有用到指标数据,SwitchSlot放置何处都不会影响到Sentinel的正常工作。

MySlotChainBuilder代码实现如下:

public class MySlotChainBuilder extends DefaultSlotChainBuilder {
    @Override
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = super.build();
        chain.addLast(new SwitchSlot());
        return chain;
    }
}

MySlotChainBuilder继承DefaultSlotChainBuilder只是为了使用DefaultSlotChainBuilder#build方法,简化ProcessorSlotChain的构造步骤,只需要在DefaultSlotChainBuilder构造的链表尾部添加一个SwitchSlot即可。

现在MySlotChainBuilder生效了吗?当然还不行,还需要将MySlotChainBuilder注册到SlotChainBuilder接口的配置文件。

在当前项目的resources资源目录的META-INF/service目录下创建一个名为“com.alibaba.csp.sentinel.slotchain.SlotChainBuilder”的文件,在该文件中配置MySlotChainBuilder类的全名,例如:

com.wujiuye.demo.common.sentinel.MySlotChainBuilder

现在,您可以在MySlotChainBuilder#build方法中添加断点,然后启动项目,正常情况下程序会在该断点停下。但由于我们并未配置开关降级规则,所以还看不到效果。

我们可以写一个配置类,在配置类调用SwitchRuleManager#loadRules API添加开关降级规则。例如:

@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);
            // 
            rules.add(rule);
            SwitchRuleManager.loadSwitchRules(rules);
    }
}

上面代码配置了一个开关降级规则,该降级规则只控制接口(资源)”/v1/test/demo”,SwitchRule.status控制开关是否打开,测试这里就不演示了。当然,这种配置方式只适用于本地测试,实际项目中我们可通过动态配置实现,这将在后面介绍Sentinel动态数据源时再介绍如何实现。

提示:实现动态配置规则并不一定需要使用Sentinel的动态数据源。

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

Spring Data R2DBC快速上手指南

本篇内容介绍如何使用r2dbc-mysql驱动程序包与mysql数据库建立连接、使用r2dbc-pool获取数据库连接、Spring-Data-R2DBC增删改查API、事务的使用,以及R2DBC Repository。

使用Spring WebFlux + R2DBC搭建消息推送服务

消息推送服务主要是处理同步给用户推送短信通知或是异步推送短信通知、微信模板消息通知等。本篇介绍如何使用Spring WebFlux + R2DBC搭建消息推送服务。

教你如何编写一个IDEA插件,并掌握核心知识点PSI

IDEA有着极强的扩展功能,它提供插件扩展支持,让开发者能够参与到IDEA生态建设中,为更多开发者提供便利、提高开发效率。我们常用的插件有Lombok、Mybatis插件,这些插件都大大提高了我们的开发效率。即便IDEA功能已经很强大,并且也已有很多的插件,但也不可能面面俱到,有时候我们需要自给自足。

Spring Boot实现加载自定义配置文件

本篇将介绍两种加载自定义配置文件的实现方式,并通过分析源码了解SpringBoot加载配置文件的流程,从而加深理解。

设计模式那些模糊不清的概念

23种设计模式属于结构型模式,而mvc模式等属于架构型模式。本篇要讨论的设计模式指的是结构型设计模式。

实现一个分布式调用链路追踪Java探针你可能会遇到的问题

Instrumentation之所以难驾驭,在于需要了解Java类加载机制以及字节码,一不小心就能遇到各种陌生的Exception。笔者在实现Java探针时就踩过不少坑,其中一类就是类加载相关的问题,也是本篇所要跟大家分享的。