当前位置: 首页 > 编程日记 > 正文

CQRS体系结构模式实践案例:Tiny Library:领域仓储与事件存储

image 领域仓储(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)或者其它的系统能够接收到事件到达通知而做进一步的处理。

当客户程序需要通过领域仓储读取聚合时,领域仓储就会新建聚合,然后从事件存储中,以该聚合的聚合根的类型作为搜索条件,将领域事件按顺序读取出来并一个个地应用在这个新建的聚合上,聚合根实体一旦捕获到事件,就会按照事件的数据内容更新对应的状态,于是,聚合也就被恢复到了最后一次事件发生后的状态了。

这个过程很简单,通过上面的分析不难发现:

    1. 由于查询部分的分离,领域仓储仅存两种操作:将聚合保存到事件存储以及从事件存储还原对象。与之对应的操作大致可以表示成下面的接口(该部分代码摘录自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: }
    1. 整个Domain Model只有一个数据源:事件存储(Event Store),用来保存所有发生在聚合上的领域事件。这个Event Store具体如何设计,可以根据应用系统的需求来决定,但总归是非常的简单,甚至于仅用一张关系型数据库的数据表就可以实现。对于采用关系型数据库实现的事件存储,由于数据表数量很少,而且之间的关系变得非常简单,于是ORM就可以省略,直接采用Direct SQL实现;如果不采用关系型数据库作为事件存储,那可以选择的范围就更大了:各种NoSQL数据库、对象数据库、内存数据库等等。就关系型数据库而言,我们可以对事件存储所使用的数据表做如下的设计:
      image
      目前,Tiny Library CQRS赖以生存的开发框架Apworks,仅提供支持SQL Server的Event Store设计(Apworks当前版本:Alpha,v1.0.4016.23016)
    2. 如果事件存储采用的是关系型数据库,领域仓储对事件存储,原则上也只有类似如下两种操作:
   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的单表事件存储。表结构如下:

image

首先,领域仓储从聚合获得未保存(即未提交)事件,然后,使用指定的序列化方式,将事件序列化为二进制流,并保存到Apworks.Events.Storage.DomainEventDataObject对象中,这个对象其实是一个DTO,它可以被序列化/反序列化,也可以被序列化为Data Contract而通过WCF在网络上自由传输。Apworks的基础结构层会通过DomainEventDataObject的属性定义,并结合一个给定的Storage Mapping Schema(也就是TinyLibrary.Services.DomainEventStorageMappings.xml文件),将DomainEventDataObject的数据保存到上面的数据表里。

image

在此简单介绍一下这个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上查看具体的代码实现。

image

在下一篇文章中,我将向大家介绍Tiny Library CQRS项目中,事件总线(Event Bus)与消息派送器(Message Dispatcher)的设计与实现,敬请期待!

相关文章:

​中国开启开源新纪元

距离“中国 Linux 第一人”宫敏博士用手提肩背的方式&#xff0c;将 20 盒装有 80G 容量的自由软件磁带背回中国转瞬已过 20 载&#xff0c;在宫敏博士的推动下&#xff0c;中国组建起国内第一个自由软件库&#xff0c;由此开源在第一代开发者心中的火种迅速传播。20 多年间&am…

kernel logo到开机动画之间闪现黑屏(android 5.X)

在BootAnimation開始画图之前&#xff0c;会先做一次clear screen的动作&#xff0c;避免出现前面的图干扰到BootAnimation的显示。 通过check main_log先确认播放开机动画是哪个function。在相应function删除clear screen的动作的相应代码。 /frameworks/base/cmds/bootanimat…

JScript Array对象的几个原型方法

代码 Array.prototype.inArray function(value) { for(vari 0; i <this.length; i) { if(this[i] value) { returntrue; } } returnfalse;};Array.prototype.max function() { for(vari 1, max this[0]; i <this.length; i) { …

太生猛!AI应届生年薪涨到80万!网友:后悔生的太早

据中国青年报报道&#xff1a;新冠肺炎疫情期间&#xff0c;非接触类交互、安全卫生等需求提升&#xff0c;以数字货币、数据应用、人工智能为代表的数字经济显著发展&#xff0c;全球经济数字化转型踩下“油门”。中国人工智能人才很可能缺口超过500万人。其实&#xff0c;早在…

Tomcat定时任务

原文: The load-on-startup element indicates that this servlet should be loaded (instantiated and have its init() called) on the startup of the web application. The optional contents of these element …

Nutanix CE on Lenovo W520 初探

話說 Nutanix 出了 CE 版本&#xff0c;這當然要來試試看 Nutanix 威力拿了部機器lenovo W520 CPU: i7-2820QM 4 coreRam: 32GBHDD:500G安裝選單選擇鍵盤配置&#xff0c;按下 ProceedNutanix 開始載入程式硬件最低需求不滿足Laptop 32G 內存Lenovo W520Nutanix 官方 CE 最低硬…

《爱情公寓2》将播 恶搞宣传片大喊“有种别看”

昨晚&#xff0c;一支重口味的宣传片消然出现在各大网站&#xff0c;一夜之间在狂转发。上线不到一小时&#xff0c;单一网站浏览量就超十万&#xff0c;视频主标写着&#xff1a;神兽组合从天而降&#xff0c;口味超重&#xff0c;少儿不宜&#xff0c;人兽悲剧&#xff0c;蛋…

“应付”大学作业,我花3小时写了一个“文本转手写”神器

作者 | Saurabh Daware译者 | 弯月&#xff0c;责编 | 郭芮来源 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09;最近&#xff0c;有一个名叫Saurabh Daware的印度大学生只花了3个小时就编写了一款自动化工具&#xff0c;能够将文本转换成手写文字&#xff0c;并用这个工…

Laravel应用

CLI 参考&#xff1a;http://laravel-china.org/docs/5.1/artisancli处理业务&#xff0c;把业务封装成一个命令&#xff0c;用php artisan来调用自定义的命令放在App/Console/Commands下创建方式php artisan make:console 命令名 —command调用时名字php artisan make:console…

匿名内部类和传接口

匿名内部类也就是没有名字的内部类正因为没有名字&#xff0c;所以匿名内部类只能使用一次&#xff0c;它通常用来简化代码编写但使用匿名内部类还有个前提条件&#xff1a;必须继承一个父类或实现一个接口 参考&#xff1a;http://www.cnblogs.com/nerxious/archive/2013/01/…

为什么这门技术如此重要?错过这次黄金期,就晚了!

老李一直怀疑自己是不是年纪大了&#xff0c;脑子跟不上了。作为十几年经验的资深 Java 工程师&#xff0c;维护这公司产品的核心代码的他&#xff0c;现在迭代产品的时候&#xff0c;经常出 Bug 。有时修复一个 Bug 时间&#xff0c;比开发一个需求的时间要长很多&#xff0c;…

字符编码简介 ANSI Unicode Unicode big endian UTF-8

1. ASCII码 我们知道&#xff0c;在计算机内部&#xff0c;所有的信息最终都表示为一个二进制的字符串。每一个二进制位&#xff08;bit&#xff09;有0和 1两种状态&#xff0c;因此八个二进制位就可以组合出256种状态&#xff0c;这被称为一个字节&#xff08;byte&#xff0…

ReactiveCocoa代码实践之-更多思考

三.ReactiveCocoa代码实践之-更多思考 1. RACObserve()宏形参写法的区别 之前写代码考虑过 RACObserve(self.timeLabel , text) 和 RACObserve(self , timeLabel.text) 的区别。 因为这两种方法都是观察self.timeLabel.text的属性&#xff0c;并且都能实现功能。估计是作者原本…

Java常用命令及Java Dump

线程Dump,包含所有线程的运行状态。纯文本格式。 堆Dump,包含线程Dump,幵包含所有堆对象的状态。二进制格式。 Java Dump方法 1.使用Java虚拟机制作Dump 指示虚拟机在发生内存不足错误时,自动生成堆Dump -XX:HeapDumpOnOutOfMemoryError 2.使用图形化工具制作Dump 使用JDK…

使用Windows远程登录Ubuntu

一、SSH登录 1、Ubuntu默认没有安装SSH &#xff0c;可以在新得利软件安装程序里&#xff0c;搜索SSH&#xff0c;标记并安装&#xff1b; 或者使用命令&#xff1a; sudo apt-get install openssh-server sudo /etc/init.d/ssh restart ssh localhost…

紧急更新下降难度,《王者荣耀》绝悟 AI 难倒一片玩家

作者 | 神经星星来源 | HyperAI超神经&#xff08;ID: HyperAI&#xff09;在 5 月 1 日~ 5 月 4 日期间&#xff0c;玩家通过《王者荣耀》最新版本客户端进入游戏&#xff0c;即可与绝悟 AI 对战。一时间哀鸿遍野&#xff0c;普通玩家、游戏主播、职业选手&#xff0c;纷纷表示…

SQL:安装多个实例,修改实例端口号,和IP加端口号连接实例

原文:SQL&#xff1a;安装多个实例&#xff0c;修改实例端口号,和IP加端口号连接实例sql server 安装第一个实例&#xff0c;默认实例的端口是1433&#xff0c; 一个库中如果有多个实例&#xff0c;从第二个实例开始的端口是动态端口&#xff0c;需要的话&#xff0c;自己手工指…

用“逐步排除”的方法定位Java服务线上“系统性”故障

说明&#xff1a;原文地址已经不可访问&#xff0c;其他地方有转载&#xff0c;不过很多丢失图片&#xff0c;所以&#xff0c;找到一处有图的重新配好图。 用“逐步排除”的方法定位Java服务线上“系统性”故障 Posted on 2014/08/25李斯宁&#xff08;高级测试开发工程师&…

清华硕士爆料:这些才是机器学习必备的数学基础

现如今&#xff0c;计算机科学、人工智能、数据科学已成为技术发展的主要推动力。无论是要翻阅这些领域的文章&#xff0c;还是要参与相关任务&#xff0c;你马上就会遇到一些拦路虎&#xff1a;想过滤垃圾邮件&#xff0c;不具备概率论中的贝叶斯思维恐怕不行&#xff1b;想试…

LINUX环境下资源下载中文目录及中文文件名称问题

为什么80%的码农都做不了架构师&#xff1f;>>> http://www.yeeach.com/2009/04/09/linux%E7%8E%AF%E5%A2%83%E4%B8%8B%E8%B5%84%E6%BA%90%E4%B8%8B%E8%BD%BD%E4%B8%AD%E6%96%87%E7%9B%AE%E5%BD%95%E5%8F%8A%E4%B8%AD%E6%96%87%E6%96%87%E4%BB%B6%E5%90%8D%E7%A7%B…

dojo从asp.net中获取json数据

搞来有搞去终于有了个结果&#xff0c;主要是一开始犯了一些低级错误。 对于json不太了解的童鞋&#xff0c;可以看看这个&#xff1a;http://www.dreamdu.com/blog/2008/10/19/json_in_javascript/ 这个例子中主要是从数据库中读取数据&#xff0c;转换成JSON格式&#xff0c;…

RHEL5 install

RHEL5 安装转载于:https://blog.51cto.com/bhanv/477708

线上java问题排查

0.jps 这个输出java进程pid #jps 查看java的线程 #top -Hp 25448 如图25757这个线程比较耗时&#xff0c;看看他在做什么 注意需要折算出线程pid的16进制值&#xff0c;然后jstack。 可以打印更多信息 #jstack pid | grep -A 20 649d 参考&#xff1a;JVM调优之jstack找出…

GitHub标星10,000+,Apache项目ShardingSphere的开源之路

【编者按】几天前&#xff0c;当 GitHub 全球产品技术生态总经理 Michael Francisco 谈到中国开发者已经成为 GitHub 上最活跃的群体时&#xff0c;有开发者提出数量之后质量也要跟上。的确&#xff0c;过去十数年间&#xff0c;中国开源一直呈现企业热使用热社区冷开发冷的景象…

JAVA中LOCK

原文链接&#xff1a;http://www.cnblogs.com/dolphin0520/p/3923167.html 一.synchronized的缺陷 我们知道如果一个代码块被synchronized修饰了&#xff0c;当一个线程获取了对应的锁&#xff0c;并执行该代码块时&#xff0c;其他线程便只能一直等待&#xff0c;等待获取锁的…

【公开课预告】AutoML知多少

5月7日周四19:00&#xff0c;商汤泰坦公开课第010期&#xff0c;论文解读系列课程第二期即将开播&#xff01;我们邀请到商汤科技的4位研究员&#xff0c;分享团队在AutoML方面的一系列研究工作&#xff0c;其中包含CVPR 2020、ICLR 2020等多篇最新论文成果&#xff0c;想要了解…

Linux kernel futex.c的bug导致JVM不可用

JVM死锁导致线程不可用&#xff0c;然后会瞬间起N个线程&#xff0c;当然也是不可用的&#xff0c;因为需要的对象死锁&#xff0c;然后耗尽文件句柄导致外部TCP无法建议拒绝服务&#xff0c;jstack之后就会恢复。 解决办法&#xff1a;替换中间件类库 &#xff0c;比如httpcli…

ruby爬虫综述

http://ihower.tw/blog/archives/2941一个ruby爬虫的例子http://hi.baidu.com/anspider/blog/item/9da210425a0e4e179213c6fb.html

Exchange 2016集成ADRMS系列-12:域内outlook 2010客户端测试

接下来&#xff0c;我们来到域内安装了office 2010的机器上进行测试。 首先我们在客户端上强制刷新组策略&#xff0c;把我们刚才设置的策略刷新下来。 然后我们可以运行gpresult /h result.html来看看策略是不是已经下来了。 策略下来之后&#xff0c;我们打开客户端上面的out…

在Linux下编写Daemon

在Linux下编写Daemon 转自&#xff1a;http://blog.163.com/prevBlogPerma.do?hostmanyhappy163&srl1644768312010718111142260&modeprev 在Linux&#xff08;以Redhat Linux Enterprise Edition 5.3为例&#xff09;下&#xff0c;有时需要编写Service。Service也是…