架构实战案例解析_09_08_可复用架构案例一如何设计一个基础服务
你好,我是王庆友。
在上一讲中呢,我们说了在架构设计中要实现业务上的复用。
一个比较可行的做法呢是把基础业务分装成共享服务,供上层所有应用的调用。
所以今天呢我就和你聊一聊如何从头开始落地这样一个典型的共享服务。
我们知道落地和微服务其实并不困难,但要实现一个能够高度复用的共享服务并不容易。
在落地过程中啊,经常会有一系列的问题困扰着我们。
比如说我们事先没有对服务的边界进行很好的划分,结果在落地的过程中,大家反复争论,具体功能的归属。
还有呢由于对业务的了解不够深入,我们要么设计不足,导致同一个服务有很多的版本,要么服务过度设计,实现了一堆永远用不上的功能。
对于落地一个共享服务来说,核心是服务边界的划分和功能的抽象设计。
服务边界确定了这个服务应该做什么?抽象设计确定了这个服务应该怎么做。
接下来呢我就一个实际的订单服务的例子为你详细讲解如何重点解决这两个问题。
这样你可以通过具体的案例深入的理解,如何落地一个共享服务,实现业务能力的复用,不同企业的订单业务是不一样的。
所以这里我先介绍一下这个订单的业务场景。
这是个o to o的交易业务。
订单有两个来源,一个是自由小程序或者apple过来的订单,还有一个是外卖平台过来的订单。
然后所有这些线上订单都会同步到门店的收银系统进行接单和进一步的处理。
这里呢我放一张订单的业务架构图,你可以到文稿中看一下。
在这里订单服务和四个应用直接打交道。
首先是小程序的服务端,它会调用订单服务,落地自有的订单。
然后有一个外卖同步程序,它会接收三方外卖平台的订单,然后调用订单服务落地订单。
那还有一个是pos的同步程序,它通过订单服务拉取订单,并推送给商户内部的收银系统。
最后一个是订单的管理后台,它通过订单服务查询和修改订单。
接下来我们就具体看一下如何从头开始落地这么一个订单服务。
首先我们要确定这个服务的边界,这个呢是服务内部设计的前提。
那划分边界时呢,你需要对相关的业务场景有充分的了解,并且在一定程度上能够预测潜在的一些需求。
在上一讲中呢,我也和你分享了划分边界一些比较实用的原则和做法。
你可以对照学习一下。
那根据我们对业务场景的分析,这个订单服务它需要负责三个方面的功能。
首先呢是订单的基本信息管理,这块呢是对应订单基础信息的增删改查功能,包括下单用户、下单、商品收货人、收货地址、收货时间、订单状态取餐码等等。
另外呢你需要注意的是,这里有多个下单渠道。
那除了这些通用的订单信息,每个渠道还有特定的渠道相关的信息,比如堂食的订单,它有取餐码,那外卖的订单有收货人和收货地址等等,这个呢都需要在我们的数据模型中给出定义。
第二部分的功能就是订单的优惠管理功能,这对应的是订单小票的相关信息。
那我们知道从一开始的商品的原始金额到最后需要用户实际支付的金额,中间啊有一系列的折扣和减免,这些呢都属于订单信息的一部分。
这些信息我们需要展示给用户看,那如果后续要进行订单成本的分摊,也需要用到它。
那第三部分呢是订单的生命周期管理,这个对应的是订单的状态变化管理。
那我们知道从不同渠道过来的订单,它的状态变化过程是不一样的。
那不同行业的订单它的变化过程也是不同的。
所以订单服务的状态要做到通用这些支持各种可能的状态定义以及订单状态的转换过程。
这个呢是订单服务设计的难点,我在后面会重点介绍。
好了,现在我们已经给出了订单服务的功能。
为了更好的定义边界,在实践中你还需要澄清哪这功能不属于这个服务,这样可以避免后续很多的争论。
所以在这里我进一步给出了订单服务不包括的功能。
那你在划分自己的服务边,限时最好也能够明确给出来。
首先作为基础服务,订单服务不会主动调用其他服务。
比如说你想了解订单的用户详情、商品详情等等,这应该由上层应用通过调用相应的服务来实现。
然后呢,和订单信息组装在一起,而不是在订单服务内部直接调用其他服务,否则会导致基础服务之间相互依赖,职责模糊。
那如果说这个信息整合的场景它非常通用。
这个呢我们可以通过创建一个在基础服务之上的聚额服务,来实现把订单信息、用户信息和商品信息整合在一起。
第二,订单服务不负责和第三方系统的集成。
在这里你可以看到订单需要在我们的订单服务和三方外卖平台以及收银系统之间进行同步。
这些同步功能呢都是针对第三方系统定制的,它不具有通用性,而我们的订单服务作为基础服务,需要具备通用性。
因此,这些和外部系统对接的功能不会在订单服务的内部实现,而是由额外的同步程序来实现。
第三,订单服务不提供优惠计算或者分摊逻辑。
订单服务不负责,具体的优惠计算,只提供优惠结果的存储和查询。
这些呢用于还原订单的费用组成,优惠的具体计算过程,一般有专门的促销系序来实现啊成本的分担,一般由后续的财务系统来负责。
这个我们在上一讲中已经说过了,这里就不详细解释了。
那最后呢,订单服务不提供履单详情,不负责详细物流信息的存储。
比如说订单已经发送到上海,订单已经到达某某快递站等等。
这些信息订单服务不负责提供这些详细信息,这些呢都属于后续履单系统的职责。
订单服务呢可以承受一些外部系统的单据号码,比如配送单号,这样呢方便上层应用,通过订单记录和配送系统进行关联,获取配送的详细信息。
但对订单服务来说,他只负责存储,不负责这个数据的进一步解释。
到这里为止,你可以看到我们通过从正反两个方面说明订单服务的职责就得到了一个边界,很清晰,职责很聚焦的订单服务。
那所有人对他的职责认识呢是一致的,尽可能的避免了后续的争论。
好,我们现在确定了这个订单服务要做什么?那接下来我们要解决的就是订单服务内部怎么做的问题了。
那作为共享服务,我们要保证订单服务功能上的通用性,就需要对内部数据模型和外部接口进行良好的抽象设计。
那对于订单的数据模型来说,订单要存储哪些信息?通过上面的介绍已经比较明确了,但对于如何管理订单的状态,情况就比较复杂。
那我们知道如果我们的订单服务针对一个具体的项目,那无论他的订单状态有多么的复杂,我们都可以事先精确的定义出来。
但不同的行业,甚至不同的企业,他们对于订单状态管理都是不一样的。
那订单服务作为共享服务,它必须要满足不同项目的订单状态管理。
所以如何解决这个问题?这里呢我有两个思路供你参考。
第一个呢是开放订单状态定义在订单服务内部,我们事先不限定订单有哪些状态。
那每个项目都可以自己定义哪些订单状态。
那服务的状态方呢,可以在订单服务的接口里传递任意的状态值。
订单服务只负责保存状态,数据不负责解释,具体的状态值,那不负责任何的规则校验,它允许订单从一个状态转换为其他任意的状态。
那这样的设计呢,理论上可以满足各种状态定义,满足各种状态之间的变化。
那这样做有很大的问题,但这里呢订单状态完全由外部部负责管理,那么对上层应用来说,它的负担会很重,不但要负责定义有哪些状态,那且还维护状态的的换状态。
那那一不小心订单可能状态态非法的变变状状态b导致业务出问题。
那另外一个做法呢就是订单服务和应用,共同管理订单状态,对订单状态、管理服务和应用各自承担一部分职责。
那我们看一下具体如何实现。
那我们知道,无论订单状态变化是如何的复杂,我们总是可以定义一个订单有哪些基本的状态,包括这些基本状态之间是如何变化的。
比如订单一开始都是用户下单后进入待支付状态,支付完成后变成一个有效的订单,然后由商家进行接单,制作完成后进行发货配送等等。
订单最终的状态要么是完成,要么是取消,这些呢是订单的基本状态,我们称之为主状态。
这些状态由订单服务负责定义,包括这些主状态之间的转换规则,比如说已经完成的订单,我们不能取消它。
那这些主状态数量呢是比较有限的,状态之间的变化关系也是比较明确的。
这个主状态,我们对大量的现有业务场景进行总结和抽象,是完全可以定义出来的。
在这个订单服务例子里呢,我们定义了以下的订单状态机,包括有哪些主状态与他们的转化关系。
那你可以到文稿中具体看一下。
那订单除了主状态,它还有子状态。
那什么是子状态呢?比如说一个订单,它处于配送中,那实际情况呢,可能是仓库已经发货了,或者说货已经到配送站或者呢快递也已经从配送站拿货正在送货过程中。
那么在这些情况,下订单的主状态都是配送中,它的子状态呢就是刚才说的细化的这几种情况。
那子状态有哪些具体的取值,不同的项目呢是不一样的,这个就开放给各个应用来定义。
所以订单服务数据模型里它有两个字段,其中的主状态有订单服务负责管理,包括状态之间的变化规则。
而子状态由上层应用来定义,应用自己来管理子状态的变化规则。
比如一个配送中的订单,它的子状态可以由仓库与发货变为快递员,正在送货中。
现在我们可以对比一下这两种订单状态的设计思路。
第一种方案呢,我们不对订单状态进行任何的管理,只是把订单的状态作为一个简单的属性进行存储。
只支持订单状态,简单的增删改查功能。
那我们知道订单状态是订单业务的核心体现。
这样的订单服务呢它是没有灵魂的,也失去了大部分业务互动的价值。
第二种方案呢服务和应用共同管理订单的状态。
对订单服务来说,它是抓大放小,通过主状态的管理,把控住订单的核心业务规则。
同时,把子状态开放给应用进行管理,为具体的业务场景提供了灵活性。
那这个主状态子上的结合起来呢,就可以满足不同行业、不同企业的订单状态管理需求。
那说完了订单的状态管理,接下来我们从调研方怎么使用服务的角度来看一下,订单服务的外部接口是怎么设计的。
外部系统和订单服务有两种交互方式,包括同步的接口调用和异步的交息通知。
首先我们看一下同步的服务接口调用。
为了方便外部调用方,我们在服务接口命名的时候,一定要规范和统一接口。
要做到看到它的名字,就大致知道它的功能,方便调用着快速找到所需要的接口,并且我们还要提供接口,具体的请求和响应压力帮出说明。
那具体的接口设计规范我就不具体展开了,每个公司都有明确的规范要求。
这里我就说一下常见的查询结口是怎么设计的。
一个订单有很多字段,每调调研方要查询信息可可能不相同。
同这些不同字段之间的组合方式有很多,我们不可能全部支持。
那么我们怎么设计查询接口来满足各种场景的需求呢?一般来说,我们可以根据返回字段数量的多少,提供三个不同力度的查询接口,来满足多样化的需求。
第一个呢是粗力度的接口,支付返回订单最基本的七到八个字段,比如说订单编号、订单状态、订单金额下单用户、下单时间等等。
第二个是中力度的接口,我们返回订单比较常用的十几个字段。
那第三个呢是细力度的接口,返回订单的详细信息。
那这样子呢,不同的查询需求就可以根据要返回信息的详细程度,选择合适的接口。
啊,通过这种方式,我们兼顾了要定义的接口数量和查询的性能。
那说完了同步的方式,我们接着说下异步的消息通知。
那订单服务呢除了提供同步的接口调用外,还针对每次订单信息的变化提供异步的消息通知。
这样感兴趣的外部系统都可以通过接收消息第一时间感知订单的变化。
那按照消息详细程度的不同,订单消息可以分为胖消息和瘦消息。
那顾名思义,胖消息它包含了尽可能多的字段,但它的传输效率比较低啊,瘦消息只是包含最基本的字段,传输效率比较高。
那如果外部系统需要更多的信息,他们可以通过进一步调用订单服务的接口来获取。
那在我们的订单服务的例子中呢,如果是订单状态的变化,我们只需要提供订单号变化前的状态和变化后的状态就可以。
因此主要是以瘦消息为主。
那如果是创建了新的订单,由为订单的字段比较多,所以使用胖消息避免外部系统进一步调用订单服务的接口。
你呢在实践中可以根据实际情况,在消息的数据量和消费者处理消息的复杂度之间做平衡。
那前面我们说了,订单服务不会主动调用外部系统接口,这里的异步消息通知就可以很好的保证外部系统及时感知订单的任何变化,同时避免订单服务和外部系统直接耦合。
好了,最后我总结一下今天所讲的内容。
要想打造一个能够高度复用的共享服务,重要的是两点清晰的边线划分和内部的抽象设计。
那今天我通过一个实际的订单服务的例子,帮助你理解如何清晰的定义服务的边界,以及如何通过抽象设计保证服务的通用性。
你在实践中一定要深入分析业务场景识别,真正的挑战在哪里?避免设计的简单化或者过度复杂化。
那通过今天的讲解,相信你在前一篇理论内容的基础上,对如何打造一个共享服务有了更深入的体会。
希望你在工作中不断的去实践,真正的掌握这些技能。
最后给你留一道思考题,在落地共享服务的时候,你都碰到过哪些挑战,都是怎么解决的那欢迎你在留言,教大家分享你的答案。
如果你在学习的实践过程中有什么问题或者思考,也欢迎给我留言,我们一起讨论,感谢收听,我们下一期再见。