你们好,我叫汤雪华。我平时工做使用Java,业余时间喜欢用C#作点开源项目,如ENode, EQueue。我我的对DDD领域驱动设计、CQRS架构、事件溯源(Event Sourcing,简称ES)、事件驱动架构(EDA)这些领域比较感兴趣。我但愿把本身所学的知识可否分享给你们,因此,把这个领域里的一些知识串联了起来,整理了一个PPT,并为每张PPT配备注释,分享给你们。但愿能对这个领域有兴趣的朋友有所帮助。html
上面的提纲是今天主要分享的内容概要。开始以前想先说一下微服务架构和CQRS架构的区别和联系。node
微服务架构如今很热,处处能够看到各大互联网公司的微服务道路的分享总结。可是,我今天的分享和微服务没有关系,但愿能够带给你们一些新的东西。若是必定要说微服务和CQRS架构的关系,那我以为微服务是一种边界思惟,微服务的目的是为了从业务角度拆分(职责分离)当前业务领域的不一样业务模块到不一样的服务,每一个微服务之间的数据彻底独立,它们之间的交互能够经过SOA RPC调用(耦合比较高),也能够经过EDA 消息驱动(耦合比较低);mysql
微服务架构和CQRS架构的关系:每一个微服务内部,咱们能够用CQRS/ES架构来实现,也能够用传统三次架构来实现;git
首先,咱们须要先理解DDD中的聚合、聚合根这两个概念。github
聚合,它经过定义对象之间清晰的所属关系和边界来实现领域模型的内聚,并避免了错综复杂的难以维护的对象关系网的造成。聚合定义了一组具备内聚关系的相关对象的集合,咱们把聚合看做是一个修改数据的最小原子单元。聚合根,每一个聚合都有一个根对象,根对象管理聚合内的其余子对象(实体、值对象);聚合之间的交互都是经过聚合根来交互,不能绕过聚合根去直接和聚合下的子实体进行交互。上面的例子中,Car、Wheel、Position、Tire四个对象构成一个聚合,其中Car是聚合根;Customer也是聚合根,Customer不能直接访问Car下的Tire(子实体),而是只能经过聚合根Car来访问。sql
上面表达了一个关于聚合的一致性设计原则:聚合内的数据修改,是ACID强一致性的;跨聚合的数据修改,是最终一致性的。遵照这个原则,可让咱们最大化的下降并发冲突,从而最大化的提升整个系统的吞吐。数据库
In-Memory的意思是指整个系统中的全部的聚合根对象都活在内存。而不是像咱们平时那样,用到的时候才从DB获取对象,而后再作修改,再保存回去。浏览器
在In-Memory的架构下,当要修改某个聚合根的状态时,它已经在内存,咱们能够直接拿到该对象的引用,且框架会尽可能保证聚合根对象的状态就是最新的。聚合根是在内存中的最小计算单元,每一个聚合内部都封装了业务规则,并保证数据的强一致性。缓存
上图我是挪用了以前比较或的LMAX架构中的一个图,表达的思想就是in-memory架构。其中Business Logic Processor就是中央业务逻辑处理器,内部承载了大量在机器内存中活着的聚合根对象。服务器
接下来,咱们再来看一下什么是事件溯源。
一个对象从建立开始到消亡会经历不少事件,之前咱们是在每次对象参与完一个业务动做后把对象的最新状态持久化保存到数据库中,也就是说咱们的数据库中的数据是反映了对象的当前最新的状态。而事件溯源则相反,不是保存对象的最新状态,而是保存这个对象所经历的每一个事件,全部的由对象产生的事件会按照时间前后顺序有序的存放在数据库中。能够看出,事件溯源的这种作法是更符合事实观的,由于它完整的描述了对象的整个生命周期过程当中所经历的全部事件。
那么,事件到底如何影响一个领域对象的状态的呢?很简单,当咱们在触发某个领域对象的某个行为时,该领域对象会先产生一个事件,而后该对象本身响应该事件并更新其本身的状态,同时咱们还会持久化在该对象上所发生的每个事件;这样当咱们要从新获得该对象的最新状态时,只要先建立一个空的对象,而后将和该对象相关的全部事件按照事件发生前后顺序从先到后再所有应用一遍便可还原获得该对象的最新状态,这个过程就是所谓的事件溯源。
另外一方面,由于是用事件来表示对象的状态,而事件是只会增长不会修改。这就能让数据库里的表示对象的数据很是稳定,不可能存在DELETE或UPDATE等操做。由于一个事件就是表示一个事实,事实是不能被磨灭或修改的。这种特性可让领域模型很是稳定,在数据库级别不会产生并发更新同一条数据的问题。
经过上面这个图,你们应该能够更直观的理解事件溯源和传统CRUD思想的区别。
Actor模型,这个概念你们应该都了解。Actor模型的核心思想是,对象直接不会直接调用来通讯,而是经过发消息来通讯。每一个Actor都有一个Mailbox,它收到的全部的消息都会先放入Mailbox中,而后Actor内部单线程处理Mailbox中的消息。从而保证对同一个Actor的任何消息的处理,都是线性的,无并发冲突。从全局上来看,就是整个系统中,有不少的Actor,每一个Actor都在处理本身Mailbox中的消息,Actor之间经过发消息来通讯。
Akka框架就是实现Actor模型的并行开发框架,而且Akka框架融入了聚合、In-Memory、Event Sourcing这些概念。Actor很是适合做为DDD聚合根。Actor的状态修改是由事件驱动的,事件被持久化起来,而后经过Event Sourcing的技术,还原特定Actor的最新状态到内存。
上图表达的是事件驱动的架构的思想。Node表示节点,每一个节点负责处理逻辑;Event表示消息,节点之间经过消息进行通讯。消息经过分布式消息队列如RocketMQ,Equeue进行通讯。
事件驱动架构的核心思想是:
上图是一个面向Topic的分布式MQ的逻辑架构图,采用这种架构的MQ有:Kafka,RocketMQ,EQueue
好了,上面是基本概念的介绍。接下来咱们来看一下CQRS/ES架构。
上图是CQRS架构的典型架构图。
CQRS自己只是一个读写分离的架构思想,全称是:Command Query Responsibility Segregation,即命令查询职责分离,表示在架构层面,将一个系统分为写入(命令)和查询两部分。一个命令表示一种意图,表示命令系统作什么修改,命令的执行结果一般不须要返回;一个查询表示向系统查询数据并返回。
CQRS架构中,另一个重要的概念就是事件,事件表示命令操做领域中的聚合根,而后聚合根的状态发生变化后产生的事件。
因为CQRS架构的一致性模型为最终一致性,因此,你的系统要接受查询到的数据可能不是最新的,而是有几个毫秒的延迟。之因此会有这个前提,是由于CQRS架构考虑到,做为一个多用户同时访问的互联网应用,当在高并发修改数据的状况下,好比秒杀、12306购票等场景,用户UI上看到的数据老是旧的。好比你秒杀时提交订单前看到库存还大于0,可是当你提交订单时,系统提示你宝贝卖完了。这个就说明,在这种高并发修改同一资源的状况下,任何人看到的数据老是Stale的,即旧的。
这里我主要分享的CQRS架构是上面第3种实现方式,也就是上图所画的架构。在我心目中,只有第三种才是真正意义上的CQRS架构。
C端的命令的执行流程
客户端如(MVC Controller)发送命令通知系统作修改:
Q端的查询的执行流程
客户端如(MVC Controller)发出查询请求系统返回数据:
读库能够有不少种,依据咱们的业务场景来选择:好比关系型DB、分布式缓存等NoSQL、搜索引擎,etc.
前面的CQRS架构图我介绍了CQRS架构的基本概念、设计初衷、一致性模型、实现方式、适用场景、架构的基本数据流这些方面。但这不是CQRS架构的所有,咱们还能够挖掘出更多有用的特性出来。好比假设咱们为这个架构引入如下一些特性,就能够达到更多意想不到的好处:
经过引入上面这些架构设计原则,咱们可让CQRS架构的C端更强大,性能更高;固然,复杂性也大大增长。因此,要完成这样一套架构,没有成熟框架的支撑,是几乎不可能的,ENode框架就是在为作这样的一个框架而努力。
咱们能够从上面几个非功能性特性去考察这个架构。大部分你们应该均可以体会到,关于消息的幂等处理这块,CQRS\ES这个架构能够作的很是完全。
平时传统咱们的消息驱动的架构,或者是RPC调用的SOA风格的应用,消息处理者或者服务被调用方,必须本身作到数据修改的幂等性。而幂等性的实现思路也不少,好比用kv来判重,用DB的惟一索引,等等。
而CQRS\ES架构,因为使用了Event Sourcing的技术,因此能够直接在EventStore中自动作到聚合根并发修改的冲突的检测、以及同一个命令的重复处理的检测。并能通知框架自动作并发处理或作从新发布该命令所产生的事件;
你们可能会疑问,为什么已经将命令经过聚合根ID进行路由了,且同一台机器内页已经经过Actor Mailbox技术解决并发问题了,仍是有并发冲突的可能呢?缘由是当咱们的服务器在出现扩容或缩容时,会出现因为集群中服务器变更致使的同一个聚合根的不一样命令可能会在不一样的机器上同时被处理,从而致使并发冲突。
最后,关于这个架构的瓶颈,相信你们已经能够发现,是在EventStore。因此,这就要求咱们设计一个超高性能的EventStore数据库。具体见后面的介绍吧。
上面这个图演示了,当C端产生的事件,在Q端的处理顺序若是不一致时,致使Q端的结果和C端不一致了。因此,事件的处理顺序必须和产生的顺序一致,这点必须保证,但能够由框架来保证,开发者无需关注。须要强调的是,这个顺序处理事件不须要交给分布式消息中间件来保证,而是应该交给Consumer来本身保重。当Consumer收到一个版本为N+2的时间,而当前Q端的版本为N,则N+2的消息须要先hold一下,不要当即处理。而后等待N+1的事件过来,N+1的事件过来并处理后,再处理N+2的事件。若是N+1的事件一直不过来,则须要永远等待。总之,这里的顺序必须保证。若是这个顺序交给分布式消息中间件去保证,那性能上会很是差,而要让分布式消息中间件实现绝对意义上的顺序消费,又要实现高可用,高性能,难度很大。我我的不太同意,除非是Consumer本身没法处理消息顺序的场景才无可奈何让分布式消息中间件来保证,好比mysql binlog的同步。
上图演示了假设一个命令修改两个或多个聚合根时,会致使阻塞大大增长,从而整个系统的吞吐会下降。而好处是,咱们能够获得聚合根之间的数据的强一致性。
上图演示了,当一个命令只修改一个聚合根时,先经过一级路由,将聚合根路由到分布式MQ的同一个队列里,而后同一个队列老是被一台固定的机器消费,从而保证同一个聚合根的命令老是在一台机器上处理。
上图掩演示了,当命令进入一台机器后,再经过Command Mailbox的二次路由,一样是根据聚合根ID,从而保证单个机器内,同一个聚合根的命令的处理是顺序线性的,从而避免了并发冲突。
EventStore处理并发和命令幂等的根本设计就是上图的两个惟一索引。
1. 聚合根ID + 事件版本号惟一;
2. 聚合根ID + 命令ID惟一;
当万一出现了并发冲突,则框架须要取出从新加载该聚合根的最新状态,而后重试当前命令;当出现了命令的重复处理,则框架须要把该命令以前产生的事件再从新取出来,发布到分布式消息中间件。由于有可能以前虽然这个事件被持久化了,但理论山有可能这个事件没有成功发布到分布式消息中间件(由于那个时候断电了,够倒霉的,呵呵)。因此,事件的消费者可能会再次收到这个事件,并处理。但这么作都是为了保证整个业务流的最终一致性。想一想以前的EDA的架构图的说明吧。
下面咱们来看看CQRS架构下,开发者须要写的代码有哪些?
首先是须要定义Command和Event。其中Command至关于DDD经典四层架构中的应用层的一个方法的参数。
Command表示命令系统作什么,表达一种意图,在架构上设计为一个DTO便可。Event表示一个事件,表示领域内发生了什么状态变化,用过去式命名事件。事件是只读的。
Command Handler是无状态的,用于处理一个或多个命令,不一样的命令有不一样的Handle方法。一个Command Handler作的典型的事情就两个:
框架能够作到开发人员无需关注底层的技术问题,好比如何存储聚合根产生的事件,如何发布事件到MQ;完全作到技术架构和业务逻辑分离。这点在传统架构下是很难作到的。
Note表示一个DDD聚合根,这里最核心的概念是:Note内部的状态的修改都是经过事件来驱动的,也就是Note要作任何修改前,老是应该先产生事件,而后框架根据事件调用到对应的Handle方法,而后咱们在Handle方法中修改Note的内部状态。
为什么要独立拆分出Handle方法呢?由于是在Event Souring事件溯源还原聚合根状态时,框架须要调用这些Handle方法。根据Event Sourcing的思想,会根据Note聚合根的ID获取该聚合根的全部的事件,而后按照事件发生的顺序,分别调用每一个事件的Handle方法,就能够还原出聚合根的最新状态了。
最后一个须要开发者写的代码就是Event Handler,根据CQRS架构的定义,Event Handler负责根据C端产生的事件来更新读库。上面的例子只是记录日志,实际咱们须要在Handle方法中更新读库,如数据库,分布式缓存等。
这是ENode中今年打算实现的文件版本的EventStore的设计思路,目前是使用的DB来实现的。我如今在作EQueue的高可用,等这个作完,就开始作EventStore的文件版本。上面PPT中的设计思路,还但愿能和你们多多交流,一块儿完善它。由于它是整个CQRS/ES架构的核心所在。
前面介绍了不少CQRS\ES架构方面的东西,最后咱们再看两个实际应用的场景:秒杀、12036购票。
要实现高并发的订单处理(生成订单、预扣库存两个核心步骤)。淘宝作的很牛逼,能够在这两个步骤都完成后直接告诉用户下单结果,固然,我认为CQRS架构也彻底能够在保证这两点处理后再返回买家的前提下,实现淘宝同样的吞吐。
这里我列举这些订单状态的目的,主要是想表达第一个状态用意:订单处理中。经过引入这个状态,咱们处理订单的的代价就轻不少了,不须要在完成生成订单、预扣库存这两个核心步骤就能够返回客户端浏览器了。买家订单提交成功后,服务端首先在分布式缓存中检查商品的库存是否足够,若是不够,则当即返回并通知买家宝贝卖完了;若是足够,则发送下单的命令到MQ(异步处理订单)。而后通知买家“您好,您的订单已收到,正在处理中。请稍后到个人订单中心查看订单处理结果。祝您购物愉快!”之类的提示。
而后当买家进入“个人订单中心”查看订单时,可能的状况有:
经过这样的订单状态的设计和交互体验,至关于把轮训查看订单处理结果的职责交给了买家。而这个小小的设计,带来的好处是极大的方便咱们实现很是高的订单处理吞吐了。固然,若是咱们能作到像淘宝这样的体验,就是下单时直接告诉结果,那天然最好了。只是这样代价更大而已。我提出这个例子的缘由是CQRS架构是一种C端异步处理命令的架构,因此在这种架构上,咱们须要一切尽可能以异步为出发点去思考和设计业务流程,设计用户交互体验。实际上这个体验在亚马逊上买东西,你可能会遇到,甚至亚马逊直接让你去你的邮箱看订单处理结果。因此,我以为这里只是一个购物习惯的差异,但对技术的要求却差异很大。
上图描述了一个DDD CQRS架构的典型的Saga的设计,对应前面的秒杀场景的订单处理流程。
上图中,Order、Conference、Payment为三个聚合根,分别表示订单、库存、支付;Order Process Manager是无状态的,表示一个流程管理器,CQRS架构中通常叫Saga。流程管理器的设计理念是:订阅事件,根据不一样的事件,发送不一样的命令。也就是说,流程管理器的职责是对流程进行建模,负责封装流程控制逻辑,而聚合根负责业务逻辑。整个订单处理的流程大概为业务层面的2PC。即下单时,要先预扣库存;而后,买家付款后要真正扣库存。
上图中,棕色的线条表示命令,蓝色的线条表示事件。
Saga是CQRS架构中处理复杂业务流程的典型作法,经过事件驱动的方式去替代传统的分布式事务。牺牲强一致性的方式来提升系统的吞吐。实际上,在高并发的状况下,有时咱们不得不选择最终一致性,由于分布式事务的成本过高。
这个案例是关于12306购票的例子,上面说了核心的业务场景和领域概念。我举这个例子的用意是为了说明,12306购票的场景,C端的领域模型是比传统的电商网站要复杂不少的,由于库存是一个动态的概念。不像普通电商,一个库存跟着SKU,很简单。12306你买了一个车子的某个区间的票以后,这个区间内的其余的票的库存数都会发生变化,并且这个库存数还要考虑座位的分配,很是复杂。
这个场景,就是我上面说的CQRS的应用场景中的:要知足高并发的写、高并发的查询,同时C端的业务模型很是复杂。要同时面对这3点,实现这个系统是很难的。
我认为,这个场景的难点不在于技术层面,而是在于DDD领域建模层面。你们若是对这个场景的领域模型,架构实现,以及示例代码感兴趣,能够看我下面的两个地址:
浅谈12306核心模型设计思路和架构设计
http://www.cnblogs.com/netfocus/p/5187241.html
12306购票领域建模示例代码:
https://github.com/tangxuehua/enode,具体看ENode开源项目中的E12306案例代码。
若是你们对这个领域感兴趣,能够访问个人博客。我博客中录制了大量的视频介绍,视频介绍汇总地址:
http://www.cnblogs.com/netfocus/p/4707789.html
谢谢你们!