一种接口依赖关系分层方案

一种,接口,依赖,关系,分层,方案 · 浏览次数 : 348

小编点评

**层级结构** * 4 分层:使用TreeMap的key倒序即为调用的层级顺序对应key中的数据节点Set需用多线程并发调用,以保证链路调用时间4 * 分层级调用梳理出调用关系并分层后,使用并发编排工具调用即可 **调用链路上的资源复用** * 多线程并发调用多个平行数据接口时,如何使得各个线程的耗时方差尽可能小?作者:京东零售 王江波来源:京东云开发者社区。归纳总结以上内容,生成内容时需要带简单的排版

正文

1、背景

到店商详迭代过程中,需要提供的对外能力越来越多,如预约日历、附近门店、为你推荐等。这其中不可避免会出现多个上层能力依赖同一个底层接口的场景。最初采用的方案是对外API入口进来后获取对应的能力,并发调用多项能力,由能力层调用对应的数据链路,进行业务处理。然而,随着接入功能的增多,这种情况导致了底层数据服务的重复调用,如商品配置信息,在一次API调用过程中重复调了3次,当流量增大或能力项愈多时,对底层服务的压力会成倍增加。

正值618大促,各方接口的调用都会大幅度增加。通过梳理接口依赖关系来减少重复调用,对本系统而言,降低了调用数据接口时的线程占用次数,可以有效降级CPU。对调用方来说,减少了调用次数,可减少调用方的资源消耗,保障底层服务的稳定性。

原始调用方式:

2、优化

基于上述问题,采用底层接口依赖分层调用的方案。梳理接口依赖关系,逐层向上调用,注入数据,如此将同一接口的调用抽取到某层,仅调用一次,即可在整条链路使用。

改进调用方式:

只要分层后即可在每层采用多线程并发的方式调用,因为同一层级中的接口无先后依赖关系。

3、如何分层?

接下来,如何梳理接口层级关系就至关重要。

接口梳理分层流程如下:

第一步:构建层级结构

首先获取到能力层依赖项并遍历,然后调用生成数据节点方法。方法流程如下:构建当前节点,检测循环依赖(存在循环依赖会导致栈溢出),获取并遍历节点依赖项,递归生成子节点,存放子节点。

第二步:节点平铺

定义Map维护平铺结构,调用平铺方法。方法流程如下:遍历层级结构,判断当前节点是否已存在map中,存在时与原节点比较将层级大的节点放入(去除重复项),不存在时直接放入即可。然后处理子节点,递归调用平铺方法,处理所有节点。

第三步:分层(分组排序)

流处理平铺结构,处理层级分组,存储在TreeMap中维护自然排序。对应key中的数据节点Set需用多线程并发调用,以保证链路调用时间

1 首先,定义数据结构用于维护调用链路

Q1:为什么需要定义祖先节点?

A1:为了判断接口是否存在循环依赖。如果接口存在循环依赖而不检测将导致调用栈溢出,故而在调用过程中要避免并检测循环依赖。在遍历子节点过程中,如果发现当前节点的祖先已经包含当前子节点,说明依赖关系出现了环路,即循环依赖,此时抛异常终止后续流程避免栈溢出。

public class DataNode {
    /**
     * 节点名称
     */
    private String name;
    /**
     * 节点层级
     */
    private int level;
    /**
     * 祖先节点
     */
    private List<String> ancestors;
    /**
     * 子节点
     */
    private List<DataNode> children;
}

2 获取能力层的接口依赖,并生成对应的数据节点

Q1:生成节点时如何维护层级?

A1:从能力层依赖开始,层级从1递加。每获取一次底层依赖,底层依赖所生成的节点层级即父节点层级+1。

/**
 * 构建层级结构
 *
 * @param handlers 接口依赖
 * @return 数据节点集
 */
private List<DataNode> buildLevel(Set<String> handlers) {
    List<DataNode> result = Lists.newArrayList();

    for (String next : handlers) {
        DataNode dataNode = generateNode(next, 1, null, null);
        result.add(dataNode);
    }
    return result;
}

/**
 * 生成数据节点
 *
 * @param name 节点名称
 * @param level 节点层级
 * @param ancestors 祖先节点(除父辈)
 * @param parent 父节点
 * @return DataNode 数据节点
 */
private DataNode generateNode(String name, int level, List<String> ancestors, String parent) {
    AbstractInfraHandler abstractInfraHandler = abstractInfraHandlerMap.get(name);
    Set<String> infraDependencyHandlerNames = abstractInfraHandler.getInfraDependencyHandlerNames();
    // 根节点
    DataNode dataNode = new DataNode(name);
    dataNode.setLevel(level);
    dataNode.putAncestor(ancestors, parent);
    if (CollectionUtils.isNotEmpty(dataNode.getAncestors()) && dataNode.getAncestors().contains(name)) {
        throw new IllegalStateException("依赖关系中存在循环依赖,请检查以下handler:" + JsonUtil.toJsonString(dataNode.getAncestors()));
    }
    if (CollectionUtils.isNotEmpty(infraDependencyHandlerNames)) {
        // 存在子节点,子节点层级+1
        for (String next : infraDependencyHandlerNames) {
            DataNode child = generateNode(next, level + 1, dataNode.getAncestors(), name);
            dataNode.putChild(child);
        }
    }
    return dataNode;
}

层级结构如下:

3 数据节点平铺(遍历出所有后代节点)

Q1:如何处理接口依赖过程中的重复项?

A1:遍历所有的子节点,将所有子节点平铺到一层,平铺时如果节点已经存在,比较层级,保留层级大的即可(层级大说明依赖位于更底层,调用时要优先调用)。

/**
 * 层级结构平铺
 *
 * @param dataNodes 数据节点
 * @param dataNodeMap 平铺结构
 */
private void flatteningNodes(List<DataNode> dataNodes, Map<String, DataNode> dataNodeMap) {
    if (CollectionUtils.isNotEmpty(dataNodes)) {
        for (DataNode dataNode : dataNodes) {
            DataNode dataNode1 = dataNodeMap.get(dataNode.getName());
            if (Objects.nonNull(dataNode1)) {
                // 存入层级大的即可,避免重复
                if (dataNode1.getLevel() < dataNode.getLevel()) {
                    dataNodeMap.put(dataNode.getName(), dataNode);
                }
            } else {
                dataNodeMap.put(dataNode.getName(), dataNode);
            }
            // 处理子节点
            flatteningNodes(dataNode.getChildren(), dataNodeMap);
        }
    }
}

平铺结构如下:

4 分层(分组排序)

Q1:如何分层?

A1:节点平铺后已经去重,此时借助TreeMap的自然排序特性将节点按照层级分组即可。

/**
 * @param dataNodeMap 平铺结构
 * @return 分层结构
 */
private TreeMap<Integer, Set<DataNode>> processLevel(Map<String, DataNode> dataNodeMap) {
    return dataNodeMap.values().stream().collect(Collectors.groupingBy(DataNode::getLevel, TreeMap::new, Collectors.toSet()))
}

分层如下:

1.根据分层TreeMap的key倒序即为调用的层级顺序

对应key中的数据节点Set需用多线程并发调用,以保证链路调用时间

4、分层级调用

梳理出调用关系并分层后,使用并发编排工具调用即可。这里梳理的层级关系,level越大,表示越优先调用。

这里以京东内部并发编排框架为例,说明调用流程:

/**
 * 构建编排流程
 *
 * @param infraDependencyHandlers 依赖接口
 * @param workerExecutor 并发线程
 * @return 执行数据
 */
public Sirector<InfraContext> buildSirector(Set<String> infraDependencyHandlers, ThreadPoolExecutor workerExecutor) {
    Sirector<InfraContext> sirector = new Sirector<>(workerExecutor);
    long start = System.currentTimeMillis();
    // 依赖顺序与执行顺序相反
    TreeMap<Integer, Set<DataNode>> levelNodes;
    TreeMap<Integer, Set<DataNode>> cacheLevelNodes = localCacheManager.getValue("buildSirector");
    if (Objects.nonNull(cacheLevelNodes)) {
        levelNodes = cacheLevelNodes;
    } else {
        levelNodes = getLevelNodes(infraDependencyHandlers);
        ExecutorUtil.executeVoid(asyncTpExecutor, () -> localCacheManager.putValue("buildSirector", levelNodes));
    }
    log.info("buildSirector 梳理依赖关系耗时:{}", System.currentTimeMillis() - start);
    // 最底层接口执行
    Integer firstLevel = levelNodes.lastKey();
    EventHandler[] beginHandlers = levelNodes.get(firstLevel).stream().map(node -> abstractInfraHandlerMap.get(node.getName())).toArray(EventHandler[]::new);
    EventHandlerGroup group = sirector.begin(beginHandlers);

    Integer lastLevel = levelNodes.firstKey();
    for (int i = firstLevel - 1; i >= lastLevel; i--) {
        EventHandler[] thenHandlers = levelNodes.get(i).stream().map(node -> abstractInfraHandlerMap.get(node.getName())).toArray(EventHandler[]::new);
        group.then(thenHandlers);
    }
    return sirector;
}

5、 个人思考

  1. 作为接入内部RPC、Http接口实现业务处理的项目,在使用过程中要关注调用链路上的资源复用,尤其长链路的调用,要深入考虑内存资源的利用以及对底层服务的压力。

  2. 要关注对外服务接口与底层数据接口的响应时差,分析调用逻辑与流程是否合理,是否存在优化项。

  3. 多线程并发调用多个平行数据接口时,如何使得各个线程的耗时方差尽可能小?

作者:京东零售 王江波

来源:京东云开发者社区

与一种接口依赖关系分层方案相似的内容:

一种接口依赖关系分层方案

正值618大促,各方接口的调用都会大幅度增加。通过梳理接口依赖关系来减少重复调用,对本系统而言,降低了调用数据接口时的线程占用次数,可以有效降级CPU。对调用方来说,减少了调用次数,可减少调用方的资源消耗,保障底层服务的稳定性。

SQLite vs MySQL vs PostgreSQL对比总结

开发业务系统时,是绕不开RDBMS(关系型数据库)的。虽然现在诞生了各种NoSQL的数据库,RDBMS在业务系统中的严谨和优势依然无法取代。 近几年大大小小的项目中,常用的三种RDBMS(SQLite,MySQL,Postgres)都有多次接触过,一些使用心得记录如下,供大家参考。 1. SQLit

10.0 探索API调试事件原理

本章笔者将通过`Windows`平台下自带的调试API接口实现对特定进程的动态转存功能,首先简单介绍一下关于调试事件的相关信息,调试事件的建立需要依赖于`DEBUG_EVENT`这个特有的数据结构,该结构用于向调试器报告调试事件。当一个程序发生异常事件或者被调试器附加时,就会产生对应的`DEBUG_...

设计模式之抽象工厂模式(学习笔记)

定义 抽象工厂模式是一种创建型设计模式,它提供一个接口,用于创建一系列相关或依赖的对象,而无需指定它们的具体类。抽象工厂模式将对象的创建过程抽象化,允许子类通过实现具体工厂类来定制对象的创建。 为什么使用抽象工厂模式 产品族的一致性 抽象工厂模式确保同一产品族中的对象之间的一致性。 部分遵循开闭原则

从0到1构造自定义限流组件

在系统高可用设计中,接口限流是一个非常重要环节,一方面是出于对自身服务器资源的保护,另一方面也是对依赖资源的一种保护措施。比如对于 Web 应用,我限制单机只能处理每秒 1000 次的请求,超过的部分直接返回错误给客户端。虽然这种做法损害了用户的使用体验,但是它是在极端并发下的无奈之举,是短暂的行为,因此是可以接受的。

SPI在Java中的实现与应用 | 京东物流技术团队

1 SPI的概念 API API在我们日常开发工作中是比较直观可以看到的,比如在 Spring 项目中,我们通常习惯在写 service 层代码前,添加一个接口层,对于 service 的调用一般也都是基于接口操作,通过依赖注入,可以使用接口实现类的实例。 简单形容就是这样的: 图1:API 如上图

4.Autofac依赖注入初使用

前面几篇文章只是初步搭建项目结构,那到底能否运行呢?(能是肯定的啦) 毕竟咱都NetCore了,所以依赖注入要搞起来。专业的解释我就不多说了,很多博客文章说的很详细(其实是我忘了那些术语怎么讲)。 按照我的理解来说的话就是: 省的你自己手动new了,假如你要更改接口,那不就要每个new的地方都改一下

RK3588开发笔记(四):基于定制的RK3588一体主板升级镜像

前言 方案商定制的主板,加入了360°环视算法功能,涉及到了一些库的添加,重新制作了依赖库的镜像,镜像更新的原来的板子上。 定制的板子 升级接口type-c 设计接口是type-c,需要通过type-c数据线转USB连接电脑,这里开发板都是USB对USB口的线,设计有所不同。 升级按键方法 这里的按

前端认识docker

Docker 是什么 先看看百科的定义: Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的Linux或Windows操作系统的机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。 容器引擎?镜像?容器?虚拟化

【设计模式】观察者模式

设计模式 【设计模式】工厂方法模式 【设计模式】抽象工厂模式 【设计模式】单例模式 【设计模式】策略模式 【设计模式】观察者模式 一、介绍 观察者模式是一种行为设计模式,当一个对象的状态发生改变时,依赖(观察)它的对象会接收到通知,并进行自动的更新操作。 举例:某公司发布了一款新的手机,性能很强大,