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

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 ancestors; /** * 子节点 */ private list children;}  
2 获取能力层的接口依赖,并生成对应的数据节点
q1:生成节点时如何维护层级?
a1:从能力层依赖开始,层级从 1 递加。每获取一次底层依赖,底层依赖所生成的节点层级即父节点层级 + 1。
/** * 构建层级结构 * * @param handlers 接口依赖 * @return 数据节点集 */private list buildlevel(set handlers) { list 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 ancestors, string parent) { abstractinfrahandler abstractinfrahandler = abstractinfrahandlermap.get(name); set 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 datanodes, map 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 processlevel(map 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 buildsirector(set infradependencyhandlers, threadpoolexecutor workerexecutor) { sirector sirector = new sirector(workerexecutor); long start = system.currenttimemillis(); // 依赖顺序与执行顺序相反 treemap levelnodes; treemap 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 接口实现业务处理的项目,在使用过程中要关注调用链路上的资源复用,尤其长链路的调用,要深入考虑内存资源的利用以及对底层服务的压力。
要关注对外服务接口与底层数据接口的响应时差,分析调用逻辑与流程是否合理,是否存在优化项。
多线程并发调用多个平行数据接口时,如何使得各个线程的耗时方差尽可能小?


FPGA管脚的调整技巧
三星电子可能砍掉Galaxy Note,专注于生产Galaxy S
智能头盔设计,给您的安全护航新体验!
通过网络管理自己的电脑
realme计划在3月份内将所有机型都升级到realme UI
一种接口依赖关系分层方案
Verizon拟通过5G技术将FiOS光纤服务拓展至全美
智慧农业物联网对农业发展的贡献都有哪些
机器视觉应用的优势体现
openharmony与鸿蒙区别
详解RFID项目实施流程
将来能不能冲着 Google Home 喊“Hey,Siri”?
科普激光驱动白光光源(LDLS)
立体声编码器原理图
鸿蒙上实现直接安装hap
2030年,中国能否成为人工智能的世界领导者?
富士通FRAM是断电情况下也能保留数据非易失性的存储器
氧化锌避雷器测试仪的使用方法
详细讲解USB3.0 Micro-B接口连接器知识!
基于零维材料的光电探测器原子结构