赞
踩
微服务设计最核心的难题是微服务的拆分,不合理的微服务拆分不仅不能提高研发效率,反倒还使得研发效率更低,因此要讲究“小而专”的设计。“小而专”的设计意味着微服务的设计不是简单拆分,而是对设计提出了更高的要求,要“低耦合、高内聚”。那么,如何做到“低耦合、高内聚”,实现微服务的“小而专”呢?那就需要“领域驱动设计”作为方法论,来指导我们的开发。
用“领域驱动设计”是业界普遍认可的解决方案,也就是解决微服务如何拆分,以及实现微服务的高内聚与单一职责的问题。但是,领域驱动设计应当怎样进行呢?怎样从需求分析到软件设计,用正确的方式一步一步设计微服务呢?现在我们用一个在线订餐系统实战演练一下微服务的设计过程。
相信我们都使用过在线订餐系统,比如美团、大众点评、百度外卖等,具体的业务流程如下图所示:
在线订餐系统的业务流程图
当我们进入在线订餐系统时,首先看到的是各个饭店,进入每个饭店都能看到他们的菜单;
下单时,订单中就会包含我们订的是哪家饭店、菜品、数量及我们自己的配送地址;
下单后,相应的饭店就会收到该下单系统;
接着,饭店接单,然后开始准备餐食;
当饭店的餐食就绪以后,通知骑士进行派送;
最后,骑士完成了餐食的派送,订单送达,我们就愉悦地收到了订购的美味佳肴。
现在,我们要以此为背景,按照微服务架构来设计开发一个在线订餐系统。那么,我们应当如何从分析理解需求开始,一步一步通过前面讲解的领域驱动设计,最后落实到拆分微服务,把这个系统拆分出来呢?
软件开发的最大风险是需求分析,因为在这个过程中谁都说不清楚能让对方了解的需求。
在这个过程中,对于客户来说:
客户十分清楚他的业务领域知识,以及他亟待解决的业务痛点;
然而,客户不清楚技术能如何解决他的业务痛点。
因此,用户在提需求时,是在用他有限的认知,想象技术如何解决他的业务痛点。所以这样提出的业务需求往往不太靠谱,要么技术难于实现,要么并非最优的方案。
与此同时,在需求分析过程中,对于研发人员来说:
非常清楚技术以及能解决哪些业务问题,同时也清楚它是如何解决的;
然而,欠缺的是对客户所在的业务领域知识的掌握,使得无法准确理解客户的业务痛点。
这就局限了我们的设计,进而所做的系统不能完美地解决用户痛点。
因此,在需求分析的过程中,不论是客户还是我们,都不能掌握准确理解需求所需的所有知识,这就导致,不论是谁都不能准确地理解与描述软件需求。在需求分析中常常会出现,客户以为他描述清楚需求了,我们也以为我们听清楚了。但当软件开发出来以后,客户才发现这并不是他需要的软件,而我们也发现我们并没有真正理解需求。尽管如此,客户依然没有想清楚他想要什么,而我们还是不知道该怎样做,这就是软件开发之殇。
如何能够破解这个困局呢?关键的思想就在于“统一语言建模”。也就是说,以上问题的根源在于语言沟通的障碍,使得我不能理解你,而你也不能理解我。因此,解决的思路就是:
我主动学习你的语言,了解你的业务领域知识,并用你的语言与你沟通;
同时,我也主动地让你了解我的语言,了解我的业务领域知识,并用我的语言与你沟通。
回到需求分析领域,我们清楚的是技术,但不了解业务,因此,应当主动地去了解业务。那么,如何了解业务呢?找书慢慢地去学习业务吗?也不是,因为我们不是要努力成为业务领域专家,而仅仅是要掌握与要开发软件相关的业务领域知识。在业务领域漫无目的地学习,学习效率低而收效甚微。
所以,我们应当从客户那里去学习,比如询问客户,仔细聆听客户对业务的描述,在与客户的探讨中快速地学习业务。然而,在这个过程中,一个非常重要的关键就是,注意捕获客户在描述业务过程中的那些专用术语,努力学会用这些专用术语与客户探讨业务。
久而久之,用客户的语言与客户沟通,你们的沟通就会越来越顺畅,客户也会觉得你越来越专业,愿意与你沟通,并可以与你探讨越来越深的业务领域知识。当你对业务的理解越来越深刻,你就能越来越准确地理解客户的业务及痛点,并运用自己的技术专业知识,用更加合理的技术去解决用户的痛点。这样,你们的软件就会越来越专业,让用户能越来越喜欢购买和使用你们的软件,并形成长期合作关系。
以我做过的一个远程智慧诊疗数据模型为例,这是一个面向中医的数据模型。在与客户探讨需求的过程中,我们很快发现,用户在描述中医的诊疗过程中,许多术语与西医有很大的不同。
比如,他们在描述患者症状的时候,通常不用“症状”这个词,而是用“表象”。表象包括症状、体征、检测指标,是医生通过不同方式捕获患者病症的所有外部表现;同时,他们在诊断的时候也不用“疾病”这个词,而是“证候”。中医认为,证候才是患者疾病在身体中的内部根源,抓住证候,将证候的问题解决了,疾病自然就药到病除了。我们把握了这些术语后,用这些术语与业务专家进行沟通,沟通就变得异常顺利。客户会觉得我们非常专业,很懂他们,并且变得异常积极地与我们探讨需求,并很快建立了一种长期合作的关系。
同时,在这个过程中,我们一边在与客户探讨业务领域知识,一边又可以让客户参与到我们分析设计的工作中来,用客户能够理解的语言让客户清楚我们是如何设计软件的。这样,当客户有参与感以后,就会对我们的软件有更强烈的认可度,更有利于软件的推广。此外,客户参与了并理解我们是怎么做软件的,就会逐步形成一种默契。使得客户在日后提需求、探讨需求的时候,提出的需求更靠谱,避免技术无法实现的需求,使得需求质量大幅度得到提高。
在领域驱动设计之初的需求分析阶段,对需求分析的基本思路就是统一语言建模,它是我们的指导思想。但落实到具体操作层面,可以采用的实践方法是事件风暴(Event Storming)。它是一种基于工作坊的 DDD 实践方法,可以帮助我们快速发现业务领域中正在发生的事件,指导领域建模及程序开发。它是由意大利人 Alberto Brandolini 发明的一种领域驱动设计实践方法,被广泛应用于业务流程建模和需求工程。
这个方法的基本思想,就是将软件开发人员和领域专家聚集在一起,一同讨论、相互学习,即统一语言建模。但它的工作方式类似于头脑风暴,让建模过程变得更加有趣,让学习业务变得更加容易。因此,事件风暴中的“风暴”,就是运用头脑风暴会议进行领域分析建模。
那么,这里的“事件”是什么意思呢?事件即事实(Event as Fact),即在业务领域中那些已经发生的事件就是事实(fact)。过去已经发生的事件已经成为了事实就不会再更改,因此信息管理系统就可以将这些事实以信息的形式存储到数据库中,即信息就是一组事实。
说到底,一个信息管理系统的作用,就是存储这些事实,对这些事实进行管理与跟踪,进而起到提高工作效率的作用。因此,分析一个信息管理系统的业务需求,就是准确地抓住业务进行过程中那些需要存储的关键事实,并围绕着这些事实进行分析设计、领域建模,这就是“事件风暴”的精髓。
因此,实践“事件风暴”方法,就是让开发人员与领域专家坐在一起,开事件风暴会议。会议的目的就是与领域专家一起进行领域建模,而会议前的准备就是在会场准备一个大大的白板与各色的便笺纸,如下图所示:
事件风暴会议图
当开始事件风暴会议以后,通常分为这样几个步骤。
首先,在产品经理的引导下,与业务专家开始梳理当前的业务中有哪些领域事件,即已经发生并需要保存下来的那些事实。这时,是按照业务流程依次去梳理领域事件的。例如,在本案例中,整个在线订餐过程分为:已下单、已接单、已就绪、已派送和已送达,这几个领域事件。注意,领域事件是已发生的事实,因此,在命名的时候应当采用过去时态。
这里有一个十分有趣的问题值得探讨。在用户下单之前,用户首先是选餐。那么,“用户选餐”是不是领域事件呢?注意,领域事件是那些已经发生并且需要保存的重要事实。这里,“用户选餐”仅仅是一个查询操作,并不需要数据库保存,因此不能算领域事件。那么,难道这些查询功能不在需求分析的过程中吗?
注意,DDD 有自己的适用范围,它往往应用于系统增删改的业务场景中,而查询场景的分析往往不用 DDD,而是通过其他方式进行分析。分析清楚了领域事件以后,就用橘黄色便笺纸,将所有的领域事件罗列在白板上,确保领域中所有事件都已经被覆盖。
紧接着,针对每一个领域事件,项目组成员开始不断地围绕着它进行业务分析,增加各种命令与事件,进而思考与之相关的资源、外部系统与时间。例如,在本案例中,首先分析“已下单”事件,分析它触发的命令、与之相关的人与事儿,以及发生的时间。命令使用蓝色便笺,人和事儿使用黄色便笺,如下图所示:
“已下单”的领域事件分析图
“已下单”事件触发的命令是“下单”,执行者是“用户”(画一个小人作为标识),执行时间是“下单时间”。与它相关的人和事儿有“饭店”与“订单”。在此基础上进一步分析,用户关联到用户地址,饭店关联到菜单,订单关联到菜品明细。
然后,就是识别模型中可能涉及的聚合及其聚合根。第 05 讲谈到,所谓的“聚合”就是整体与部分的关系,譬如,饭店与菜单是否是聚合关系,关键看它俩的数据是如何组织的。如果菜单在设计时是独立于饭店之外的,如“宫保鸡丁”是独立于饭店的菜单,每个饭店都是在引用这条记录,那么菜单与饭店就不是聚合关系,即使删除了这个饭店,这个菜单依然存在。
但如果菜单在设计时,每个饭店都有自己独立的菜单,譬如同样是“宫保鸡丁”,饭店 A 与饭店 B 使用的都是各自不同的记录。这时,菜单在设计上就是饭店的一个部分,删除饭店就直接删除了它的所有菜单,那么菜单与饭店就是聚合关系。在这里,那个代表“整体”的就是聚合根,所有客户程序都必须要通过聚合根去访问整体中的各个部分。
通过以上分析,我们认为用户与地址、饭店与菜单、订单与菜品明细,都是聚合关系。如果是聚合关系,就在该关系上贴一张紫色便笺。
按照以上步骤,一个一个地去分析每个领域事件:
在线订餐系统的领域事件分析图
当所有的领域事件都分析完成以后,最后再站在全局对整个系统进行模块的划分,划分为多个限界上下文,并在各个限界上下文之间,定义它们的接口,规划上下文地图。
按照 DDD 的思想进行微服务设计,首先是从需求分析开始的。但 DDD 彻底改变了我们需求分析的方式,采用统一语言建模,让我们更加主动地理解业务,用客户的语言与客户探讨需求。统一语言建模是指导思想,事件风暴会议是实践方法。运用事件风暴会议与客户探讨需求、建立模型,我们能更加深入地理解需求,而客户也更有参与感。此外,事件风暴会议可以作为敏捷开发中迭代计划会议前的准备会议的一个部分。
然而,通过事件风暴会议形成的领域模型,又该如何落地到微服务的设计呢?还会遇到哪些设计与技术难题呢?下一讲将进一步讲解领域模型的微服务设计实现。
微服务的技术架构其实并不难。很多开发团队在微服务转型初期,将关注点主要放到了对微服务技术架构的学习。然而,当他们真正开始将微服务落地到具体的业务中时,才发现,真正的难题是微服务按照什么原则拆分、如何拆分,以及会面对哪些潜在风险。下面我们来一一解决。
在前面的内容中,我们多次提到过微服务的拆分原则,接下来我将为你详细讲解下。
微服务的拆分原则就是“小而专”,即微服务内高内聚、微服务间低耦合。
“微服务内高内聚”,就是单一职责原则,即每个微服务中的代码都是软件变化的一个原因。因这个原因而需要变更的代码都在这个微服务中,与其他微服务无关,那么就可以将代码修改的范围缩小到这个微服务内。把这个微服务修改好了,独立修改、独立发布,该需求就实现了。这样,微服务的优势就发挥出来了。
“微服务间低耦合”,就是说在微服务实现自身业务的过程中,如果需要执行的某些过程不是自己的职责,就应当将这些过程交给其他微服务去实现,你只需要对它的接口进行调用。譬如,“用户下单”微服务在下单过程中需要查询用户信息,但“查询用户信息”不是它的职责,而是“用户注册”微服务的职责。这样,“用户下单”微服务就不需要再去执行对用户信息的查询,而是直接调用“用户注册”微服务的接口。那么,怎样调用呢?直接调用可能会形成耦合。通过注册中心,“用户下单”微服务调用的只是在注册中心中名称叫“用户注册”的微服务。而在软件设计时,“用户注册”可以有多个实现,哪个注册到注册中心中,就调用哪个。这样,微服务之间的调用就实现了解耦。
通过 DDD 进行业务建模,再基于领域模型进行限界上下文划分,就能保证系统的设计,在限界上下文内高内聚,在限界上下文间低耦合。所以,基于限界上下文进行微服务的拆分,就能保证微服务设计的高质量。同时,通过对上下文地图的分析,就能理清微服务之间的接口调用关系,从而协调多个开发团队协同开发。
正如第 06 讲中谈到,领域模型的绘制,不是将整个系统的领域对象都绘制在一张大图上,那样绘制很费劲,阅读也很费劲,不利于相互的交流。因此,领域建模就是将一个系统划分成了多个子域,每个子域都是一个独立的业务场景。围绕着这个业务场景进行分析建模,该业务场景会涉及许多领域对象,而这些领域对象又可能与其他子域的对象进行关联。这样,每个子域的实现就是“限界上下文”,而它们之间的关联关系就是“上下文地图”。
在本案例中,围绕着领域事件“已下单”进行分析。它属于“用户下单”这个限界上下文,但与之相关的“用户”及其“地址”来源于“用户注册”这个限界上下文,与之相关的“饭店”及其“菜单”来源于“饭店管理”这个限界上下文。因此,在这个业务场景中,“用户下单”限界上下文属于“主题域”,而“用户注册”与“饭店管理”限界上下文属于“支撑域”。同理,围绕着本案例的各个领域事件进行了如下一些设计:
“已下单”的限界上下文分析图
通过这样的设计,就能将“用户下单”限界上下文的范围,与之相关的上下文地图以及如何接口,分析清楚了。有了这些设计,就可以按照限界上下文进行微服务拆分。按照这样的设计拆分的微服务,所有与用户下单相关的需求变更都在“用户下单”微服务中实现。但是,订单在读取用户信息的时候,不是直接去 join 用户信息表,而是调用“用户注册”微服务的接口。这样,当用户信息发生变更时,与“用户下单”微服务无关,只需要在“用户注册”微服务中独立开发、独立升级,从而使系统维护的成本得到降低。
“已接单”与“已就绪”的限界上下文分析图
同样,如上图所示,我们围绕着“已接单”与“已就绪”的限界上下文进行了分析,并将它们都划分到“饭店接单”限界上下文中,后面就会设计成“饭店接单”微服务。这些场景的主题域就是“饭店接单”限界上下文,而与之相关的支撑域就是“用户注册”与“用户下单”限界上下文。通过这些设计,不仅合理划分了微服务的范围,也明确了微服务之间的接口,实现了微服务内的高内聚与微服务间的低耦合。
按照 07 讲所讲到的领域模型设计,以及基于该模型的限界上下文划分,将整个系统划分为了“用户下单”“饭店接单”“骑士派送”等微服务。但是,在设计实现的时候,还有一个设计难题,即领域事件该如何通知。譬如,当用户在“用户下单”微服务中下单,那么会在该微服务中形成一个订单;但是,“饭店接单”是另外一个微服务,它必须要及时获得已下单的订单信息,才能执行接单。那么,如何通知“饭店接单”微服务已经有新的订单。诚然,可以让“饭店接单”微服务按照一定的周期不断地去查询“用户下单”微服务中已下单的订单信息。然而,这样的设计,不仅会加大“用户下单”与“饭店接单”微服务的系统负载,形成资源的浪费,还会带来这两个微服务之间的耦合,不利于之后的维护。因此,最有效的方式就是通过消息队列,实现领域事件在微服务间的通知。
在线订餐系统的领域事件通知
如上图所示,具体的设计就是,当“用户下单”微服务在完成下单并保存订单以后,将该订单做成一个消息发送到消息队列中;这时,“饭店接单”微服务就会有一个守护进程不断监听消息队列;一旦有消息就会触发接收消息,并向饭店发送“接收订单”的通知。在这样的设计中:
“用户下单”微服务只负责发送消息,至于谁会接收并处理这些消息,与“用户下单”微服务无关;
“饭店接单”微服务只负责接收消息,至于谁发送的这个消息,与“饭店接单”微服务无关。
这样的设计就实现了微服务之间的解耦,使得日后变更的成本降低。同样,饭店餐食就绪以后,也是通过消息队列通知“骑士接单”。在整个微服务系统中,微服务与微服务之间的领域事件通知会经常存在,所以最好在架构设计中将这个机制下沉到技术中台中。
通过第 07 讲所讲到的一系列领域驱动设计:
首先通过事件风暴会议进行领域建模;
接着基于领域建模进行限界上下文的设计。
所有这些设计都是为了指导最终微服务的设计。
在 DDD 指导微服务设计的过程中:
首先按照限界上下文进行微服务的拆分,按照上下文地图定义各微服务之间的接口与调用关系;
在此基础上,通过限界上下文的划分,将领域模型划分到多个问题子域,每个子域都有一个领域模型的设计;
这样,按照各子域的领域模型,基于充血模型与贫血模型设计各个微服务的业务领域层,即各自的 Service、Entity 与 Value Object;
同时,按照领域模型设计各个微服务的数据库。
最后,将以上的设计最终落实到微服务之间的调用、领域事件的通知,以及前端微服务的设计。如下图所示:
在线订餐系统的微服务设计
这里可以看到,前端微服务与后端微服务的设计是不一致的。前面讲的都是后端微服务的设计,而前端微服务的设计与用户 UI 是密切关联的,因此通过不同角色的规划,将前端微服务划分为用户 App、饭店 Web 与骑士 App。在用户 App 中,所有面对用户的诸如“用户注册”“用户下单”“用户选购”等功能都设计在用户 App 中。它相当于一个聚合服务,用于接收用户请求:
“用户注册”时,调用“用户注册”微服务;
“用户选购”时,查询“饭店管理”微服务;
“用户下单”时,调用“用户下单”微服务。
采用 DDD 进行需求的分析建模,可以帮助微服务的设计质量提高,实现“低耦合、高内聚”,进而充分发挥微服务的优势。然而,在微服务的设计实现还要解决诸多的难题。本讲一一拆解了微服务设计实现的这些难题,及其解决思路。然而,要更加完美地解决以上问题,不是让每个微服务都去见招拆招,而是应当有一个微服务的技术中台统一去解决。这些方面的设计将在后面微服务技术中台建设的相关章节进行讲解。
下一讲我们将演练在以上领域模型与微服务设计的基础上,如何落实每一个微服务的设计,以及可能面临的设计难题。
自本专栏上线以来,有许多小伙伴跟我交流了很多相关的 DDD 知识。我发现,当大家看到贫血模型、充血模型、策略模式、装饰者模式时,发出这样的感慨:“难道这就是 DDD 吗?和我们平时的开发没有什么不同啊。”殊不知,其实你还没有 Get 到 DDD 的真谛 。
什么是 DDD 的真谛呢?那就是领域建模,它改变了我们过去对软件开发的认知。如图 1 所示,DDD 的精髓是:
首先深刻理解业务;
然后将我们对业务的理解绘制成领域模型;
再通过领域模型指导数据库和程序的设计。
图 1 领域驱动设计的真谛
过去,我们认为软件就是,用户怎么提需求,软件就怎么开发。这种开发模式使得我们对需求的认知浅薄,不得不随着用户的需求变动反复地改来改去,导致我们很累而用户还不满意,软件研发风险巨大。
正是 DDD 改变了这一切,它要求我们更加主动地去理解业务,掌握业务领域知识。这样,我们对业务的理解越深刻,开发出来的产品就越专业,那么客户就越喜欢购买和使用我们的产品。
然而,真实世界是非常复杂的,这就决定了我们不可能一开始就深刻理解业务。起初,我们对业务的理解浅薄,基于它做出来的领域模型也是浅薄的,导致最后开发出来的软件虽然也能用,但用户并不一定满意。然而,如果我们不断地与客户沟通,深入地理解业务,听取他们的意见,我们对业务的理解就会越来越深刻、越来越准确。再结合我们的专业技术知识,就能够理解我们的软件需要解决客户的什么问题,怎样做才是最优,怎样做才让客户感觉好用。
这时就不再是客户提需求了,而是我们主动地提需求、主动地改进功能,去解决客户的痛点,这样做的效果是,客户会感觉“不知道为什么,我就觉得你们的软件好用,用着很顺手”。这时,不但客户不会再改来改去,而且我们的软件做得也越来越专业,越来越有市场竞争力,这才是 DDD 的真谛。
这里有个问题,如果我们对业务理解不深刻就会影响到产品,那么能不能一开始就对业务理解得非常深刻呢?这几乎是不可能的。我们经常说,做事不能仅凭一腔热血,一定要符合自然规律。其实软件的设计开发过程也是这样。
在最开始你对业务理解比较粗略的时候,就从主要流程开始领域建模。
接着,不断往领域模型中加东西。随着功能一个一个地添加,领域模型也变得越来越丰富、越来越完善。每次添加新功能的时候,运用“两顶帽子”的方式先重构再加新功能,不断地完善每个设计。
这样,领域模型就像小树一样一点儿一点儿成长,最后完成所有的功能。
这样的设计过程叫“小步快跑”。采用小步快跑的设计方法,一开始不用思考那么多问题,从简单问题开始逐步深入,设计难度就降低了。同时,系统始终是处于变更中,使设计更加易于变更。
回到 08 讲微服务设计部分,当在线订餐系统完成了事件风暴的分析以后,接着应当怎样设计呢?通过划分限界上下文,已经将系统划分为了“用户注册”“用户下单”“饭店接单”“骑士派送”与“饭店管理”等几个限界上下文,这样的划分也是后端微服务的划分。紧接着,就开始为每一个限界上下文进行领域建模。
首先,从“用户下单”上下文开始。通过业务领域分析,绘制出了如图 2 所示的领域模型,该模型的核心是“订单”,通过“订单”关联了用户与用户地址。一个订单有多个菜品明细,而每个菜品明细都对应了一个菜单,每个菜单隶属于一个饭店。此外,一个订单还关联了它的支付与发票。起初,它们的属性和方法没有那么全面,随着设计的不断深入,不断地细化与完善模型。
在这样的基础上开始划分限界上下文,用户与用户地址属于“用户注册”上下文,饭店与菜单属于“饭店管理”上下文。它们对于“用户下单”上下文来说都是支撑域,即给“用户下单”上下文提供接口调用的。真正属于“用户下单”上下文的,就只有订单、菜品明细、支付、发票这几个类,它们最终形成了“用户下单”微服务及其数据库设计。由于用户姓名、地址、电话等信息,都在“用户注册”上下文中,每次都需要远程接口调用来获得。这时就需要从系统优化的角度,适当将它们冗余到“订单”领域对象中,以提升查询效率。同样,“菜品名称”也进行了冗余,设计更新如图 3 所示:
完成了“用户下单”上下文以后,开始设计“饭店接单”上下文,设计如图 4 所示。上一讲谈到,“用户下单”微服务通过事件通知机制,将订单以消息的形式发送给“饭店接单”微服务。具体来说,就是将订单与菜品明细发送给“饭店接单”上下文。“饭店接单”上下文会将它们存储在自己的数据库中,并在此基础上增加“饭店接单”类,它与订单是一对一的关系。
同样的思路,通过领域事件通知“骑士派送”上下文,完成“骑士派送”的领域建模。
通过以上设计,就将上一讲的微服务拆分,进一步落实到每一个微服务的设计。紧接着,将每一个微服务的设计,按照第 03 讲的思路落实数据库设计,按照第 04 讲的思路落实贫血模型与充血模型的设计。
特别值得注意的是,订单与菜品明细是一对聚合。过去按照贫血模型的设计,分别为它们设计订单值对象、Service 与 Dao,菜品明细值对象、Service 与 Dao;现在按照充血模型的设计,只有订单领域对象、Service、仓库、工厂与菜品明细包含在订单对象中,而订单 Dao 被包含在订单仓库中。贫血模型与充血模型在设计上有明显的差别。关于聚合的实现,下一讲再详细探讨。
前面讲了,我们不可能一步到位深刻理解业务,它是一个逐步深入的过程。譬如,在设计“用户地址”时,起初没有“联系人”与“手机号”,因为通过关联用户就可以获得。然而,随着业务的不断深入,我们发现,当用户下单的时候,最终派送的不一定是给他本人,可能是另一个人,这是起初没有想到的真实业务场景。为此,在“用户地址”中果断增加了“联系人”与“手机号”,问题得到解决。
此外,如果用户下单以后又需要取消订单,这样的业务场景又该如何设计呢?通过与客户的沟通,确定了该业务的需求:
如果饭店还未接单,可以直接取消;
如果饭店已经接单了,需要经过饭店的确认方可取消;
如果饭店已经就绪了,就不可取消了。
这样,首先需要“饭店接单”上下文提供一个状态查询的接口,以及饭店确认取消的接口。接着,订单取消以后需要记录一个取消时间,并形成一个“订单取消”领域事件,通知“饭店接单”上下文。为此,“用户下单”上下文需要在订单中增加一个“取消时间”。
然而,当“用户下单”上下文对“订单”对象更新以后,“饭店接单”与“骑士派送”上下文是否也要跟着更新呢?前面提到,对微服务的设计,是希望:
每次变更的时候尽可能只更新一个微服务,以降低微服务的维护成本;
即使不能,也应当尽可能缩小更新的范围。
增加“取消时间”这个字段,对“饭店接单”上下文是有意义的,它的相应变更无可厚非。但对于“骑士派送”上下文来说,“取消时间”对它没有一毛钱关系,因此不希望对它进行更新。微服务间的调用是基于 RESTful 的接口调用,参数是通过 JSON 对象传递,是一种松耦合调用。因此,在“饭店接单”与“骑士派送”上下文中,即使“订单”对象的数据结构不一致,也不影响它们的调用。因此,在“骑士派送”上下文不需要更新,更新范围就缩小了,维护成本降低了。
在完成了以上设计以后,还有一个难题就是订单状态的跟踪。
订单状态的跟踪
当用户下单后,往往会不断地跟踪订单状态是“已下单”“已接单”“已就绪”还是“已派送”。然而,这些状态信息被分散到了各个微服务中,就不可能在“用户下单”上下文中实现了。如何从这些微服务中采集订单的状态信息,又可以保持微服务间的松耦合呢?解决思路还是领域事件的通知。
通过消息队列,每个微服务在执行完某个领域事件的操作以后,就将领域事件封装成消息发送到消息队列中。比如,“用户下单”微服务在完成用户下单以后,将下单事件放到消息队列中。这样,不仅“饭店接单”微服务可以接收这个消息,完成后续的接单操作;而且“订单查询”微服务也可以接收这个消息,实现订单的跟踪。如图 5 所示。
图 5 订单状态的跟踪图
通过领域事件的通知与消息队列的设计,使微服务间调用的设计松耦合,“订单查询”微服务可以像外挂一样采集各种订单状态,同时不影响原有的微服务设计,使得微服务之间实现解耦,降低系统维护的成本。而“订单查询”微服务通过冗余,将“下单时间”“取消时间”“接单时间”“就绪时间”等订单在不同状态下的时间,以及其他相关信息,都保存到订单表中,甚至增加一个“订单状态”记录当前状态,并增加 Redis 缓存的功能。这样的设计就保障了订单跟踪查询的高效。要知道,面对大数据的高效查询,通常都是通过冗余来实现的。
DDD 的真谛是领域建模,即深入理解业务。只有深入理解业务,将对业务的深入理解设计到领域模型中,设计出来的软件才更加专业,让用户的使用更满意。因此,基于每个限界上下文进行领域建模,不断地将每个功能加入模型中,落地每个微服务的设计。当业务越来越复杂,理解越来越深入的时候,适时地调整原有的模型,就能适应新的功能,使设计始终高质量。
下一讲将现有的微服务设计进一步落实技术实现上,做“去中心化的数据管理”,解决跨库查询等技术难题。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。