CQRS体系结构模式实践案例:Tiny Library:领域仓储与事件存储
领域仓储(Domain Repository)与事件存储(Event Store)是CQRS体系结构应用系统中C部分(Command部分)的重要组件。虽然都是存储机制,但两者有着本质的区别:领域仓储是属于领域层的,而事件仓储则是属于基础结构层的。领域模型产生事件,领域仓储负责保存、发布事件,并通过事件序列重塑领域模型。由于领域仓储的存在,使得“内存领域模型(In-memory Domain)”成为可能。
在上文中我已经对对象的状态做了一些介绍,通过这些介绍我们能够了解到,在应用系统中,是领域事件导致了对象状态的变化,于是,我们只需要把这些领域事件按顺序记录下来,我们就有能力将领域模型还原到任何一个时间点上。就以Tiny Library中的Reader聚合为例,当Reader刚刚被创建的时候,它的Name状态是空的,客户程序可以通过Reader实体的ChangeName方法来改变Name的状态。ChangeName方法会直接产生一个ReaderNameChangedEvent的领域事件,告知系统,现在发生了一件事情,这件事情将会改变Reader实体的状态。Reader实体获得了这个事件通知,就将Name状态设置为事件数据中的给定名称,同时,这个ReaderNameChangedEvent事件也被临时保存在了Reader实体中。
另一方面,当客户程序调用领域仓储来保存Reader时,仓储会将Reader中所有的领域事件读取出来,按照顺序逐个保存到事件存储中,与此同时,将这些事件发布到事件总线(Event Bus)上,以便同一系统的其它组件(比如Query Database)或者其它的系统能够接收到事件到达通知而做进一步的处理。
当客户程序需要通过领域仓储读取聚合时,领域仓储就会新建聚合,然后从事件存储中,以该聚合的聚合根的类型作为搜索条件,将领域事件按顺序读取出来并一个个地应用在这个新建的聚合上,聚合根实体一旦捕获到事件,就会按照事件的数据内容更新对应的状态,于是,聚合也就被恢复到了最后一次事件发生后的状态了。
这个过程很简单,通过上面的分析不难发现:
- 由于查询部分的分离,领域仓储仅存两种操作:将聚合保存到事件存储以及从事件存储还原对象。与之对应的操作大致可以表示成下面的接口(该部分代码摘录自Apworks Application Development Framework):
1: public interface IDomainRepository : IUnitOfWork, IDisposable
2: {
3: TAggregateRoot Get<TAggregateRoot>(long id)
4: where TAggregateRoot : class, ISourcedAggregateRoot, new();
5:
6: void Save<TAggregateRoot>(TAggregateRoot aggregateRoot)
7: where TAggregateRoot : class, ISourcedAggregateRoot, new();
8: }
- 整个Domain Model只有一个数据源:事件存储(Event Store),用来保存所有发生在聚合上的领域事件。这个Event Store具体如何设计,可以根据应用系统的需求来决定,但总归是非常的简单,甚至于仅用一张关系型数据库的数据表就可以实现。对于采用关系型数据库实现的事件存储,由于数据表数量很少,而且之间的关系变得非常简单,于是ORM就可以省略,直接采用Direct SQL实现;如果不采用关系型数据库作为事件存储,那可以选择的范围就更大了:各种NoSQL数据库、对象数据库、内存数据库等等。就关系型数据库而言,我们可以对事件存储所使用的数据表做如下的设计:
目前,Tiny Library CQRS赖以生存的开发框架Apworks,仅提供支持SQL Server的Event Store设计(Apworks当前版本:Alpha,v1.0.4016.23016) - 如果事件存储采用的是关系型数据库,领域仓储对事件存储,原则上也只有类似如下两种操作:
1: // 查询事件存储
2: SELECT * FROM [Events] WHERE AggregateId=xxx ORDER BY Version
3:
4: // 向事件存储保存事件
5: INSERT INTO [Events] ([AggregateId], [Timestamp], [Version], [Data]) VALUES (...)
当然,在实际应用中,领域仓储与事件存储的实现并没有那么简单。原因可以通过如下几个疑问进行了解:
- 领域仓储的设计中,没有提到从事件存储中删除事件数据,时间一长,岂不是事件存储会变得很大?
没错!领域仓储从来不会从事件存储中删除数据,即使是客户程序请求删除某个领域对象,这一操作也同样会产生一个事件(比如:ReaderDeletedEvent)并保存在事件存储中。这样做的理由来自于Event Sourcing所带来的一种数据分析与跟踪的可能性:Event Audit。它允许你将你的领域模型还原到任何时间点,然后通过事件重放(replay)来诊断你的模型数据。不仅如此,你还可以利用这些保存的数据重新搭建你的测试环境,用来对对象数据进行测试。当然,目前大部分系统可能用不到这样的Event Audit的功能,那么,在引入“快照”的情况下,你可以从事件存储数据库中定期地删除数据。然而,这是另外一种“退化”的CQRS设计,也同样是合理的,不过这不是我们讨论的范围。我们要讨论的是,时间一长,事件存储变得巨大怎么办?
CQRS架构社区中有一句非常有意思的话,就是:Storage is cheap,data is valuable(存储是廉价的,而数据是有价值的)。通常,都是通过大容量存储备份以及数据归档来解决这样的问题:对于较早的事件数据,我们选用高速而昂贵的存储介质进行备份,而对于更早的事件数据,则可以采用低速而便宜的存储介质进行归档,综合采用两种不同的方案以使得事件存储端“性价比”达到最高。当然,这样的策略同样需要“快照”的支持 - 某些聚合的生命周期可能很长,于是就会在它们身上产生大量的事件数据,当领域仓储重建这些聚合的时候,需要把大量的事件依次地“应用”在这些聚合上,岂不是会花很多时间?
在此,我们通过引入“快照”的概念来解决这个问题。在系统中,可以根据一定的“快照策略”来确定何时应该对聚合进行“快照”。每当这个快照策略的条件符合,系统就会对聚合做一次快照,并将快照数据记录在事件存储中。比如:我们可以指定,每n个事件发生时,就对聚合做一次快照,于是,当我们需要获得第n+3个事件发生时,该聚合的状态的时候,就只需要直接从事件存储中读取第n个事件发生时,聚合的快照,然后再依次将n+1、n+2、n+3个事件应用到聚合即可。这样就大大缩短了重建聚合所需的时间,也使得上面第一个问题中归档的实现成为可能
在Apworks应用开发框架中,目前版本(Alpha,v1.0.4016.23016)对快照的支持是采用的GoF的memento模式,而对快照策略的支持就显得非常简单:仅仅是通过Apworks.Events.Storage.IDomainEventStorage.CanCreateOrUpdateSnapshot方法进行定义的。在Apworks.Events.Storage.SqlDomainEventStorage类中,实现了这个方法,并指定每当第1000个领域事件发生时,对聚合做一次快照。如果你打算继续采用SQL Server作为事件存储,并打算重新定制快照策略,请新建一个类并继承Apworks.Events.Storage.SqlDomainEventStorage类,然后重写CanCreateOrUpdateSnapshot方法;如果你打算采用其它的介质作为事件存储,则请自行实现Apworks.Events.Storage.IDomainEventStorage接口 - 在保存聚合的时候,领域仓储不仅需要将事件保存到事件存储,而且还需要将事件推送到事件总线上,这样做从技术上很难保证操作的原子性,换句话说,会不会造成数据的不一致性?
是的,这就是所谓的“两次提交”(Two-Phase Commit, TPC)操作。在设计中应该避免TPC的出现,因为在两次提交之间会发生很多事情,如果不能保证操作的原子性,也就无法保证数据的一致性。对于CQRS体系结构的应用系统而言,这是致命的。目前在事件存储部分,避免TPC有两种方案:A.将事件存储整合到事件总线;B.将事件总线整合到事件存储。总之,思想只有一个:就是采用同一个持久化机制来整合存储部分与总线部分。有关TPC的深入研究,我会在后续的扩展话题中讨论。目前版本的Apworks(Alpha,v1.0.4016.23016)不提供对TPC的支持
现在,我们再来看看Tiny Library CQRS项目中,事件存储的实现方式。实际上,Tiny Library CQRS采用的是Apworks应用开发框架所提供的默认的事件存储机制:基于SQL Server的单表事件存储。表结构如下:
首先,领域仓储从聚合获得未保存(即未提交)事件,然后,使用指定的序列化方式,将事件序列化为二进制流,并保存到Apworks.Events.Storage.DomainEventDataObject对象中,这个对象其实是一个DTO,它可以被序列化/反序列化,也可以被序列化为Data Contract而通过WCF在网络上自由传输。Apworks的基础结构层会通过DomainEventDataObject的属性定义,并结合一个给定的Storage Mapping Schema(也就是TinyLibrary.Services.DomainEventStorageMappings.xml文件),将DomainEventDataObject的数据保存到上面的数据表里。
在此简单介绍一下这个Storage Mapping Schema。由于我们使用的是关系型数据库,为了解耦“数据对象/属性”与“数据表/字段”的匹配,Apworks引入了Storage Mapping Schema,这个文件有点像NHibernate中的Mapping XML,但比NHibernate的Mapping XML简单很多:它不支持对数据对象关系与表关系的映射,它不是一个ORM。在Storage Mapping Schema中,仅仅简单地定义了数据对象/数据表,以及对象属性/字段的映射关系,这是由于,CQRS体系结构从实现上降低了关系型数据库的地位,定义数据表及其之间的关系已经不那么重要了。这里我又可以给出两种方案:如果你仍然希望在事件存储部分采用关系型数据库,并打算去维护复杂的数据表关系,那么,你可以不选用Storage Mapping Schema,而采用ORM(比如NHibernate),此时,DomainEventDataObject就是ORM上的“实体”;如果你不打算采用关系型数据库,而选择对象数据库(比如:Db4O),那么,你也不需要去维护任何的Mapping XML,对象数据库会帮你打理好一切,这将大大提高系统性能。以下是Storage Mapping Schema的XSD结构,以供参考。该XSD文件已被包含在Apworks应用开发框架的安装包里,用户可以在Apworks安装目录的scripts子目录中找到这个文件。
1: <?xml version="1.0" encoding="UTF-8"?>
2: <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
3: elementFormDefault="qualified"
4: attributeFormDefault="unqualified">
5: <xs:element name="StorageMappingSchema">
6: <xs:annotation>
7: <xs:documentation/>
8: </xs:annotation>
9: <xs:complexType>
10: <xs:sequence minOccurs="0">
11: <xs:element ref="DataTypes"/>
12: </xs:sequence>
13: </xs:complexType>
14: </xs:element>
15: <xs:element name="DataTypes">
16: <xs:complexType>
17: <xs:sequence minOccurs="0" maxOccurs="unbounded">
18: <xs:element ref="DataType"/>
19: </xs:sequence>
20: </xs:complexType>
21: </xs:element>
22: <xs:element name="DataType">
23: <xs:complexType>
24: <xs:sequence minOccurs="0">
25: <xs:element ref="Properties"/>
26: </xs:sequence>
27: <xs:attribute name="FullName" type="xs:string" use="required"/>
28: <xs:attribute name="MapTo" type="xs:string" use="required"/>
29: </xs:complexType>
30: </xs:element>
31: <xs:element name="Properties">
32: <xs:complexType>
33: <xs:sequence minOccurs="0" maxOccurs="unbounded">
34: <xs:element ref="Property"/>
35: </xs:sequence>
36: </xs:complexType>
37: </xs:element>
38: <xs:element name="Property">
39: <xs:complexType>
40: <xs:attribute name="Name" type="xs:string" use="required"/>
41: <xs:attribute name="MapTo" type="xs:string" use="required"/>
42: <xs:attribute name="Identity" type="xs:boolean" use="optional"/>
43: <xs:attribute name="AutoGenerate" type="xs:boolean" use="optional"/>
44: </xs:complexType>
45: </xs:element>
46: </xs:schema>
最后,在此给出Apworks应用开发框架中基于SQL Server的Event Store的类关系图,供大家参考。为了节省版面空间,此图中隐藏了类中的属性与方法定义,有兴趣的朋友可以到Apworks的站点http://apworks.codeplex.com上查看具体的代码实现。
在下一篇文章中,我将向大家介绍Tiny Library CQRS项目中,事件总线(Event Bus)与消息派送器(Message Dispatcher)的设计与实现,敬请期待!
CQRS体系结构模式实践案例:Tiny Library:领域仓储与事件存储相关推荐
- 领域驱动设计案例:Tiny Library:领域模型
本讲主要介绍基于Entity Framework的领域驱动设计建模.首先回顾一下Tiny Library的业务逻辑: 任何用户可以添加Library中的图书(简化起见,图书不能修改也不能删除),也可以 ...
- DDD的模式与实践案例
花名:神帅,毕业5年,混迹于大小厂打怪刷实战经验.资深Java开发工程师. 在企业服务领域和电商领域均有积累,最近一直在研究DDD和低代码领域,对后端微服务业务平台架构的实践和发展比较感兴趣. 公众号 ...
- 基于CQRS的架构在答题PK小游戏中的实践案例
1. 前言 \\ 领域驱动设计(Domain-Driven Design,下文简称 DDD)在微服务时代成为了风口话题,而在 DDD 领域,我们常常看到命令查询与职责分离(Command and Qu ...
- Kubernetes(K8s)容器设计模式实践案例 – 分散收集模式
<Kubernetes与云原生应用>专栏是InfoQ向轻元科技首席架构师王昕约稿的系列 文章.本专栏包含8篇内容,将会从介绍和分析Kubernetes系统以及云原生应用 入手,逐步推出基于 ...
- 从原理到策略算法再到架构产品看推荐系统 | 附Spark实践案例
原文链接:mp.weixin.qq.com 作者 | HCY崇远 01 前言 本文源自于前阵子连续更新的推荐系统系列,前段时间给朋友整理一个关于推荐系统相关的知识教学体系,刚好自身业务中,预计明年初 ...
- 神策数据荣获“2017金融科技·大数据优秀案例之最佳实践案例奖”
当前,金融市场活跃度不断提升,业务模式不断创新,金融领域数据量呈爆炸式增长,蓬勃发展的大数据产业给金融业的发展带来了新机遇,也提出了新的挑战. 6 月 29 日, 「数据猿·超声波」之金融科技商业价 ...
- 互联网创新创业大赛优秀范例_第五十九期创业沙龙——“互联网+”大学生创新创业大赛实践案例...
原标题:第五十九期创业沙龙--"互联网+"大学生创新创业大赛实践案例 第五十九期创业沙龙 第六届"互联网+".2020年"创青春"系列竞赛开 ...
- 再获认可|九州云获评2022分布式云与云边协同创新实践案例
6月14日,由中国信通院.中国通信标准化协会联合主办的"2022云边协同大会"顺利召开,会上正式发布2022分布式云与云边协同创新实践案例评选结果.九州云"5G专网+边 ...
- 【TOP100】100个中国大数据应用最佳实践案例—为您打开万亿元大数据产业的财富之门
热门下载(点击标题即可阅读) ☞[下载]2015中国数据分析师行业峰会精彩PPT下载(共计21个文件) 2017年3月28日至29日,由工业和信息化部指导.中国信息通信研究院和数据中心联盟主办的&qu ...
最新文章
- iterparse中的events参数start和end的用法
- [BZOJ2707]走迷宫
- linux查看修改环境变量日志,linux查看和修改PATH环境变量的方法
- 解决夜神模拟器无法联机调试 adb server version (**) doesn't match this client (**); killing...
- asp.net架构之请求处理过程:HttpModule,HttpHandler
- NLP《词汇表示方法(六)ELMO》
- HALCON学习之旅(七)
- java变量默为public_《Java8学习笔记》读书笔记(六)
- SQL server 2005安装问题汇总
- 2017.4.24 聪明的质检员 思考记录
- 【kafka】kafka Kafka分区leader迁移
- 剑指offer面试题[34]丑数
- ELK下钉钉邮件告警通知
- mysql 数据库备份的多种方式
- 正则表达式 相关教程
- SPSS Modeler 报错
- 专升本C语言知识点笔记
- java openxml word_OpenXml读取word内容的实例
- linux修改时区为UTC
- 微分几何学类毕业论文文献都有哪些?
热门文章
- 汇编程序设计与计算机体系结构软件工程师教程笔记:指令
- 海思3559A上编译libjpeg-turbo源码操作步骤
- 提高C++性能的编程技术笔记:跟踪实例+测试代码
- 八年级计算机网络公开课,计算机网络公开课教案.doc
- java instanceof 报错_java instanceof方法
- java监听组合按键_js监听组合按键
- c语言中平均值用什么表示_学C语言有什么用?
- Java项目:植物大战僵尸(java+swing)
- 【js】将json类型的数组或对象转为字符串
- 【jsp】页面跳转的两种方法