Redian新闻
>
一种接口依赖关系分层方案

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

公众号新闻

来源 | OSCHINA 社区

作者 | 京东云开发者-京东零售 王江波

原文链接:https://my.oschina.net/u/4090830/blog/10085060

1、背景

到店商详迭代过程中,需要提供的对外能力越来越多,如预约日历、附近门店、为你推荐等。这其中不可避免会出现多个上层能力依赖同一个底层接口的场景。最初采用的方案是对外 API 入口进来后获取对应的能力,并发调用多项能力,由能力层调用对应的数据链路,进行业务处理。然而,随着接入功能的增多,这种情况导致了底层数据服务的重复调用,如商品配置信息,在一次 API 调用过程中重复调了 3 次,当流量增大或能力项愈多时,对底层服务的压力会成倍增加。
正值 618 大促,各方接口的调用都会大幅度增加。通过梳理接口依赖关系来减少重复调用,对本系统而言,降低了调用数据接口时的线程占用次数,可以有效降级 CPU。对调用方来说,减少了调用次数,可减少调用方的资源消耗,保障底层服务的稳定性。

原始调用方式:

2、优化

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

改进调用方式:

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

3、如何分层?

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

接口梳理分层流程如下:

第一步:构建层级结构
首先获取到能力层依赖项并遍历,然后调用生成数据节点方法。方法流程如下:构建当前节点,检测循环依赖(存在循环依赖会导致栈溢出),获取并遍历节点依赖项,递归生成子节点,存放子节点。
第二步:节点平铺
定义 Map 维护平铺结构,调用平铺方法。方法流程如下:遍历层级结构,判断当前节点是否已存在 map 中,存在时与原节点比较将层级大的节点放入(去除重复项),不存在时直接放入即可。然后处理子节点,递归调用平铺方法,处理所有节点。
第三步:分层(分组排序)
流处理平铺结构,处理层级分组,存储在 TreeMap 中维护自然排序。对应 key 中的数据节点 Set<DataNode> 需用多线程并发调用,以保证链路调用时间

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<DataNode> 需用多线程并发调用,以保证链路调用时间

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. 多线程并发调用多个平行数据接口时,如何使得各个线程的耗时方差尽可能小?

2023 源创会线下重启,基础软件技术面面谈。
🕜时间:2023 年 7 月 1 日
📍地点:广东省深圳市南山区高新南四道创维半导体设计大厦裙楼四楼·SKYWORK会议中心【国际会议中心】


【嘉宾预告】


演讲人:

张钰,粤港澳大湾区数字经济研究院开发工程师,Moonbit平台核心开发人员,编程语言理论爱好者。


演讲主题:

Moonbit 编程语言平台简介


演讲大纲:

Moonbit 是由张宏波老师带领的基础软件中心团队开发的一个专为云计算、边缘计算设计的编程语言平台,这个项目包括了面向webassembly生态设计的一款应用型编程语言及其相关完整的配套工具链,本次演讲主要对会介绍这个项目动机背景、宏观方向上的设计考量以及目前的进展进行介绍,并且会有现场demo进行一些编程语言特性的演示。


👇 立即参与



往期推荐



红帽回应“背叛”开源

基于Rust的高性能编辑器Zed,在玩一种很新的 “开源”

DragGAN开源仅一天,star数超2万,史上最强AI修图工具




这里有最新开源资讯、软件更新、技术干货等内容

点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦


微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
乌克兰两种扫雷坦克助力进攻,一种采用5对轮底盘,另一种极其稀有23款进口依维柯双拓展房车,天花板级别配置 戴德E途ProSpring循环依赖那些事儿(含Spring详细流程图)三大名牌手机苹果、三星、华为到底哪个好一种心理习惯,正在摧毁你的亲密关系,你身边很多人都有鹤冲天 记梦平和公布新生要求:分层考、标化能力强!注意:初升高暑假非常关键!优秀的代码都是如何分层的?看了直呼NB!恐童症,一种精神疾病,一种社会问题平和新生要求:分层考、标化能力强!初升高暑假非常关键!Apache Doris冷热分层技术如何实现存储成本降低70%?全球首例α-地贫患者成功脱离输血依赖,“天下无贫”还有多远?PPT千万别“平铺”了,请试下分层排版!判例译析 | 原告违法抗辩限制规则之“依赖原则”罗翔:拒绝读书是一种愚蠢,因为读书而滋生出骄傲与傲慢是一种更大的愚蠢德国还没对华摆脱依赖,中国先对德“去依赖化”了正式打通!"高度依赖美国的局面正在被改写"BIG惜败!再牛的方案也比不过一头鲸(附方案文本下载)暴雨持续洪灾肆虐,3000年来北京“城”与“水”的关系岂止一种?最新社会分层表别乱分层,PO、VO、DAO、BO、DTO、POJO 到底应该用在哪里,你知道吗?奎芯科技副总裁王晓阳:驱动云/边缘侧算力建设的高性能互联接口方案| 2023全球AI芯片峰会演讲预告随着中国减少对美乌依赖,“有重大事件发生”面试官:业务开发时,接口不能对外暴露怎么办?我有 3 种实现方案!低价开团丨桌面分层收纳架,自由叠加节省空间,桌面瞬间整洁了臨黃庭堅《諸上座帖》今晚直播!中考分流、高考分层,“被剩下的孩子”出路何在?| 全人教育系列《八角笼中》爆火:善良不是一种选择,而是一种能力冰淇淋事件后,宝马MINI又发新车!“我们不想依赖中国”……SpringBoot 接口快速开发神器(接口可视化界面实现)欧盟国家减少依赖俄罗斯石油和天然气对话戴锦华、余红苗:母女是一种有痛感的关系结婚五年,妻子变成“老妈子”:被假性亲密关系榨干的夫妻,都有同一种结局谷雨有感中国足球,战略不行
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。