一种可灰度的接口迁移方案

发表于 2年以前  | 总阅读数:312 次

在快速迭代的互联网背景下,系统为了实现快速上线,常常会选择最快的开发模式,例如我们常见的mvp版本迭代。大部分的业务系统对于未来业务的发展是不确定的,因此随着时间的推移,往往会遇到各种各样的瓶颈,例如系统性能、无法适配业务逻辑等问题,这时可能就涉及到系统架构的升级。系统升级往往包含最基础的两个部分:接口迁移重构和数据迁移重构,在系统架构升级的过程中,最重要的是需要保证系统稳定性,即用户不感知。因此文本的目的是提供一种可灰度、回滚的设计思路,实现稳定的架构升级。

场景

在我们系统迭代过程中,往往涉及到重构、数据源切换、接口迁移等场景,为了保障系统平稳上线,因此在接口迁移过程中应该保证可回滚、可灰度。接口迁移可能也涉及到数据迁移,两者的先后顺序应该不影响到系统的稳定性。总结一下,接口迁移的目标:

  1. 可灰度,即使用新老接口是能够控制的。
  2. 可回滚,如使用新接口异常,能够快速回滚到老接口。
  3. 不入侵业务逻辑,不改动原来的业务逻辑代码,等迁移完毕后再整体下线,防止直接侵入修改造成不可逆的影响。
  4. 老接口在系统平稳运行后收口,即对老的数据源访问、老的接口能够平稳下线

迁移方案

本文主要为接口迁移和数据迁移提供了一种思路,在第3节里会有实践的核心代码实现。(代码只是提供思路,并不是能够直接运行的代码)

总体迁移方案

下图表示了接口迁移的思路,参考了cglib的jdk的代理方式。假设你有一个待迁移接口类(目标类),那么你需要重新写一个代理类作为迁移后的接口。目标类和代理类的选择通过开关去控制,开关涉及到两个层面:

  1. 总开关:用于控制是否全量切换新接口,当接口迁移稳定上线 且 数据迁移完毕(如有)
  2. 灰度开关:可以设置一个灰度开关列表,用于控制你的那些接口/数据需要走代理接口

针对不同的接口逻辑,代理接口实现逻辑会有差异,具体场景如下文所述。

单条数据查询

针对单条数据,可以通过数据源来判断来源。基于可灰度和回滚的原则,目标类和代理类的路由规则如下:

  1. 优先判断总开关,如果总控制开关已打开,则说明迁移已完成并且验证校验完毕,此时走代理接口,这样可以实现接口、数据的收口,达到我们的迁移目标。
  2. 如果数据不存在于老数据表中,那么无论这条数据有没有存在于新表中,我们都可以直接走代理接口,收拢新数据的接口逻辑。
  3. 如果数据存在于老数据表中,但是不在灰度名单内,此时使用目标类(回滚时可这么操作),走原来的接口方法,即老逻辑,这是不会影响到系统功能。
  4. 如果数据存在于老数据表中,但是在灰度名单内,说明这条数据已经迁移完成待验证,此时可以使用代理类(灰度时可这么操作)走新的接口逻辑。

多条数据查询

不同于单条数据的查询,我们需要查询中新表、老表中所有符合条件的数据,多条数据查询涉及到数据重复的问题(即数据会同时存在于老表和新表中),因此需要对数据进行去重,然后再合并返回结果。

数据更新

因为在数据迁移后到系统灰度的过程中存在中间时间,所以在数据更新时我们应该通过双写来保持新、老表数据的一致性。同时为了对接口和数据进行收口,我们也要先判断总控开关是否开启,如果总开关已经打开,则数据更新只需要更新新表即可。

数据插入

对数据和接口收口,我们需要对增量数据进行切换,因此直接使用代理类并将数据插入到新表中,控制老表的数据增量,在数据迁移的时候只需要考虑存量数据即可。

实践

例如在零售场景中,每个门店都有唯一的身份标识门店id,那么我们的灰度列表就可以存放门店id列表,按门店维度进行灰度,来粒度化影响范围。

代理分发逻辑

分发逻辑是核心逻辑,数据的去重规则、接口/仓储层代理转发都是基于这套逻辑来控制:

  1. 先判断总开关,总开关开启说明迁移完成,此时全部通过代理类走新的接口逻辑和数据源。
  2. 判断灰度开关,如果在灰度过程中包含了灰度的门店,那么就通过代理类走新的接口;否则走原接口的老逻辑,实现接口的切换。
  3. 新数据转发到代理类,对新的逻辑和数据进行收口,防止增量数据的产生。
  4. 批量查询接口需要转发到代理类,因为涉及到对新、老数据进行去重、合并的过程。
    /**
     * 是否开启代理
     *
     * @param ctx 上下文
     * @return 是:开启代理,否:不开启代理
     */
    public Boolean enableProxy(ProxyEnableContext ctx) {
        if (ctx == null) {
            return false;
        }

        // 判断总开关
        if (总开关打开) {
            // 说明数据迁移完成,接口全部切换
            return true;
        }

        if (单个门店操作) {
            if (存在老数据源) {
                // 判断是否在灰度名单,是则返回true;否则返回false;

            } else {
                // 新数据
                return true;
            }
        } else {
            // 批量查询,需要走代理合并新、老数据源
            return true;
        }
    }

接口代理

接口代理主要通过切面来拦截,通过注解方法的方式来实现。代理注解如下


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableProxy {

    // 用于标识代理类
    Class<?> proxyClass();

    // 用于标识转发的代理类的方法,默认取目标类的方法名
    String methodName() default "";

    // 对于单条数据的查询,可以指定key的参数索引位置,会解析后转发
    int keyIndex() default -1;
}

切面的实现核心逻辑就是拦截注解,根据代理分发的逻辑去判断是否走代理类,如果走代理类需要解析代理类型、方法名、参数,然后进行转发。


@Component
@Aspect
@Slf4j
public class ProxyAspect {

    // 核心代理类
    @Resource
    private ProxyManager proxyManager;

    // 注解拦截
    @Pointcut("@annotation(***)")
    private void proxy() {}

    @Around("proxy()")
    @SuppressWarnings("rawtypes")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
            Class<?> clazz = joinPoint.getTarget().getClass();
            String methodName = methodSignature.getMethod().getName();
            Class[] parameterTypes = methodSignature.getParameterTypes();
            Object[] args = joinPoint.getArgs();
            // 拿到方法的注解
            EnableProxy enableProxyAnnotation = ReflectUtils
                .getMethodAnnotation(clazz, EnableProxy.class, methodName, parameterTypes);

            if (enableProxyAnnotation == null) {
                // 没有找到注解,直接放过
                return joinPoint.proceed();
            }


            //判断是否需要走代理
            Boolean enableProxy = enableProxy(clazz, methodName, args, enableProxyAnnotation);
            if (!enableProxy) {
                // 不开启代理,直接放过
                return joinPoint.proceed();
            }

            // 默认取目标类的方法名称
            methodName = StringUtils.isNotBlank(enableProxyAnnotation.methodName())
                ? enableProxyAnnotation.methodName() : methodName;

            // 通过反射拿到代理类的代理方法
            Object bean = ApplicationContextUtil.getBean(enableProxyAnnotation.proxyClass());
            Method proxyMethod = ReflectUtils.getMethod(enableProxyAnnotation.proxyClass(), methodName, parameterTypes);
            if (bean == null || proxyMethod == null) {
                // 没有代理类或代理方法,直接走原逻辑
                return joinPoint.proceed();
            }

            // 通过反射,转发代理类方法
            return ReflectUtils.invoke(bean, proxyMethod, joinPoint.getArgs());

        } catch (BizException bizException) {
            // 业务方法异常,直接抛出
            throw bizException;
        } catch (Throwable throwable) {
            // 其他异常,打个日志感知一下
            throw throwable;
        }
    }
}

仓储层代理

如果走了代理类,那么逻辑都会被转发到ProxyManager,由代理类管理器来负责数据的分发、去重、合并、更新、插入等操作。

  • 单条数据查询

代理查询流程图如下图所示,目标接口的目标方法会通过代理被切面拦截掉,切面判断是否需要走代理接口

  1. 如果不需要走代理接口(即数据源是老的并且未被灰度),则继续走目标接口
  2. 如果需要走代理接口(即数据源是新的或者老数据迁移后在灰度列表内),则调用代理接口方法,在代理接口方法中会对仓储层逻辑进行进一步的转发,由ProxyManager统一进行收口。在单条数据的查询逻辑里,只需要调用代理仓储层服务查询新数据源就可以了,逻辑比较简单。

例如单个门店的信息查询,那么我们核心控制器ProxyManager方法逻辑就可以这么实现:



    public <T> T getById(Long id, Boolean enableProxy) {

        if (enableProxy) {
            // 开启代理,就走代理仓储层的查询服务
            return proxyRepository.getById(id);
        } else {
            // 没开启代理,走原来仓储层的服务
            return targetRepository.getById(id);
        }
    }
  • 多条数据查询+去重

多条数据的去重逻辑是一样,去重规则如下:

  1. 新表、老表都不存在,数据剔除,不反回结果。
  2. 新表没有,使用老表数据的信息。
  3. 老表没有,使用新表数据的信息。
  4. 老表、新表都存在数据(迁移完成),此时判断总控是否打开,以及数据是否在灰度名单,满足其一使用新表数据;否则使用老表数据

基于以上去重逻辑,所有的查询接口都可以抽象成统一的方法

  1. 查询老数据,业务定义,用supply函数封装查询逻辑
  2. 查询新数据,业务定义,用supply函数封装查询逻辑
  3. 合并去重,抽象出统一的合并工具

核心的流程如下图所示,目标接口的目标方法都会被切面拦截,转发到代理接口。代理接口在调用数据源的地方可以进一步转发给ProxyManager进行查询&合并。如果总开关未开启,说明全量数据还没有迁移验证完毕,那么还是需要查老的数据源(防止数据遗漏)。如果开关开启了,则说明迁移完成,此时不会再调用原来的仓储层服务,达到了对老的数据源收口的目的。

例如批量查询门店列表,可以这么合并,核心实现如下:



    public <T> List<T> queryList(List<Long> ids, Function<T, Long> idMapping) {
        if (CollectionUtils.isEmpty(ids)) {
            return Collections.emptyList();
        }

        // 1. 查询老数据
        Supplier<List<T>> oldSupplier = () -> targetRepository.queryList(ids);
        // 2. 查询新数据
        Supplier<List<T>> newSupplier = () -> proxyRepository.queryList(ids);
        // 3. 根据合并规则合并,依赖合并工具(对合并逻辑进行抽象后的工具类)
        return ProxyHelper.mergeWithSupplier(oldSupplier, newSupplier, idMapping);
    }

合并工具类实现如下:


public class ProxyHelper {

    /**
     * 核心去重逻辑,判断是否采用新表数据
     *
     * @param existOldData 是否存在老数据
     * @param existNewData 是否存在新数据
     * @param id      门店id
     * @return 是否采用新表数据
     */
    public static boolean useNewData(Boolean existOldData, Boolean existNewData, Long id) {
        if (!existOldData && !existNewData) {
            //两张表都没有
            return true;
        } else if (!existNewData) {
            //新表没有
            return false;
        } else if (!existOldData) {
            //老表没有
            return true;
        } else {
            //新表老表都有,判断开关和灰度开关
            return 总开关打开 or 在灰度列表内
        }

    }

     /**
     * 合并新/老表数据
     *
     * @param oldSupplier 老表数据
     * @param newSupplier 新表数据
     * @return 合并去重后的数据
     */
    public static <T> List<T> mergeWithSupplier(
        Supplier<List<T>> oldSupplier, Supplier<List<T>> newSupplier, Function<T, Long> idMapping) {

        List<T> old = Collections.emptyList();

        if (总开关未打开) {
            // 未完成切换,需要查询老的数据源
            old = oldSupplier.get();
        }

        return merge(idMapping, old, newSupplier.get());
    }


    /**
     * 去重并合并新老数据
     *
     * @param idMapping      门店id映射函数
     * @param oldData        老数据
     * @param newData        新数据
     * @return 合并结果
     */
    public static <T> List<T> merge(Function<T, Long> idMapping, List<T> oldData, List<T> newData) {
        if (CollectionUtils.isEmpty(oldData) && CollectionUtils.isEmpty(newData)) {
            return Collections.emptyList();
        }

        if (CollectionUtils.isEmpty(oldData)) {
            return newData;
        }

        if (CollectionUtils.isEmpty(newData)) {
            return oldData;
        }

        Map<Long/*门店id*/, T> oldMap = oldData.stream().collect(
            Collectors.toMap(idMapping, Function.identity(), (a, b) -> a));

        Map<Long/*门店id*/, T> newMap = newData.stream().collect(
            Collectors.toMap(idMapping, Function.identity(), (a, b) -> a));

        return ListUtils.union(oldData, newData)
            .stream()
            .map(idMapping)
            .distinct()
            .map(id -> {
                boolean existOldData = oldMap.containsKey(id);
                boolean existNewData = newMap.containsKey(id);
                boolean useNewData = useNewData(existOldData, existNewData, id);
                return useNewData ? newMap.get(id) : oldMap.get(id);
            })
            .filter(Objects::nonNull)
            .collect(Collectors.toList());
    }
}
  • 增量数据

代码省略,直接执行代理仓储层的插入方法即可

  • 更新数据

更新数据需要双写,如果总开关打开(即迁移完毕),则可以停止老数据的写入,因为不会再读了。


@Transactional(rollbackFor = Throwable.class)
    public <T> Boolean update(T t) {
        if (t == null) {
            return false;
        }

        if (总开关没打开) {
            // 数据没有迁移完毕
            // 更新要双写,如有,保持数据一致
            targetRepository.update(t);
        }

        // 更新新数据
        proxyRepository.update(t);
        return true;
    }

实践

本文只是提出一种迁移的方案思路,可能并不能适用于所有场景,但是在系统升级的过程中,工程师面对的最终的目标应该是一致的,即为了让系统稳定的上线,并且在出现问题时能够安全回滚。本文的实现逻辑是通过注解和切面实现对目标接口的方法进行转发,转发到代理类接口,从而切换到新逻辑和新数据源,并由ProxyManager来适配数据源的代理分发逻辑,完成数据的查询、更新、新增逻辑。

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/UJM8Jffgeg03ggQFRP2Msw

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237269次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8108次阅读
 目录