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、 个人思考
-
作为接入内部RPC、Http接口实现事务处理的项目,在运用过程中要重视调用链路上的资源复用,特别长链路的调用,要深入考虑内存资源的使用以及对底层服务的压力。
-
要重视对外服务接口与底层数据接口的呼应时差,剖析调用逻辑与流程是否合理,是否存在优化项。
-
多线程并发调用多个平行数据接口时,怎么使得各个线程的耗时方差尽可能小?
作者:京东零售 王江波
来源:京东云开发者社区