本节开始进入DDD的战术阶段,首先要讲解的必然是DDD中的架构,毕竟程序员就喜欢这个……不过这里的架构不同于我们常说的微服务架构、单体架构、无服务架构或服务网格。不严谨来讲,上述4种为涉及到系统结构、部署方式、服务器架构等更为全面的、包含软、硬件等内容的宏观系统架构(这个不在我们的主要范围内,主要是以个人这点水平吹个牛还行,不成系统的)。而我们要讲的更多的是聚焦于基于BC的架构模式,也就是BC在落地时所使用的设计模式。
提示:
严格上来讲其实并没有单体架构这个概念,微服务架构出来前大家一般都会把多个模块集中在一个程序集中,是一种约定俗成的模式,也没人给它起个名。后来为了将其与分微服务架进行区分,才有了所谓单体的概念。
个人对于DDD中常用的的架构模式总结出了5种(总结出更多模式的大牛请嘴下留情):分层、Saga、洋葱、命令查询责任分离(CQRS)和事件源(ES),稍后会逐一进行讲解。有人会说怎么没有六边形?这东西其实和洋葱是一样的,您可以认为两者相同。另外多说两句,很多的文章都贴出来了六边形或洋葱以及非常有名的Clean Architecture架构并给出了大量的解释,我个人认为这些都是同一种架构模式的不同描述(当然也不排除有些人故意让你看不懂),不管名字叫得多邪乎,其实本质上是一个东西。比如有一张著名的架构图,我就不信您看完了不晕(其实落地的时候还是分层的,再加上有了Spring这个神器,别说六边了,十二边咱也不惧)。
强调一点,我个人其实是一个经典三层架构模式的拥护者。虽然可以熟练的使用模型驱动的方式进行代码落地,但日常工作中选择起来会比较慎重,能用三层的绝不用四层,能用面向过程的肯定不用面向对象,此乃我的懒人哲学观。不过我可不管这个叫“懒”,这叫“务实”。另外,没有任何模式是银弹,您需要去取舍(当然了,取舍的前提是你得知道有哪些,其适合的场景是什么。如果仅知道三层,那还取舍个毛线啊)。有的时候我觉得过度设计比不设计还要可怕。不设计的代码一般来说都是那种面向过程的,代码行数多,不过也比较直观;那些设计过度的代码才是恶心,改一行代码几乎需要走查所有的代码尤其作者不是自己的时候,一边改一边骂爹不说,你还不敢轻易上,万一哪一块没考虑到呢?我依稀记得曾经入了一个群讨论DDD问题:有几个哥们在讨论ES,有个牛人说他们的一个系统全面采用了ES(使用Axon框架)来实现,各种的好……我弱弱的问了一句ES一般是不是都是局部模式,仅适用一些特定的场景吧?结果直接被人家说“我们说的是事件源,不懂不要瞎说,不是你说的那个Elastic Search……”,不是我吐槽,这种就属于典型的无知者无畏了。难听的话咱不能再多说,忒丢份,但这类工程师夸夸其谈、好大喜功,少了一些最起码的职业素养。
一、分层架构其实分层还真不能和CQRS、ES、Saga同日而语,这是一种基础模式或可称其为编程模式,后面三者是依据业务所需而建立起的一种设计模式。放在一起说其实有点驴唇不对马嘴,不过也能加深我们的理解还有利于我多水一些文字。分层式开发虽然已经成为了一种事实上的标准,但就这样,不会用的人也比比皆是。好多程序员连这个最基本的模式都搞不明白更别说更复杂的设计方法了,这还真不是技术不够,是懒!这“懒”和我的“懒”不一样。咱的“懒”是为了追求对设计的“度”的掌握;后面的“懒”那是真懒,谁告诉你开发就是无脑的堆代码的?就算是最简单的3层也有许多的限制。我就见过一个方法超过1000行代码,其中0行是注释。代码长不可怕,麻烦的是看到后面时把前面的给忘了!
一提分层架构您想到的肯定是经典三层,没错!个人很喜欢的一种模式,直观!简单!用起来老痛快了。其实,一个系统中的大部分的功能使用经典三层就可以搞定了除非您开发的是火箭导航这类系统。这话您可能会反对,您可能会说银行啊、金融啊、保险啊这类系统,业务那么复杂怎么可能会使用三层?客观来讲,这类业务的复杂度不需要怀疑,但我个人认为越是复杂的系统越是会有一堆庞大的基础设施和基础业务做支撑,而一般基础类业务的开发其实根本就用不着使用特别复杂的设计模式,那东西开发起来多费劲啊,有框架也不行。三层不代表代码质量差,高手写起三层来照样有大师的水准。技术的选择永远都是由于业务进行驱动的,切记切记。还记得上一章说的分层模式吗?再啰嗦一句:三层模式请使用“严格分层架构”。下面贴了一张三层模式的图,我知道你会,主要为了显示我专业。对了,三层模式也叫“事务脚本”,Martin Flower称其为“反模式”,主要是这种模式属于典型的面向过程式编程,虽然入门简单,但着实不适用于复杂业务的场景,扩展和维护起来忒费劲。
另外一种分层架构是DDD中所提的对象驱动设计模式(ODD,这名是我起的,其实就是面向对象编程,使用了如实体、值类型等DDD中战术部分所提概念),共四层。这个模式使用起来约束比较多,是最最最基本的模式,说其是其它几类架构的基础也不为过。实际上,我后面所讲的内容也都是以这模式为主的。这个模式有一个厚厚的业务模型层,依赖关系请参看下图,几乎各层都依赖于领域模型(我才不会让用户接口层直接指向BO,天机不可外泄),设计的时候也是从领域模型层开始,根据领域模型选择数据库。一会儿您把这个模式和后面说的洋葱对比一下,是不是会发现所谓的洋葱整个一忽悠人的,其实和下面的是一个玩意儿……
这里,我概括出一个结论:分层模式是所有模式的基础,使用面向对象编程时采用4层架构,否则使用经典3层。洋葱模式或六边形模式是一种概念形架构,通过在4层模式的使用中增加约束和规范即可达到目标,使用Spring后则更为简单;后面提到的那四个货是根据业务的所需(主要是非功能性需求)而使用的一种设计模式,落地后仍然是上面两类分层模式之一。对于上面的结论您可能不认同,不过谁写文章就听谁的,反正我信了。
二、SagaSaga模式主要解决两个场景问题:分布式事务和多服务交互。Saga通过最终一致性的方式实现分布式事务,在使用的时候一般会用到MQ,所以务必要注意消息队列的可靠性。Saga是一种EDA架构,其使用了两个重要的领域模型:命令、事件,具体概念及案例后面安排。通过如下图可以看出来,本模式引入了一个中心化的事务控制器(Saga有两种模式:让事务参与者作为协调者角色推动子事务的执行(去中心化Saga)和采用统一事务控制器模式(中心化Saga),个人比较倾向于第二种,所以这里也仅介绍第二种),通过把一个大的、长的事务分成多个小业务片段(子事务)属于比较典型的分治法。 中心化的事务控制器引入后可以实现事务参与者间的解耦,这种解耦也能缓解多服务交互时的复杂性。试想一下,如果有一个流程需要3个服务参与,就会存在多达6条链路;4个服务时需要12条链路……让人望而生畏。
谈到了事务就多说两句,其一致性一般包含两类:强一致性和弱一致性(后来有人觉得“弱”这个词让人很不爽,感觉就和“差”一样,反正是一个贬义成份居多的词,所以又给他起了个新名“最终一致性”)。强一致性的事务如本地事务、2PC、3PC等,这种属于刚性事务,不太适合分布式系统而且性能也差了点意思。分布式系统已成为了当前的主流,CAP问题你是无法绕开的,所以大多数的分布试系统优先使用了“AP”。您想啊,分布式的一个主要目的就是为了提升系统的可用性,如果优先CP,那分布式优点就少了一大半。虽说弱化了“C”,但弱不代表没有,这也是为什么现在都讲究最终一致性,其实是在分布式环境中所面临的一种无奈,只能妥协。对了,最终一致性的事务了有个牛掰的名称“柔性事务”,千万别忘了。所以在这里说了这么多关于事务的事情,是因为Saga的一个主要作用就是做分布式事务,网上有相关论文您可以翻翻,虽然不一定用得上,但吹牛的时候还是有个谈资的。另外,也有一些框架支持Saga比如:阿里的Seata、华为的ServiceComb,可以搞来玩玩儿。
我自己用的时候一般不使用上述框架,应用的场景也不是为了事务控制,而是做流程控制。比如一个业务流程需要A、B、C三个不同的服务节点参与完成,流程配置不同所使用的结点也不一样,可能是“ABC”、“AC”、“ACBA”等,此等情形下通过引入Saga模式可以有效的解决服务调度繁琐的情况(Saga会根据不同的事件触发不同的命令,由事件来驱动业务流程的前行)。此处的流程调度控制器并不能代替工作流引擎,毕竟后者主要面向的是流程设计,也很少用于分布试场景下的服务调度。另外,自己写一个简单的Saga其实已经够用了,这东西就和工厂、模板这种设计模式差不多,真心没那么神秘。
三、洋葱洋葱或六边型您可以认为是同一种模式,只是叫法不同而矣。前面我已经说过,这个模式是概念模型。其实现时一般会基于4层的面向对象编程架构,通常还需要有支持IoC的框架(如Spring)配合。当然,落地时还强制要求遵循一定的约束与规范,尤其是各层的责任和访问顺序都有着严格的限制。
四、命令查询责任分离(CQRS)CQRS本质上是一种在数据存储和查询方面进行优化的模式,并未引入什么玄妙的概念。这个模式把命令(对数据有影响的操作)和查询分成两个不同的实现模式。命令端(C端)一般也是使用4层架构;查询端(Q端)一般使用3层。通常来讲,会有两个不同的数据源(可能是同样的库也可能不同)分别用于查询和命令场景,数据源之间使用事件进行同步。另外,使用CQRS时,C端一般使用异步将参数以消息的形式发送到MQ然后立即反馈结果至调用端,速度那是相当的快;Q端可以使用您能使用的任何模式比如大表、缓存等来加快查询速度,反正只要够快就行,唯一要注意的是Q端不能修改数据源。
这个架构主要是用于高并发的场景尤其是C端并发多时候,实际使用时需要考虑用户的体验毕竟调用后就返回给了客户端,实际成功与否还两说呢。数据不一致性是另外一个需要重点考虑的问题。此外,如果使用了ES(事件源)模式,肯定也得使用CQRS架构。因为ES写入的是事件,而业务系统是需要查询业务数据当前的值的。这里有一个问题需要特别的说明,网上很多文章一说CQRS就习惯性和ES挂钩,它们两个的关系应该是这样:使用了ES,一般会使用CQRS模式;使用CQRS不代表一定要使用ES。
另外,还有一种编程模式叫CQS(命令查询分离),这个和CQRS有着本质的不同。前者一般只有一个数据源,C和Q端属于同一个服务,C端一般也不使用异步;后者在数据源上一般会多于1个,C和Q分属两个服务。CQS我认为是一种强制型编程模式尤其是使用ODD模式的时候,C操作使用4层而Q操作使用3层。也就是说,就算是同一个业务,其架构模式也不同,需要区分到查询操作和命令操作。
五、事件源(Event Source)事件源架构本身并无特殊性,只是其处理命令的时候存储的是事件而不是直接更新数据库表的值。您琢磨琢磨,一般我们存储数据其实都是存储其最终态,比如有个字段“Status(状态)”,其值为“3”。数据库表直接存储“3”这个值,至于这个值是从何而来,其变化轨迹如何等问题一般考虑也不会存储起来。而ES记录了一个对象状态的变化轨迹,每一次变化对应一个事件,该模式适用于高并发且同时需要跟踪对象变化的场景。由于存储事件可以使用键值对(键:对象ID;值:事件)的方式,所以您可以想象一下,如果使用了比如HBase这种作为数据源,那速度绝对杠杠的。这个模式一般还会与CQRS模式配合,妥妥的一对好基友。查询的场景一般是将对象的最终状态供用户使用,这个很简单的需求对于事件源模式其实是非常困难的,ES架构下只存储了对象所经历的命令,只有把一个对象所经历的事件重跑一次才能形成最终态;如果涉及对象列表或多表级联查询,那就不是ES能搞定的了更不要说未来的报表、数据仓库等需求。所以可以想象,使用ES时必然需要同时有一个查询库配合才行。这种架构比较复杂,不是一个普适的模式。
使用ES的系统,运维起来也会是个噩梦。举个例子,我在写代码的时候由于考虑不周误一个字段的值变成了“8”,正确的应该是“7”,影响了1000条数据。正常情况下我直接修复代码中的BUG并批量更新数据库的值,最多再多一步把缓存干掉,事情就结了。使用ES模式后,数据库中存储的是事件,想通过SQL直接改数据简直难于上青天,等您写好更新工具后搞不好还得同步整个辞职申请。
另外,ES属于非常典型的事件驱动编程,其编程思维模式也与我们一般的方式不同,说白了就是有点别扭,可见后面的案例。ES模式本人系统学习过但未曾在真实项目中使用,所以即使后面有案例您也只能参考使用,真要搞还得做好充分的心理准备以及相对成熟的框架做支撑。
上面介绍了5个模式,后面会进行详细介绍。重点说明一下:分层模式才是本系列的重点,其它4种仅是补充,您需要根据实际业务的现状和需求选择对应的模式。还有一点,本文所说的东西可能与书上不一样,也有可能会与您的认识不符,不过这是我个人的理解,反正您爱说什么说什么,我是虚心接受但坚决不改。毕竟“仰天大笑出门去 我辈岂是蓬蒿人”。