一种接口依赖关系分层方案 | 京东云技术团队
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、 个人思考
-
作为接入内部RPC、Http接口实现业务处理的项目,在使用过程中要关注调用链路上的资源复用,尤其长链路的调用,要深入考虑内存资源的利用以及对底层服务的压力。
-
要关注对外服务接口与底层数据接口的响应时差,分析调用逻辑与流程是否合理,是否存在优化项。
-
多线程并发调用多个平行数据接口时,如何使得各个线程的耗时方差尽可能小?
作者:京东零售 王江波
来源:京东云开发者社区

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
为医生打造专属数字分身!华为云联合万木健康打造医疗医学科普和患者教育数字人引擎
摘要:如今,医生出镜的视频已经成为喜闻乐见的医学科普和患者教育手段,但医生难以抽出时间拍摄、拍摄时间较长、成本较高等制作痛点也日益凸显。对此,国内首个医生AI数字人运营服务商——成都万木健康科技有限公司找到了破局之法。 本文分享自华为云社区《为医生打造专属数字分身!华为云联合万木健康打造医疗医学科普和患者教育数字人引擎》,作者: HWCloudAI。 “少吃动物脂肪、高胆固醇和糖类食品,多吃水果、蔬菜和膳食纤维的食物,对咱中老年人预防冠心病有着重要作用!”每天看一段北京安贞医院心血管科副主任医师“张琳琳”短视频,已经成为北京市民王阿姨的日常,“我和老伴都有心脑血管慢性病,张主任和她同事每天都会讲一些有用的健康知识。每期都看,按照她讲的内容改善饮食、锻炼身体,健康状况比之前好多了。” 如今,医生出镜的视频已经成为喜闻乐见的医学科普和患者教育手段,但医生难以抽出时间拍摄、拍摄时间较长、成本较高等制作痛点也日益凸显。对此,国内首个医生AI数字人运营服务商——成都万木健康科技有限公司(以下简称“万木健康”)找到了破局之法。 事实上,王阿姨喜欢的“张医生”并非真人,而是万木健康基于华为云AI数字...
- 下一篇
持续优化 XView 性能,大促弹窗搭投实践 | 京东云技术团队
背景 618 大促来了,对于业务团队来说,最重要的事情莫过于各种大促营销。如会场、直播带货、频道内营销等等。而弹窗作为一个极其重要的强触达营销工具,通常用来渲染大促氛围、引流主会场、以及通过频道活动来提升频道复访等。因此,如果能将运营的策略及想法快速转化为弹窗的内容并触达给用户,这对于提升运营效率及玩法灵活性的是极其有意义的。 此前,在 JD 主站的大多弹窗业务应用中,XView 仅仅是作为一个 h5 的容器被使用,或基于 XView 提供的一些简单的配置化能力来配置一个弹窗。作为 h5 容器的方式虽然灵活,但需要研发的参与。而基于 XView 提供的配置化能力,虽然只需产品、运营侧配置,但是配置化逻辑繁琐、学习成本高、且对于弹窗样式有局限,不够灵活。因此,此前使用的俩种方式,弊端都可以归结为:使用成本高。 为了持续优化XView 性能, XView 从配置化的方式,升级为组件化搭建的方式,以一种更加直观的可视化拖拽元素组件搭建弹窗样式,而后附加投放的配置,其目标便是解决此前所面临的几个重点问题: 学习成本高 不够灵活 需要编码 升级完成后,本次 618 的一些重要弹窗均迁移到全新的搭...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Red5直播服务器,属于Java语言的直播服务器
- CentOS7设置SWAP分区,小内存服务器的救世主
- CentOS7安装Docker,走上虚拟化容器引擎之路
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS关闭SELinux安全模块
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Linux系统CentOS6、CentOS7手动修改IP地址