DDD领域驱动设计相关知识总结

DDD(Domain Drive Design)与TDD测试驱动设计都是不同的软件架构设计理论,与具体使用的技术手段无关。传统的软件设计逻辑基本都是从用户交互角度出发,软件核心的业务逻辑也都是增删改查等一系列数据实体的操作,更多的是关心的数据的转换。对于简单的业务逻辑抽象,传统的设计方法更直接,实现也更简单。而对于比较复杂的业务逻辑,领域驱动设计能够更好的划分不同业务之间的边界,更容易在开发中分离责任,同时也让服务健壮性和扩展性更好,但DDD也不是银弹,不是所有的软件开发都应该使用这种模式,这种模式对开发的要求更高,复杂性也更高,本文主要讨论在软件设计中应用DDD的一些相关的关键设计方式和实现手段,以及他们的一些优缺点。

首先,本文所涉及的关键词有:

  1. DDD——Domain Drive Design
  2. Micro Service
  3. CQRS——Command Query Responsibility Segregation
  4. EDA——Event Driven Architechture
  5. ES——Event Sourcing
  6. Akka:Scala的一个非常优秀的Actor Model的实现
  7. Lagom:Lightbend(前身TypeSafe)的微服务框架

DDD只是一种设计思想,其中包含了很多抽象的设计概念,比如领域、值对象、服务、仓库、聚合等,我不太熟悉,这里不做详细介绍,单纯的讨论设计方法意义不大,就跟敏捷开发似的有很多成熟的理论框架比如SCRUM,其实事实上真正应用起来都不那么的绝对按照理论来规划,DDD也只是一种高度的软件设计的表现层,而我们更关心具体的DDD的实现方式以及里边真正有趣的能够带来思想变革的地方。

说起DDD其实跟现在很火的微服务有很多关联的地方,知道微服务的也远比知道DDD的人多一些,我想微服务可以看作是DDD实践从理论到形式的一种简化,以更方便的让人们接受和理解。微服务炒了这么多年,而真正实践很完美的案例其实也并不那么多,有以下几个原因:

  1. 服务划分的边界不容易掌握,服务并不是越微越好,盲目的划分带来很多问题,增加通信、调度成本;
  2. 没有成熟的框架和运维工具支持,使得微服务在服务之间调用、发现、部署以及依赖管理上都存在一些难点,反而增加了整个架构的复杂度,带来的难处可能远大于其好处;
  3. 现有微服务的设计多采用Rest风格的HTTP接口,服务之间调用是同步的,容易发生严重的滚雪球似的崩溃现象,整个集群的瓶颈类似木桶效应,由最弱的服务决定,通常整体表现更差。

我们期待有一种比较成熟完备的框架能够比较完美的表达DDD设计模式,同时也能够解决微服务所带来的各种问题,目前看来Actor Model对于表达DDD模式具有天然的优势,不同的Actor之间天然存在边界,每个Actor处理自己相关的业务逻辑,如果需要其他Actor帮助,通过发消息的形式来驱动,可以说用Actor Model实现DDD风格的软件设计,从微观到宏观上完成了领域驱动的统一,微观上在进程内的微线程级别对领域进行了划分,也就是Actor本身,在宏观角度进程(微服务)之间也通过Actor无差别的消息发送形式屏蔽了这些差别,使得构造DDD风格的集群非常容易,且由于结合了事件驱动形式,整个软件架构可以都是Reactive的,运行效率也会非常高。从开发角度可以认为整个软件架构集群是一个整体,在服务内部和外部之间采用统一的事件驱动形式,完美的统一了不同层次的设计和表现,只是在生产上根据需要优化不同领域或者服务所需要的资源以及所在的物理节点的位置。Akka就是一种Actor Model的实现,采用Scala语言,具有良好的DSL和运行效率,可以作为实现DDD+EDA的完美的结合,基于DDD和EDA来构造微服务的架构,可以解决上边所说的微服务带来的负面问题,而Lagom恰好正是这么一个框架,下边会详细介绍。

说了太多领域驱动设计和事件驱动架构,这两者都比较容易理解,下边着重说一下在DDD设计中,或者说在所有的软件设计中,最核心的数据对象的设计和操作的一些形式,软件表面上提供的是服务,但核心都是这些服务所操作的数据,对于非计算密集型的业务场景,性能瓶颈主要也是由于频繁的数据操作引起的,比如说数据库IO操作,所以可以说处理好了这部分,就解决了绝大部分的性能问题,而CQRS就是一个比较有趣的概念,在某些场景可以发挥奇效,同时CQRS这种命令端和查询端的划分以及设计技巧,也是领域驱动设计在数据操作甚至也是服务操作的一种特例设计,代表了DDD的一种特色。当然具体决定要不要使用CQRS这种风格来处理数据实体操作,要看具体的业务场景是否适合。

CQRS

CQRS主张读写分离,广义的CQRS在很多层面上可以实现,比如数据库的主从部署,但是典型的CQRS应该指的是读/写比例非常高的应用,可以在框架或者程序业务逻辑中分别优化读写的领域,通常CQRS主要应用在DDD开发模式中,配合事件驱动来保证全异步的处理过程,最大化利用系统资源,同时具有非常高的可扩展性,稳定性较强。全异步的CQRS在遇到峰值并发时可以有更强的自保能力,不至于出现雪崩效果,通过消息中间件缓存未处理的消息,削峰填谷,而传统的REST风格的微服务会因为同步的等待请求返回结果导致其他请求不能正常接受处理,使系统不可用。Lagom就是一个DDD模式的微服务框架,其中的持久化层就采用了CQRS风格,以非常优秀的AKKA作为底层实现基础,Lagom的CQRS自然也是全异步且支持ES消息溯源的。

说到CQRS首先要提Event Souring,事件溯源是CQRS的一种实现技巧,通常用在C端,数据不再是通过增删改查反应在数据库的一条记录,而是由一系列的事件以及注册在这些事件上的操作所结合起来产生的一种数据状态,类似于时光机器,需要通过快照和Apply后续所有的事件来获得数据最新的状态。使用ES的C端不再有对数据的修改和删除等操作,有的只是事件和命令的插入,ES的一些优势和缺陷同时也是CQRS的一部分优点和缺点。

优势:

  1. 只需要插入事件和命令到固化的数据库,速度比通过索引UPDATE快,尤其对于一些Write-Ahead-Loging的NOSQL数据库,写入速度会非常快
  2. 全异步的事件驱动,相对于传统的同步数据库请求,改进明显,数据库不再是瓶颈
  3. 可以随时回溯到某个状态,查看用户的一系列操作,容易查找问题
  4. 所记录的事件可以用来进行BI分析,挖掘用户的隐藏属性,操作习惯,进行一些有价值的预测

缺点:

  1. 事件数量非常大,存储是个问题
  2. 数据状态的恢复比较慢,对于事件很多会长时间修改某一个实体的情况,虽然可以采用快照来快速恢复,但依然实时性差一些
  3. 只保证最终一致性
  4. 对重构很不友好,如果改变了某些数据结构设计,为了兼容以前的事件,需要多做一些处理
  5. 不方便直接手动修改数据了,因为数据不存在一个最终状态,只有事件,修改Q端的结果并不能影响实际的数据状态,只会造成不一致

In-Memory也是CQRS的一种优化手段,C端的持久化实体是加载到内存的,用户的请求只是需要修改内存的状态就可以返回,可以一秒钟处理远大于数据库能力数量的请求,而后期的事件和命令的持久化也是异步的基于消息的,最大化提高了写入速度。

Group Commit,通过在业务逻辑端实现批量提交,减少对数据库的操作,成倍的提高请求的执行效率。

Command,内存中加载对应的对象实体,接受命令并持久化命令,发送成功的事件交给事件处理部分,事件处理部分持久化事件并更新实体状态,更新状态后成功的事件继续发送给Q端视图,如果有需要的话,用来更新ReadSide所需要的数据的状态,这里就包括了以前经常用到的数据增删改查的操作,但一般也是很简单的对一张表的处理。整个操作都是异步的消息驱动的,保证写入速度很快,不会受制于数据对象和数据库阻抗失衡。

Query

CQRS想到存在的问题

  1. 异步通信的性能损耗,多次异步操作会严重依赖底层线程池对事件的处理,线程的切换等也会影响执行效率,最重要的是无法保证低延时,即使先进先出的队列也是可能需要切换线程来运行,即使很简单的操作延时也不会很低,起码是毫秒级别,在保证吞吐量和健壮性的时候,牺牲了低延时。这个有点吹毛求疵,其实对于有大量数据库请求的业务逻辑来说,这点Dispatcher的消耗是完全可以接受的,用全异步换来无阻塞,只是对于完全的内存处理的逻辑会降低响应速度,比如测试Hello World benchmark,当然这个没有什么实际意义了。
  2. 创建聚合根Entity 需要在客户端创建其ID,而不能依赖Database等设备来生成自增长的ID,因为实际数据库生成ID是一读一写的过程,跟CQRS的思想相冲突。如果交由某个AggregateRoot来管理ID,又会造成性能瓶颈,降低性能,不过也不会比数据库更慢,只是有通信消耗。
  3. Query数据所用的View也需要数据库支持,Q节点通过接收C节点持久化Event同时修改State成功之后的新的事件,用来更新视图,一般来说适合简单的Select操作,不推荐Join等涉及多个表的操作,可以考虑使用Document形式存储,嵌入保存相关联的数据模型。
  4. 对于简单的业务类型不推荐CQRS,只是增加复杂性,只有对于业务逻辑复杂,一次完整的事件需要很多后续的响应的时候才适合,比如用户发了一个微信,后期有很多的额外工作要做,不只是保存一条消息文本,还要更新用户的活跃时间,下载消息所包含的多媒体信息,并推送给某个客服人员等等这种业务逻辑,而对于传统的表单类型,比如提交一个办公用品申请这种用户操作,明显CRUD更直观简单,不产生后续的操作。
  5. 对于秒杀和抢火车票这种峰值很高的业务类型,CQRS可以让系统不局限于数据库的处理能力,适用于的这种业务的特点一般实体的关联非常少,某个命令只需要修改一个实体,不用同时考虑多个实体的状态来决定下一步要做什么,更简单集中,只是并发很大。另外业务模型要足够稳定,尽量避免重构,类似的还有股市等交易系统,需要大家并发很大的对某一些实体进行修改,通常新只是新产生交易记录,很少对以前的数据进行修改,用户动作基本上完全按照time serial的形式,符合事件驱动的特征,有个实践例子是某交易系统使用CQRS模式实现了每秒上百万的并发,基于IN MEMORY和ES可以使得并发尽可能的迅速,用户可以接受一定时间的交易延迟,不需要马上看到交易的结果,天生适合事件驱动和CQRS。

总得来说,使用CQRS可以解决一些目前互联网存在的一些问题,但是也只适合某些特定的领域,并不是普适的,而事件驱动可以应用在所有的场景,并没有什么副作用。

推荐阅读更多精彩内容