使用CQRS重新考虑架构

命令查询的责任分离Command Query Responsibility Segregation (简称CQRS)模式是一种架构体系模式,能够使改变模型的状态的命令和模型状态的查询实现分离。这属于DDD应用领域的一个模式,主要解决DDD在数据库报表输出上处理方式。

来自Rethinking architecture with CQRS一文对CQRS进行详细描述。

很多应用都需要持久层保存状态,因为这些状态不能丢失,如果只是放在内存中,而不是持久到磁盘上。状态是在领域模型中,每当模型保存到数据库以后,会有更多复杂的查询,都是使用复杂且慢的SQL语句实现的。比如如下复杂SQL语句:


queryBuilder.append(
"m.*, " +
"m.origin_participant_id as message_origin_participant_id, " +
"po.first_name as message_origin_participant_first_name, " +
"po.avatar as message_origin_participant_avatar, " +
"po_ua.username as message_origin_participant_username, " +
"CASE WHEN !isnull(po.fieldworker_project_id) THEN 'fieldworker' WHEN !isnull(po_ap.project_id) THEN 'fundraiser' ELSE 'player' END AS message_origin_participant_type, " +
"po.city as message_origin_participant_city, " +
"c.name as message_origin_participant_country, " +
"pd.first_name as message_destination_participant_first_name, " +
"pd.avatar as message_destination_participant_avatar, " +
"pd_ua.username as message_destination_participant_username, " +
"CASE WHEN !isnull(pd.fieldworker_project_id) THEN 'fieldworker' WHEN !isnull(pd_ap.project_id) THEN 'fundraiser' ELSE 'player' END AS message_destination_participant_type, " +
"m.destination_participant_id as message_destination_participant_id " +
"from internal_message m " +
"left join player po on m.origin_participant_id = po.id " +
"left join (select player_id, project_id from ambassador_project where enabled = true group by player_id) po_ap on po_ap.player_id = po.id " +
"left join user_account po_ua on po.user_account_id = po_ua.id " +
"left join country c on po.country_id = c.id " +
"left join player pd on m.destination_participant_id = pd.id " +
"left join (select player_id, project_id from ambassador_project where enabled = true group by player_id) pd_ap on pd_ap.player_id = pd.id " +
"inner join user_account pd_ua on pd.user_account_id = pd_ua.id " +
"where m.destination_participant_id = ? "
);

如此复杂的SQL语句为什么不能用一句简单SQL完成呢?


SELECT * FROM messages WHERE receiving_participant = ?

如果你要这么做,就必须使用到CQRS模式。

Greg Young在infoQ的采访中“State Transitions in Domain-Driven Design”谈到了CQRS,Greg 解释了把领域模型分为两种:状态校验,以及状态转换,维持当前状态的一个视图。

CQRS架构如下图:

在客户端就将数据的新增修改删除等动作和查询进行分离,前者称为Command,走Command bus进入Domain对模型进行操作,而查询则从另外一条路径直接对数据进行操作,比如报表输出等。

当一个Command进来时,从仓储Repository加载一个聚合aggregate对象群,然后执行其方法和行为。这样,会激发聚合对象群产生一个事件,这个事件可以分发给仓储Repository,或者分发给Event Bus事件总线,比如JavaEE的消息总线等等。事件总线将再次激活所有监听本事件的处理者。当然一些处理者会执行其他聚合对象群的操作,包括数据库的更新。

因为领域对象操作和数据库保存持久这两个动作分离,因此,数据表结构可以和领域对象松耦合(JiveJdon源码可展示领域对象和数据表不再是一对一对应依赖,这也是使用Hibernate 等ORM框架容易造成的问题),你可以优化数据表结构专门用于查询。

再者,由于事件驱动了领域模型的状态改变,如果你记录这些事件audit ,将可以将一些用户操作进行回放,从而找到重要状态改变的轨迹,而不是单纯只能依靠数据表字段显示当前状态,至于这些当前状态怎么来的,你无法得知。当你从数据库中获得聚合体时,可以将相关的事件也取出来,这些叫Event Sourcing,事件源虽然没有何时何地发生,但是可以清楚说明用户操作的意图。

虽然这种架构有些复杂,但是好处却很多,主要的是实现透明的分布式处理Transparent distributed processing,当使用事件作为状态改变的引擎时,你可以通过实现多任务并发处理,比如通过JVM并行计算或事件消息总线机制,事件能够很容易序列化,并在多个服务器之间传送,(EJB提倡贫血失血模型,实际就是为解决胖模型在多个服务器之间传送时序列化耗费性能,现在我们不序列化模型,而是改变模型数据的事件)。

你可以让聚合体(胖模型)一直驻留在特定服务器中,这样可以为不同的应用选择不同的SLA。

由于事件处理是异步的,用户就不必等待所有操作完成(同步则是所有操作完成后才返回操作结果界面给用户,异步架构符合Jdon一直提倡的异步架构),CQRS实际可以利用BASE原理(CAP定律和BASE思想),实现最终一致,取得高可伸缩性,尽管最终一致使很多程序员有些恐慌,但是分布式ACID 事务却也让他们产生怨恨,其实最终一致其实最多是几个毫秒的延迟,但是你可以充分利用这几秒的延迟做更多事情。

当你使用异步事件模式以后,可以使用Esper 等EDA工具进行事件拦截和分析,该文作者就是利用它实现 RSA Adaptive Authentication用户登录后的授权处理。

最后该文作者推荐了一个基于Spring的CQRS的框架cqrs4j,这个框架是5天前刚刚推出,不过了解Jdonframework的人也许知道,Jdon框架6.2也是可以实现CQRS,不过是在同年11月上架的,且不谈两个框架比较,单理念上Jdonframework是世界同步的,说明世界不同角落里人都想到同一个问题了,本文很多理念其实在J道网站已经倡导多时,并且通过Jdonframework的Domain Events + 异步实现了。

相关CQRS 讨论

[该贴被banq于2009-12-24 14:54修改过]

呵呵,不错。看来大家对DDD的认识和我们的想法逐渐吻合了。DDD+cache+Domain event+BASE==Jdonframework.

这篇文章其实提出了一个颠覆性的概念:分布式系统之间传输事件,把事件作为数据进行分布,当然也可以把事件作为key-value存储的数据。

目前EJB标准JavaEE6等都明显落后了,透明化out-of-box分布式架构是一个趋势。

这个概念和EJB提出的将实体数据在服务器之间传输完全相反,所以,EJB才积极倡导失血模型,就是模型没有方法(只有setter/getter方法),只有字段数据,没有行为,就是不能太胖,否则序列化性能就很差,但是没有行为的模型能称为对象模型吗?就像人失去血液能活吗?所以,失血模型是严重违背OO的,但是因为EJB把持着实现市场,所以,连同关系数据库编程思路,误导了一代程序员。

Spring作为另外一个实现市场,虽然Spring 3.0出来了,但是由于其只对组件技术架构实现支持,对模型实体不闻不问(由Hibernate掌管),没有解决模型事件和技术组件架构之间的通讯联系方式,结果,导致大部分程序员使用SSH或S2SH时,只是由服务触发模型,模型无法再触发其他服务或组件(模型最后成为事件最后终点站,这也是失血模型的一个特点,数据是被驱动的,但是模型对象不是数据,模型对象有行为,必须可以发出事件驱动其他),或者将服务或组件直接注射到模型中,导致业务模型和技术架构多点耦合。

以上矛盾也体现了EDA架构和所谓SOA架构的冲突所在。

作为一个好的框架,不对模型进行in-memory支持,而是通过Hibernate的二级缓存ehcache绕道实现,这是当前Spring+Hibernate(SSH或S2SH)的最大问题所在。另外Hibernate其实是将领域模型和关系数据库进行捆绑,反而让领域模型丧失了其为需求服务的自由,比如异步加载,即用即取lazy load等等。

所以,从DDD要求出发,一定要有一个综合将实体模型和服务组件技术架构合起来考虑的框架,比如Qi4j或Jdonframework,Spring + Hibernate这种分离思路其实是沿袭了EJB的Session Bean + Entity Bean/JPA思路,这种思路离DDD要求实在太远,可以看成是模型的汇编语言了。


[该贴被banq于2009-12-24 19:35修改过]

Map-reduce讲究:移动计算比移动数据更加廉价,它将大量的数据分为小的block分发给集群上的node,然后在每个集群node上面对切分后的block进行运算。

而当前Model是充血的,必然会聚合很多数据,但是通过事件,将聚合根模型中的与事件关联的模型数据或者子对象通过EDA架构分发出去,这一分发就为系统伸缩性奠定了一个良好的基础,其实这种事件分发也是一种切分,一种基于事件的切分,一种新型并行编程模式,当前jdon框架事件触发异步以后是通过线程池实现的,如果以后需要更强的运算能力和伸缩能力,可以采用JMS来对消息进行真正分布式并行计算。看来大千世界,思想的本质都是一致的。

看了banq的介绍和解释,突然有点豁然开朗的感觉。

以前的编程j2ee都是宣扬通过三层的构架方式,并且大家都在传道这样的方式符合组件化、模块化、伸缩性等都是多么多么的好。对于新手来说开始只能相信,到最后的习惯,真的是影响了一代程序员。可是在做了几年的j2ee开发以后,发现三层结构的那些优点也只是一厢情愿,君不见修改一处则动全身;发现大家一上来的是讨论po怎么建,数据库如何设计。

我们一边在说软件是对现实的抽象和模拟,一边说软件是一门艺术,那么采用这样的方式我们的艺术在哪里,且不说艺术,我们的软件和现实状况的一致性有多少距离,我想应该可以用十万八千里来形容。

我个人的理解,DDD就是用最符合现实、最符合实际运行模式的方式去编写软件。domain,repository,service,command,events这些都是现实业务流程中最真实的名词,最符合人类思考的方式和习惯,其实如果的软件能够完全符合这些特征,那么软件这个时候才真的是一门艺术了。

摘录
以前的编程j2ee都是宣扬通过三层的构架方式,并且大家都在传道这样的方式符合组件化、模块化、伸缩性等都是多么多么的好。对于新手来说开始只能相信,到最后的习惯,真的是影响了一代程序员。可是在做了几年的j2ee开发以后,发现三层结构的那些优点也只是一厢情愿,君不见修改一处则动全身;发现大家一上来的是讨论po怎么建,数据库如何设计。

我们一边在说软件是对现实的抽象和模拟,一边说软件是一门艺术,那么采用这样的方式我们的艺术在哪里,且不说艺术,我们的软件和现实状况的一致性有多少距离,我想应该可以用十万八千里来形容。

我个人的理解,DDD就是用最符合现实、最符合实际运行模式的方式去编写软件。domain,repository,service,command,events这些都是现实业务流程中最真实的名词,最符合人类思考的方式和习惯,其实如果的软件能够完全符合这些特征,那么软件这个时候才真的是一门艺术了。


有人开发软件把它数学化
有人开发软件把它艺术化

而jdon所倡导并且实践的是艺术化和数学化结合。所以顶一下楼上的发言。
[该贴被oojdon于2009-12-26 16:23修改过]

hibernate JPA等ORM框架由于将关系数据表和实体对象绑定,如果我们直接将这个实体对象作为领域模型的实体(如果不是,系统中有两种实体对象,增加复杂性),那么无疑无法进行查询和命令(比如模型的修改)的分离,也就是是反CQRS模式。

CQRS和关系数据库的读写分离策略也是相似的,查询是读,而状态修改一般是写,CQRS允许我们为查询优化数据表结构,因为对象领域模型的存储,数据表结构是无所谓的,但是如果你使用Hibernate/JPA等ORM框架,就很难实现为查询优化数据表结构,因为ORM框架绑定领域模型和数据表。

这里是一个DDD的Domain Driven Design PPT,也谈到了CQRS,也称CQS架构,很不错。

下面这张图也是CQS架构图:

[该贴被banq于2009-12-29 11:35修改过]


不知道 bang 目前这种架构 有可实现的范例么? 最好有example 学学~~~ 另外国外有大型的应用么?

还有个问题,对于查询,后台的存储架构,以及与主数据库的数据一致性管理,如何具体实现呢? 不知道bang 有何高见?

2009年12月29日 12:55 "conquersky"的内容
还有个问题,对于查询,后台的存储架构,以及与主数据库的数据一致性管理,如何具体实现呢? 不知道bang 有何高见?

这里有两种一致性,一个用户界面数据的一致性,还有一个是你说的数据库数据的一致性。

我们通常比较关心的是前者,至于你说的后者,技术上完全没有问题了,比如采取Key-values存储作为数据库即可。

关于是否有实践,你发言的这个论坛就是使用这种架构,因为Jdonframework就是根据这种原理开发的。至于其他大型实践架构,实际可以参看NoSQL实践,两者一致的,NOSQL大型实践案例包括EBay Facebook twitter linkedin等世界排名前茅的网站。

关于CQRS,是greg young通过实践提出的,见INFOQ他的访谈录,谈他的一个实践CQRS心得。

我简短翻译一下他的访谈录(顺便发牢骚:infoQ中国版编辑不知在做什么,这篇重要文章没有翻译?):
greg young当前工作在温哥华的IMIS, British Columbia. 我们开发一个算法交易系统,目前运行在全世界各大交易所,我当前是CTO,大部分时间是关注整体架构。

我们的领域模型是内存模型(in-memory model),代表市场中股票当前状态,我们所有业务策略都运行在这个领域模型中,我们选择领域驱动设计DDD其实相当不寻常的,对于那些不熟悉的人来说,性能是第一,当你每秒处理20,000消息,...你通常关注可伸缩性scalability, 低延迟low latency, 但是我们觉得代码的可理解性很重要,我们选择OO编程可以让我们更加适应变化。

我去年QCon演讲过,我们的实践和经典理论的DDD区别是:我们实体对象中封装的是可改变的状态,例如:volume-traded 命令是删除我们模型中的volume. 这其实产生一个事件,导致我们领域中的状态发生变化(banq注:典型的状态模式)

这样非常有用,我们就不必在领域之外再进行状态持久保存,我们只有状态的切换,我们能获取这些状态切换事件,然后传到其他场景bounded contexts(DDD定义的绑定场景), 而在其他场景当前状态或当前状态的镜像能以不同方式建立和显示,这种情况很普遍,事务系统是以一种方式看数据,而报表系统则以另外不同方式看系统(命令查询分离 CQRS)

相比围绕数据库的正常传统方式,他们就很象以一种非正常化的方式处理数据,通过将切换事件保存到事件流中,你就可以在这第一个模型中做你任何传统正常化方式可以做的任何事情,在事件流的另外一边(监听者)有了第二个模型,它是以非正常化呈现,最好的方式是在他们之间建立一个桥,这比通常同步性质的直接调用要好多,而且更加可伸缩性。

下面是greg young关于DDD实践的一些体会,太长,啃原文吧。

[该贴被banq于2009-12-29 14:42修改过]

2009年12月29日 09:23 "banq"的内容
hibernate JPA等ORM框架由于将关系数据表和实体对象绑定,如果我们直接将这个实体对象作为领域模型的实体(如果不是,系统中有两种实体对象,增加复杂性),那么无疑无法进行查询和命令(比如模型的修改)的分离,也就是是反CQRS模式。CQRS和关系数据库的读写分离策略也是相似的,查询是读,而状态修改一般是写,CQRS允许我们为查询优化数据表结构,因为对象领域模型的存储,数据表结构是无所谓的,但是如果你使用Hibernate/JPA等ORM框架,就很难实现为查询优化数据表结构,因为ORM框架绑定领域模型和数据表。这里是一个DDD的Domain Driven Design PPT,也谈到了CQRS,也称CQS架构,很不错。下面这张图也是CQS架构图:[该贴被banq于2009-12-29 11:35修改过]


这点非常认同。最近在公司的项目中采用了DDD的方式+Hibernate,就遇到了很多问题,hibernate会拦截我们领域模型的方法,结果导致,我们模型中本来已经设置好的数据,被hibernate冲了,并且就如banq老师所说Hibernate还是有数据库的影响,是与数据库绑定的,因此我觉得DDD+EDA架构,底层的用key-value会是一种比较好的可伸缩性的架构。

贴一篇linkedin利用voldemort这个key-value存储系统来搭建可伸缩性架构的文章。

Jay Kreps on Project Voldemort Scaling Simple Storage At LinkedIn

贫血模型和充血模型的争论由来已久,banq老师能不能做一个系统的分析,提供一个阶段性的结论给大家参考。

至于key-value,我觉得在功能上仅仅是sql的一个超小的子集,其应用只能是在当前硬件或其它集群技术无法满足性能上的要求时才考虑使用妥协方案。关系型数据库经历了这么多年的考验,“瓶颈”一说只是在新兴的大型计算、超大容量存储的环境是下才有感受,而那最初也不是关系型数据库要服务的对象。只要这些需求不断的扩大,关系型数据库一定会提出相应解决方案,之所以目前没有这样的产品只是因为肯投票(钞票)的用户目前还不够多。不赞成“数据库已死”的说法,太冒险而且对我们来说没什么好处(数据库死了,我们再拿什么来混饭吃,这是阶级立场)。
[该贴被atester于2009-12-30 11:24修改过]

被Hitenate冲了是什么意思?merge以后不需持久化的普通字段值丢了?
[该贴被admin于2009-12-30 11:52修改过]

按照我的理解,命令任务,让领域对象只负责业务行为交互。至于查询,通过领域对象的事件操作序列,异步去调整所需要查询的数据信息,不知道这样理解对不对。

按照我的理解,命令任务,让领域对象只负责业务行为交互。至于查询,通过领域对象的事件操作序列,异步去调整所需要查询的数据信息,不知道这样理解对不对。