新老系统切流方案设计与实现
  qbian 2020年12月09日 158 2

一 背景

软件开发初期,需求相对简单和明确,开发出来的应用只需要满足需求能够快速上线就好,这个时候的应用在代码和业务逻辑上都比较清晰。

随着业务的不断增长,各种各样的需求也接踵而至。有倒排需求(时间紧,项目周期根据上线时间点往前推)、紧急需求(可能是某个业务逻辑漏洞需要立刻修复上线)、定制化需求(为了某种特殊场景提出的定制化功能),当这些需求不断在应用上实现,应用内会存在大量if else的代码,就算引入了良好的设计模式,也会使应用越来越臃肿,最后难以维护。

随着时间的推移,公司人员的变动也是在所难免的,新人介入现有的应用做需求,很难按照最初的应用设计来做,也很难按照最初的代码规范来写,也会不断的加大应用维护成本。

业务的增长和人员的变动都会推进一个应用从上线到被下线的进程。

当业务快速增长,应用系统的维护跟不上需求的落地时,就需要对现有系统做重构了。重构的方式有:

  • 在现有系统内部分代码慢慢迭代重构。
  • 应用功能拆分:类似现在比较火的微服务架构,将属于某个领域内的功能拆分到一个应用内。上游流量可以不经过现有应用,直接调新应用。
  • 可复用功能下沉:类似现在比较火的中台设计,将可多个应用都使用的某个功能下沉为一个下游应用,上游流量调用现有应用,现有应用在调下游新拆分的应用。

今天我们要说的类似可复用功能下沉的这种拆分模式。在不改变上游调用方式的情况下,将现有的某个功能下沉到新应用,并需要按照不同的策略静默切流。

二 需求

要想完成静默切流,就不能对现有功能有影响,也就需要完全兼容现有业务场景下的所有逻辑,不同的业务场景可能会有:

  • 将某个接口的功能完全下沉到下游应用:调下游成功后(失败降级调本地),将下游返回结果直接返回给上游调用方;
  • 将某个接口的功能部分下沉到下游应用:调下游成功后(失败降级调本地),拿到返回结果,在现有应用内继续处理接下来的逻辑;
  • 新旧应用数据同步更新:本地和下游都需要调用(失败忽略,输出日志、可告警);
  • 数据查询:本地和下游都调用(失败忽略,输出日志、可告警),并判断下游结果是否和本地一致,优先以本地结果为准;

在静默切流的前提下,还需要做到能够根据不同业务维度去控制切流的流量,不同的维度可能会有:

  • 切流总开关,控制是否切流,存在异常可通过开关关闭;
  • 流量所属业务平台;
  • 用户ID;
  • 订单尾号百分比;

三 方案

详细时序如下:

创建订单接口切流时序.png

3.1 切流请求上下文

对需要切流的接口再接收到请求后,根据请求上下文信息构造切流上下文对象,切流请求上下文可定义如下:

@Data
public class TangentFlowCondition {
    /**
     * 订单号
     */
    private String orderNo;
    
    /**
     * 请求用户ID
     */
    private Long userId;
    
    /**
     * 所属平台
     */
    private Integer platform;
}

3.2 切流配置

切流配置信息随时可能会改动,需要支持改动后的配置可以动态发布,这里可以使用Apollo配置中心。

Apollo:应用在启动的时候会从Apollo拉取指定namespace下的所有配置信息缓存在本地,并启动一个异步线程通过http长轮询的方式监听配置中心下指定namespace的配置变更事件,有配置变更的话,会实时拉取最新配置并更新本地缓存。

可定义一份如下配置:

{
    "switch": "y",
    "items": [
        {
            "platform": 0,
            "userIdList": [],
            "orderNoTailNumberLt": 20
        },
        {
            "platform": 1,
            "userIdList": [],
            "orderNoTailNumberLt": 50
        }
    ]
}

配置信息说明:

配置项 描述
switch 切流开关,用于控制是否开启切流,可选枚举:y -> 开启切流、n -> 关闭切流、* -> 不判断下面条件,全部切流
items 具体切流条件配置
items.platform 流量所属平台,符合该平台的流量走该切流配置
items.userIdList 用户ID,在配置内的用户走切流调用
items.orderNoTailNumberLt 订单尾号后两位小于该配置,走切流调用

3.3 切流条件判断

根据切流请求上下文和配置信息,我们就可以判断这次请求是否满足切流条件,对于满足条件的请求,就可以根据具体切流规则去调用对应的实现。条件判断如下:

public boolean shouldTangentFlow(TangentFlowCondition condition) {
    // 获取 Apollo 配置
    TangentFlowConfig config = ApolloUtil.getTangentFlowRoleConfig();
    
    if ("*".equals(config.getTangentFlowSwitch())) {
        return true;
    }

    if ("n".equals(config.getTangentFlowSwitch())) {
        return false;
    }

    if (ObjectUtils.isEmpty(config.getItems())) {
        return false;
    }

    // 判断具体切流条件
    return (matchPlatform(config, condition) && matchUser(config, condition))
            || (matchPlatform(config, condition) && matchOrderNo(config, condition));
}

3.4 切流规则实现

解决了配置和请求上下文问题,接下来就是在应用内怎么实现静默切流了,还需要考虑异常降级;

方案一:自定义注解,使用在需要切流的方法上,可配置切流后的目标Class,对应的methodName,下游调用成功后回调类Class,回调methodName,以及具体调用策略枚举类型。在注解实现处,获取当前请求切流上下文信息,判断是否满足切流条件,满足条件的情况下。再根据配置的调用策略,通过spring根据配置的Class获取目标bean,再通过反射调用目标bean的method方法。

方案二:抽象接口,本地和下游服务实现同一个接口,通过多态的方式,同一个接口有不同的实现方式,该接口再有一个代理服务的实现类,代理实现类通过has-a的方式引用本地实现和下游服务实现,在不同方法的内部实现上判断调用策略走不同的调用。

四 实现

4.1 方案一实现

切流规则枚举:

public enum TangentFlowStrategyEn {
    /**
     * 无论新老方法.仅执行一个
     */
    ONLY,

    /**
     * 新老方法都执行
     */
    PARALLEL,

    /**
     *  执行新接口,新接口执行成功后再执行success回调接口。
     *  如果失败,则执行原先的接口
     */
    SUCCESS_CALL_BACK,

    /**
     * 新老方法都执行,并校验返回结果,结果不一致的话,返回旧方法的结果
     */
    PARALLEL_CHECK_FAIL_BACK,
}

自定义注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface TangentFlowAnnotation {

    /**
     * 切流接口实现类
     */
    Class clazz();

    /**
     * 切流接口实现类的目标方法
     */
    String method() default "";

    /**
     * 切流成功后需要执行回调方法的class.
     */
    Class successCallBackClazz() default ObjectUtils.Null.class;

    /**
     * 回调类的回调方法
     */
    String successCallBackMethod() default "";

    /**
     * 切流执行策略
     */
    TangentFlowStrategyEn strategy() default TangentFlowStrategyEn.ONLY;
}

注解实现:

@Component
@Aspect
public class TangentFlowAspect {

    @Resource
    private TangentFlowRole tangentFlowRole;

    @Pointcut("@annotation(com.xxx.xxx.xxx.TangentFlowAnnotation)")
    private void myPointcut() {}

    @Around("com.xxx.xxx.xxx.TangentFlowAspect.myPointcut()")
    public Object intercept(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] invokeApiArgs = getArgs(joinPoint);

        TangentFlowCondition tangentFlowCondition;
        try {
            tangentFlowCondition = (TangentFlowCondition) invokeApiArgs[invokeApiArgs.length - 1];
        } catch (Exception e) {
            // 获取切流上下文异常
            return joinPoint.proceed();
        }

        // 判断是否需要切流
        boolean shouldTangentFlow = false;
        try {
            shouldTangentFlow = tangentFlowRole.shouldTangentFlow(tangentFlowCondition);
        } catch (Exception e) {
            // 判断是否满足切流条件异常
            return joinPoint.proceed();
        }
        if (!shouldTangentFlow) {
            return joinPoint.proceed();
        }

        Object res;
        switch (tangentFlow.strategy()) {
            // 判断具体执行策略,根据不同策略执行目标方法
        }

        return res;
    }
}

4.2 方案二实现

定义接口:

public interface TradeService {

    /**
     * 创建订单接口
     * @param parameter
     * @return
     */
    Object createOrder(Object parameter);
}

切流实现:

@Service
public class TangentFlowTradeServiceImpl implements TradeService {

    @Override
    public Object createOrder(Object parameter) {
        // 切流实现
        return null;
    }
    
}

本地实现:

@Service
public class LocalTradeServiceImpl implements TradeService {
    
    @Override
    public Object createOrder(Object parameter) {
        // 本地实现
        return null;
    }
    
}

代理实现:

@Service
public class ProxyTradeServiceImpl implements TradeService {

    @Resource
    private LocalTradeServiceImpl localTradeService;

    @Resource
    private TangentFlowTradeServiceImpl tangentFlowTradeService;

    @Override
    public Object createOrder(Object parameter) {
        // 这里判断切流策略 strategy,根据不同接口不同策略调用不同实现
        if (true) {
            return localTradeService.createOrder(parameter);
        }
        return tangentFlowTradeService.createOrder(parameter);
    }
}

总结:实际场景会更复杂,这里只是简单梳理了下大概思路和设计方案,仅供参考。

最后一次编辑于 2021年01月28日 3

魂枫

很强 赞赞赞!!!

2020-12-09 16:46:30      回复

魂枫

很强 赞赞赞!!!

2020-12-09 16:46:23      回复