阅读说明:
- 如果有排版格式问题,请移步 www.yuque.com/mrhuang-ire... 《业务单系统架构设计心得(二):流程编排》,选择宽屏模式效果更佳。
- 本文为原创文章,转发请注明出处。如果觉得文章不错,请点赞、收藏、关注一下,您的认可是我写作的动力。
上一节《业务单系统架构设计心得(一)》中,讲到了业务系统分层。本文对流程层继续深究,讨论流程层沉淀出通用组件。
业务复用编排
会用到编排的,大多是有业务复用,流程层根据业务需求串联起不同的子服务,上一节已经比较详细介绍了过程,这里不再详细展开叙述。这里补充一下串联方式,有以下三种方式:
- 硬编码:在系统中代码写死要串联的服务;
- 配置式:通过配置的方式进行串联,这种好处是比较灵活;
- 注解式:java中的注解能够做到标记的作用,通过自定义注解实现串联;
异步查询编排
在实际业务中,一个app页面可能会加载很多信息,以美团外卖商家端为例: 因此,服务端必须一次性返回订单的所有信息,包含订单主信息、商品、结算、配送、用户信息、骑手信息、餐损、退款、客服赔付等。企业通常采用微服务架构,这些不同的模块通常部署在不同的机器上,因而需要发起多次rpc请求数据。如果继续采用同步加载的方式,这将带来一个新的问题,对外请求太多拖垮接口响应时间,商家的用户体验太差。
遇到性能的第一问题是使用线程池,使用多个线程并行处理任务,能够明显改善接口的性能。但是,这不是完美的,使用线程池虽然可以解决异步的问题,但是没有解决组合的问题。我们来对实际业务流程抽象,将流程中每个小步骤看做节点,那么业务可以简化成如下流程。
该简化流程涵盖了实际业务中的可能出现的各种组合情况:
- 零依赖:节点向下没有依赖,图中有节点CF1、 CF2、CF3。
- 一元依赖:节点向下有一个依赖项,图中有节点CF4、CF6。
- 两元或者多元依赖:节点向下有两个或者多个依赖项,图中两元依赖节点有CF5,三元依赖节点有CF7。
- 两元或者多元被依赖:节点被向上两个或者多个节点依赖,图中有CF3。
显然,对于前后依赖的问题,java线程池没有很好的解决,而这是业务流程中经常会出现的场景。java8之后,新加了CompletableFuture,CompletableFuture就很好的解决了执行依赖的问题。
异步编排执行框架
本节将讲述基于CompletableFuture搭建异步编排执行框架,示例仍以上图业务依赖简化版为例。
1.构建依赖关系
构建依赖关系前,需要将流程进行拆分多个节点,确定节点两两之间的依赖关系。
2.生成依赖树
根据上一步两个节点之间的依赖关系,构建一颗完整的依赖树,这里需要记录下父节点与其依赖的所有子节点的关系。最下层没有依赖的节点是叶子节点,图中CF1, CF2, CF3就是叶子节点。
3.节点执行
节点执行分为两步。第一步,找到叶子节点,叶子节点因为没有依赖,可以直接执行。
4.节点唤醒
在第三步中,被依赖节点执行完时,需要唤醒上层节点,上层节点检查依赖的所有子节点是否都已完成,如果都完成,则执行本节点,直至最上层的节点也完成,整个流程结束。这其中有个关键的一点是,子节点与父节点之间如何唤醒,到这里能够想到通过注册回调事件来实现了。这里使用CompletableFuture来实现,关键代码如下:
java
//节点1
CompletableFuture<String> childFuture1 = new CompletableFuture<>();
//节点2
CompletableFuture<String> childFuture2 = new CompletableFuture<>();
//节点3
CompletableFuture<String> childFuture3 = new CompletableFuture<>();
////节点4, 依赖节点1,2,3, 注册回调事件
CompletableFuture<String> childFuture4 = new CompletableFuture<>();
CompletableFuture.allOf(childFuture1, childFuture2, childFuture3)
// 当node节点所有的父节点结果都构建完成时唤醒node节点
.whenComplete((Void aVoid, Throwable throwable) -> {
{} //节点4的逻辑
childFuture4.complete("");
});
{}//这里执行节点1的逻辑
childFuture1.complete("");
{} //这里执行节点1的逻辑
childFuture2.complete("");
{} //这里执行节点1的逻辑
childFuture3.complete("");
使用异步编排的几个注意事项
1.CompletableFuture使用自定义线程池。手动传递线程池参数的好处是可以更方便的调节参数,并且可以给不同的业务分配不同的线程池,以求资源隔离,减少不同业务之间的干扰。 2.使用自定义线程池需要传递调用线程的上下文内容。子线程启动时,继承调用线程的上下文内容,比如调用线程的traceId等等,以及业务上塞入的线程ThreadLocal变量。子线程退出时,需要清理掉继承的信息。 3.线程池数量不能过多。线程过多时,同时处理太多请求,实际业务上通过rpc组件请求外部服务,请求将达到io线程上。如果是存在io耗时的请求,将导致io线程一直被占用,影响整个服务的相应,并没有达到异步提高性能的效果。