2022- 如何设计一个复杂的业务系统?从对领域设计、云原生、微服务、中台的理解开始
如何设计一个复杂的业务系统?从对领域设计、云原生、微服务、中台的理解开始
01 如何解决复杂业务设计
软件架构设计本身就是一个复杂的事情,但其实业界已有一个共识,那就是“通过组件化完成关注点的分离从而降低局部复杂度”。其实现在我们用的无论是容器、中间件、消息、数据库等,在某种意义上都是组件化的产物。这样的好处是在不同的系统里可以复用。在云原生兴起的今天,以通用的、组件化的服务形式更容易为我们所用,所以说现在如果还不享用云原生技术红利,那你就会被时代抛弃。

云原生在技术上能够最大程度的解决众多非功能性质量和技术需求(如上图
我们再回到架构设计的本质,即为什么我们要在代码实现前做设计。设计首先是要解决问题的复杂度。于是有人做了一个架构,交给了一个团队去实现,很快发现实现的架构和设计完全是两回事。当然原因很明确——缺少了交流和沟通;其次是要建立团队协作沟通的共识。即使我们做好了一个团队都达成共识的架构设计,大家都兢兢业业把设计变成了现实,一个长期困扰软件行业的问题出现了,需求总是在变化,无论预先设计如何“精确”,总是发现下一个坑就在不远处,结果往往是情况越来越糟糕,也就是我们常说的架构“腐化”了,最后大家不得不接受重写。这些经历让我们逐步明确了软件架构设计的实质是通过核心问题的分离降低复杂度,并让系统能够更快地响应外界业务的变化,并且使得系统能够持续演进。在遇到变化时不需要从头开始,保证实现成本得到有效控制。
所以,我觉得从架构设计角度,以下三点是最为关键的:
- 让我们的模型、组件和业务划分尽量靠近变化的本质,比如对于一般电商系统来说,就是用户、商品、交易、支付等,这样的划分能够让我们将变化“隔离”在一定的范围
( 业务模块) 内,从而帮助我们有效减少改变点。 - 设计上,业务模型内部是高内聚,模型之间是低耦合,即各自完成的业务是相对独立的,不会因为一方掉线而牵连另外一方,比如商品推荐功能挂掉了,但是交易和支付业务应该继续正常提供服务,可能提示用户暂时无法提供推荐服务,或者干脆降级为兜底策略。
- 模型、组件在业务上尽可能是复用的,正是这样的复用才成就了今天的互联网级架构,我们不会每做一个电商系统都从零做起。而被“复用”最多的业务模块显然会重点设计和运营,成为核心业务模块。当然架构上这样的电商系统必然也会比较健壮。
上面的三点毫无疑问都指向了业务,从业务出发、面向业务变化是我们现代架构设计成功的关键,所以说复杂业务架构设计的核心实质是保证面对业务变化时我们能够有足够快的响应能力。
02 领域设计
前面说了业务软件开发的常见病:从一个小的项目不断开发演化变成一个大型业务系统,但随着新需求的不断增加,最终演变成了开发团队的噩梦。而这些噩梦大部分是源于软件的概念完整性(“概念完整性”一词来源于软件工程的经典著作《人月神话
“技术服务于业务、业务驱动技术”是目前大部分人的共识,尤其是对商业公司而言。而
战略建模
在战略层面,
- 定义好界限上下文
关于界限上下文的定义,随便一本讲
划分上下文的规则,无非就是放之四海而皆准的“高内聚、低耦合”,这么说可能还是太虚。其实真正让大家感到纠结的是,不知如何切分的那些东西之间所存在的关联,有的甚至干脆都纳入到一个上下文里。其实,我认为与其关注上下文的“大小”,不如关注模型的“质量”,关注概念的完整性是不是容易被破坏。我觉得,判断大小是不是合适,要结合应用开发团队的能力,看开发团队能在多大的一个范围内掌控软件的概念完整性。只要是开发团队没有问题,这个范围就算再大也都是可以的。
如果开发团队的水平在业界属于上游,那么维护上下文的范围往往是很大的;一些公司开发团队的水平参差不齐,所以在项目的实施过程中,可能需要划分相对小的上下文,尽可能减少“屎山”的不断堆积。
- 做好防腐层
界限上下文需要时刻保护好自己所维护的边界,以及边界内概念的完整性,这时需要将某个上下文的概念转化为另一个上下文概念的地方就叫做“防腐层”。防腐层的实现有很多种,典型的比如作为适配器

战术建模
- 一个叫做
Order 的聚合。 - 这个订单聚合的聚合根是一个叫做
OrderHeader 的实体,实体OrderHeader 的ID 叫做OrderId (订单号) 。 - 通过
OrderHeader 实体,我们可以访问OrderItem 实体的一个聚合。OrderItem 这个实体的局部ID 叫做ProductId (产品ID ) 。因为业务善变不允许在同一个订单的不同订单项内出现同一个产品,所以我们可以选择产品ID 作为订单项的局部ID 。
“聚合是数据修改的单元”,基于这个原则,我们可以做到“聚合内强一致、聚合外最终一致”,比如,我们可以不能接受一个订单内的所有订单项的金额之和不等于订单头的总金额,我们就必须把订单头和订单行项这两个实体划分到同一个聚合内。
设计聚合的原则
我们不妨先看一下《实现领域驱动设计》一书中对聚合设计原则的描述,原文是有点不太好理解的,我来稍微解释一下:
-
在一致性边界内建模真正的不变条件。聚合用来封装真正的不变性,而不是简单地将对象组合在一起。聚合内有一套不变的业务规则,各实体和值对象按照统一的业务规则运行,实现对象数据的一致性,边界之外的任何东西都与该聚合无关,这就是聚合能实现业务高内聚的原因。
-
尽量设计小的聚合。如果聚合设计得过大,聚合会因为包含过多的实体,导致实体之间的管理过于复杂,高频操作时会出现并发冲突或者数据库锁,最终导致系统可用性变差。而小聚合设计则可以降低由于业务过大导致聚合重构的可能性,让领域模型更能适应业务的变化。
-
通过唯一标识引用其它聚合。聚合之间是通过关联外部聚合根
ID 的方式引用,而不是直接对象引用的方式。外部聚合的对象放在聚合边界内管理,容易导致聚合的边界不清晰,也会增加聚合之间的耦合度。 -
在边界之外使用最终一致性。聚合内数据强一致性,而聚合之间数据最终一致性。在一次事务中,最多只能更改一个聚合的状态。如果一次业务操作涉及多个聚合状态的更改,应采用领域事件的方式异步修改相关的聚合,实现聚合之间的解耦(相关内容我会在领域事件部分详解
) 。 -
通过应用层实现跨聚合的服务调用。为实现微服务内聚合之间的解耦,以及未来以聚合为单位的微服务组合和拆分,应避免跨聚合的领域服务调用和跨聚合的数据库表关联。
上面的这些原则是
设计聚合的步骤
电商系统大家都比较熟悉了,而且关于电商业务来说有许多比较成熟的模型可以直接借鉴;那下面我们以另外一个场景

-
第一步:采用用例分析或事件风暴等方法,根据业务行为,梳理出在过程中发生这些行为的所有的实体和值对象,比如投保单、标的、客户、被保人等等。
-
第二步:从众多实体中选出适合作为对象管理者的根实体,也就是聚合根。判断一个实体是否是聚合根,上一章也说过,你可以结合以下场景分析:是否有独立的生命周期?是否有全局唯一
ID ?是否可以创建或修改其它对象?是否有专门的模块来管这个实体。图中的聚合根分别是投保单和客户实体。 -
第三步:根据上一章说的设计聚合的原则,找出与聚合根关联的所有紧密依赖的实体和值对象。构建出一个包含一个聚合根、多个实体和值对象的对象集合,这个集合就是聚合。在图中我们构建了客户和投保这两个聚合。
-
第四步:在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。这里需要说明一下:投保人和被保人的数据,是通过关联客户
ID 从客户聚合中获取的,在投保聚合里它们是投保单的值对象,这些值对象的数据是客户的冗余数据,即使未来客户聚合的数据发生了变更,也不会影响投保单的值对象数据。从图中我们还可以看出实体之间的引用关系,比如在投保聚合里投保单聚合根引用了报价单实体,报价单实体则引用了报价规则子实体。 -
第五步:多个聚合根据业务语义和上下文一起划分到同一个限界上下文内。
03 不同场景下的领域建模策略
由于企业内情况千差万别,发展历程也不一样,有遗留单体系统的微服务改造,也有全新未知领域的业务建模和系统设计,还有遗留系统局部优化的情况。不同场景下,领域建模的策略也会有差异。下面我们就分几类场景来看看如何进行领域建模。
新建系统
新建系统对于复杂的业务领域,领域可能需要多级拆分后才能开始领域建模。领域拆分为子域,甚至子域还需要进一步拆分。比如:保险它需要拆分为承保、理赔、收付费和再保等子域,承保子域再拆分为投保、保单管理等子子域。复杂领域如果不做进一步细分,由于问题域太大,领域建模的工程量会非常浩大。你不太容易通过事件风暴,完成一个很大的领域建模,即使勉强完成,效果也不一定好。
对于复杂领域,我们可以分三步来完成领域建模和微服务设计。
-
拆分子域建立领域模型,根据业务领域的特点,参考流程节点边界或功能聚合模块等边界因素。结合领域专家和项目团队的讨论,将领域逐级分解为大小合适的子域,针对子域采用事件风暴,划分聚合和限界上下文,初步确定子域内的领域模型。
-
领域模型微调梳理领域内所有子域的领域模型,对各子域领域模型进行微调。微调的过程重点考虑不同领域模型中聚合的重组。同步考虑领域模型和聚合的边界,服务以及事件之间的依赖关系,确定最终的领域模型。
-
微服务的设计和拆分根据领域模型和微服务拆分原则,完成微服务的拆分和设计。
单体遗留系统
如果我们面对的是一个单体遗留系统,只需要将部分功能独立为微服务,而其余仍为单体,整体保持不变,比如将面临性能瓶颈的模块拆分为微服务。我们只需要将这一特定功能,理解为一个简单子领域,参考简单领域建模的方式就可以了。在微服务设计中,我们还要考虑新老系统之间服务和业务的兼容,必要时可引入防腐层。
04 云原生时代下的挑战
随着云原生技术的兴起,现在企业级架构就更加的云化,而云化的架构风格有了新的关注点:弹性边界。弹性边界是一个云原生企业级应用架构的核心概念,它指把弹性作为最优先的考虑要素而划定的系统边界,决定了我们是否能够充分发挥云原生平台的全部能力。于是我们就需要新的方法来弥补以前业务模型的不足,以满足新的云原生化的需要。现在可以说,微服务基本上就是以云原生架构作为基础,而在固定弹性的平台上使用微服务架构,有极高的实施成本。可以说,云原生实际上就应该是微服务的前置条件。
在云原生时代,我们需要将弹性作为首要考虑的因素,纳入建模的考量。那么弹性边界,就是我们划分系统的重要依据。而且,我们还需要考虑弹性边界间的依赖关系,尽量避免弹性耦合。对于业务建模来说,为了配合云原生时代的架构,我觉得要做到如下几点:
-
确立一种模型结构能够反映弹性边界,而这时候需要考虑不同弹性边界的原则来划分界限上下文;如果两个上下文明显具有不同的弹性诉求,那就应该拆分。而如果具有一致的弹性诉求,可以考虑先不拆。那这个时候拆分微服务到底能多“微”呢?简单说就是“微”到能够更好的利用弹性来控制成本的大小。
-
从异步模型的视角,去优化业务逻辑;典型就是
MQ 消息队列系统,由于有broker ,所以生产者和消费者不必在同一时间都保持可用性以及相同的吞吐量,而且生产者也不需要马上等到回复。 -
位置的松耦合:典型就是服务注册中心,消费端完全不需要直接知道提供端的具体位置,而都通过注册中心来查找服务来访问。
-
在弹性边界切分业务上下文时,同一个弹性边界内部维护业务强一致性。
-
在异步调用产生中间态异常时,需要维护业务最终一致性。
05 不要忽视组织结构的影响
“康威定律”告诉我们,组织结构会决定团队沟通的结构,也会决定产品的结构。对组织结构的梳理,在需求调研的时候会经常做。如果是信息收集而言,业务架构设计在这里并没有什么特殊之处。区别在于,业务架构的目标是企业级的能力规划,希望能够突破壁垒、形成合力。正是这个原因,组织结构对业务架构设计的反作用力也是很大的,企业级数字化转型方案要与组织结构相匹配,否则落地的时候会困难重重。可以说,部门利益是做企业级架构的最大障碍之一,跨越这个障碍也是对架构师的能力的要求之一。当然,有些情况下,没有更好的解决方案时,不动也是一种选择。
以我的经验,这种问题没有特别好的办法,无非是两种:一是有超强能力者主导,在最高层的支持下,强力推进这种决策,但是企业越大,尤其是以业务为主导地位的企业中,这种结构就愈难形成;二是加强企业内部的业务架构人员的能力和数量(最好各个部门都有类似的角色
06 SOA- 微服务- 中台:妥协的艺术
多年前,这些传统的大型
然而实现它非常困难,但是破坏它却非常容易。一套
一般来说,我们会让服务的物理边界和界限上下文的领域边界基本堆砌,一个界限上下文对一个或多个可以独立部署的服务应用,服务应用包含了界限上下文的核心业务逻辑的实现。
但是,我们的架构师和开发人员太喜欢“分而治之”了,微服务的广泛使用甚至说是滥用,让我们看很多微服务真的是很“微”,几乎是一个
当然,微服务技术基础设施的发展也为服务之间的调用提供了更多的便利,跨越微服务的边界成为了常态;这个时候,业务开发人员区分“同一个上下文内的服务调用”和“上下文之间的防腐层”就要时刻保持头脑清醒,这时候的界限上下文和微服务的物理边界往往很难对齐,这就必然增加了维护每个界限上下文概念完整性的难度。
既然维护一个个“微小”的独立的界限上下文概念完整性越来愈难,那么我们干脆将它们再聚合起来吧?将它们融合到一个大小适度的界限上下文,那这就是所谓的企业级业务架构,也就是我们现在说的业务中台,最终目的可以说想要获得“企业级”的大和谐。
所以在一定程度上讲,软件工程就是妥协的艺术,是“中庸之道”。我们要不要中台,要大的中台,不管企业的大小,都应该结合自身的业务目标以及拥有的资源,在“维护更大范围的概念完整性”和“维护更多的防腐层代码”之间做出平衡,那这也是一个企业级架构师所要做的最核心的事情之一。
我们团队这些年确实做了一些“业务中台方法论”的积累和实践,并且在一些项目中做了实践,当然其中最灵魂的部分之一还是前面说的领域设计。以前很多人说