领域驱动设计的实践 – CQRS & Event Sourcing

1、前言

领域驱动(Domain – Driven Design)设计的理念在于建立一系列既符合软件所处领域本身又适合软件分析开发需要的领域模型。命令查询与职责分离(Command Query Responsibility Segregation)和事件溯源(E、vent Sourcing)是为一种领域驱动设计的实践。

本文旨在简要介绍CQRS & Event Sourcing, 希望能够给大家在设计业务系统上提供一种新的思路和选择。

2、领域驱动设计

在开始介绍CQRS之前,有必要先了解DDD中的一些基本思想和概念。

各行各业都有业务系统和软件开发的需求。比如Fintech公司会开发贷款业务系统,证券公司会开发股票行情交易软件,旅行社会开发在线旅游网站。虽然作为程序员,我赞成大家都能博学多才,上晓天文,下知地理。但是毕竟术业有专攻,做贷款业务系统时,我们需要请教信贷专家;研发股票行情交易系统时,我们会和交易员一起讨论;实践旅游网站时,兴许我们需要请教资深的旅行家……

那么当我们和领域专家围坐一起,高谈阔论之时,我们怎样才能做到有效的沟通,而不是鸡同鸭讲呢?DDD给了我们如下一些启发:

1、确定的领域模型(Domain Model)

明确的领域模型是一切的基础。

一个良好定义的领域模型一般会有以下几个特点:

该模型应该包含所有来自领域专家的知识

该模型可以让开发团队很清楚的界定领域边界,并且判断知识的上下文一致性

开发者可以将该模型以代码的形式进行表述

该模型可以方便地应对来自领域的变化

2、通用的语言(Ubiquitous Language)

对于领域中的名词,概念,所有的开发者和领域专家会采用同样的词汇,并且有着同样的理解。

比如在支付系统中,“渠道”可以是“微信支付”,“连连支付”,而不是“工商银行”, “招商银行”。如果开发者和领域专家的理解不同,那最后设计出来的产品势必南辕北辙。

3、实体(Entities),值(value), 服务(Services)

实体有一个全局唯一的标识,并且在整个生命周期中不变。

比如在支付系统中,一个交易(Transaction)就是一个实体。每个交易都有自己独有的交易ID。

值没有一个唯一的标识。比如在支付系统中。

交易的状态可以分为:“创建中”,“处理中”,“成功”,“失败”。 所以交易的状态就是一个值。

服务:除了实体和值之外,对于描述的动作,领域驱动的设计认为这是一个服务。

比如在支付系统中,与第三方支付绑定银行卡的行为,我们就可以认为是一种服务。

4、聚合(Aggregate)和聚合根(Aggregate Root)

相对于实体,值和服务是用来进行领域驱动设计中的建模模型。聚合和聚合根则是根据领域的原则分割并且描述实体之间的组合。

想象一下,如果一个系统中有许多用户,每个用户都可以修改部分数据。那么如何保证数据的一致性问题:

当每一个用户修改数据时,将数据库中所有的表都锁定。确实,这样可以保证强一致性,但是这肯定不是一个用户体验好的系统,并且性能十分糟糕

当每一个用户修改数据时,只将部分数据锁定。如此在用户的可用性和系统的一致性上能取到权衡

这里的问题就在于,如何界定B方案中的“部分”, DDD认为聚合就是在考量系统一致性后,相关的实体和值组合在一起的最小不可分割的集合。而聚合根本质仍然是一个实体, 在DDD中认为聚合根是访问聚合的唯一方式。

可能说的比较抽象,仍然以支付系统为例:我们认为“订单”就是一个聚合。订单可以包含多个“交易”, 同时订单也是一个实体,因为订单号是订单的唯一“标识”,订单本身也可以作为订单自身聚合的聚合根,外界通过访问订单才能访问订单中的交易。

以上便是DDD的一些基本概念,作为开发者而言,我并不赞同概念的堆砌和教条主义,其实很多时候,我们已经不自觉的使用了DDD的一些概念潜移默化的指导我们平时的软件开发,比如我们会在开发的小组内,使用约定俗称的名词,开发者和业务员都能明白这些没有歧义的名词(通用语言), 开发者也会站在业务员的角度思考软件系统内部设计分割的原则(聚合/聚合根的设计)。我相信DDD不是软件设计的条条框框,而是大量软件设计实践后,对于良好设计范式的一个总结和提炼。

3、命令查询与职责 & 事件溯源的系统架构

首先我们来看一个经典的基于数据驱动(Data Driven)的系统设计结构:

这是一个非常经典的系统设计,数据驱动的架构有很多现代ORM可以方便地实现基本的功能,优点不言而喻。我们就来谈谈这样的系统的局限性:

1、无法实践领域驱动编程

很明显,这样系统对领域对象最基本的操作就是增删改查(CURD),但是增删改查是计算机世界的术语,并不是一个领域的通用语言。在领域世界中,通用的语言远远比增删改查复杂的多。

还是以支付系统为例:比如创建“订单”,这并不是一个简单的增加操作。因为“订单”可能包含多个“交易”,所以创建“订单”其实包括增加一个“订单”信息以及增加该订单下的多个“交易”信息,并且最终将订单和交易增加(序列化)到数据存储中。而查询订单,则包括查询订单所属的“交易”,最终组合出订单并且返回。

如果最终的数据存储是一个关系型的数据库,则创建“订单”和查询“订单”的操作,需要开发人员理解订单和交易的关系,并且转换订单/交易模型至一个关系型的数据库。

2、单一的对象实体作用于数据读写

仍然以支付系统为例:无论是增加订单还是查询订单,在这样的系统中,订单被建模成一个单一的实体。无论是存储还是读取,都会将整个订单对象序列化到数据存储或者反序列出来。如果我们只是修改一个订单的状态,并且查询最新的订单状态,真的需要将整个对象都写入数据存储或者读取出来吗?撇开系统性能而言,安全性也是一个值得考量的问题。

基于对现实世界的观察,任何的方法,都可以拆解为两类:命令和查询。查询负责返回数据,并且不改变数据的状态。命令负责改变数据的状态,产生事件,但是不返回任何数据。任何复杂的方法(DDD中的服务),都可以是命令和查询的组合。

由此,我们来看一下基于CQRS & Event Sourcing的系统设计:

在CQRS的架构设计中,客户端可以发送命令,或者要求查询。对于命令而言,由命令总线负责分发给相应的命令处理器。命令处理器通过事件溯源加载得到相应的聚合根,修改聚合,并且产生相应的事件。事件首先会被存储,继而被事件总线分发给事件处理器。由事件处理器根据相应的事件将领域模型转换成写数据库中的存储表现形式。

写数据库可以以一种可靠的方式,将数据同步到读数据库,对于接受最终一致性的系统而言,这是可以接受的同步方式。

而另一方面,对应查询的需求,可以由简单的查询处理器接受查询请求,将写数据库中的数据转换成查询需要的形式予以返回。

事件溯源是一种通过采集所有的历史事件还原一个聚合状态的方法。

以一个支付系统的订单(聚合)为例。订单的生命周期可以是创建 -> 待计划 -> 执行中 -> 完成。那么对应的事件可以是订单创建事件,订单计划执行事件,订单执行事件,订单完成事件。

对于普通数据驱动的设计而言,订单的信息存储可能是这样的:(关系型数据库)

而对于支持事件溯源的系统而言,订单的存储可以是这样的:

{

“aggregateId” : “201609011005”

“eventPayload “: “Created”

“timeStamp” : “2016/09/01 10:50:01 ”

}

{

“aggregateId” : “201609011005”

“eventPayload “: “Scheduled”

“timeStamp” : “2016/09/01 10:51:11 ”

}

{

“aggregateId” : “201609011005”

“eventPayload “: “Executing”

“timeStamp” : “2016/09/01 18:02:59 ”

}

事件溯源将这三个事件依次加载处理,便可以还原出订单的现在状态。在聚合事件数量大的情况下,采用事件快照(Event Snapshot)可以有效提高事件溯源的效率和速度。

经过以上分析,在CQRS & Event Sourcing的设计中,我们可以看到以下优点:

读写分离:不同于数据驱动的设计,读写使用的同一个流程,甚至是同一个模型。在CQRS中,领域模型根据事件序列化至数据库。而查询模块则完全可以定义需要查询领域模型。读写是完全隔离的。如果使用数据库同步的方式,读写甚至可以使用不同的数据库(取决于系统对一致性的需求)。所以在这里,我们可以提高系统的吞吐量和性能。并且可以分别对写数据库和读数据库做出针对性的优化。

符合领域设计的原则:无论是命令还是事件,都是基于对现实世界的观察。不同于增删改,整个系统是由命令和事件驱动,由命令对相应的聚合(实体)进行修改。而修改则产生了相应的事件,事件可以再产生命令,如此往复。

我们的世界此刻不正是由无数个事件叠加产生的结果吗?

整个系统的所有事件都有历史记录:对于任何聚合的生命周期中,如何被创建,修改直至回收的过程,都可以通过一个又一个事件被回溯,分析。我们不仅仅关心聚合最终状态,对中间记录的分析同样也有价值。

同样,CQRS & Event Sourcing 也有自身的局限性:

系统结构相对于经典的设计而言复杂。需要设计命令总线,命令分发器,事件总线,事件分发器。需要设计良好的事件存储机制,以及事件溯源机制

对于简单,静态的系统,或者是没有复杂协作上下文(Bounded Context)的领域模型的系统,引入CQRS并不会得到很多益处,相反会使得系统臃肿,庞大

因为引入了部分DDD领域设计的概念,对于开发人员也有一定的学习曲线

4、总结

CQRS & ES 给我们提供了一种有别于传统经典体系的设计思路,在业务系统中分析哪里需要使用CQRS & ES 需要我们权衡实施这种新体系架构所需的代价和长期的回报。此文简要介绍了CQRS & ES 在领域驱动设计内的实践,希望能抛砖引玉,与诸君共勉。

本文作者:王磊(点融黑帮),点融网高级软件工程师,目前就职于Fincore部门。曾在投行工作四年,专注于支付业务。爱旅行,爱网球,爱乒乓。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容