关于CQRS实用性的疑问

最近有关注CQRS的思路,觉得很新颖,但是在实用性上我有些疑问。

1、首先CQRS在存储了事件流以后,还需要将事件结果同步到查询数据库里,而这个同步过程,我的理解应该是从事件仓储中取出事件流进行模型的重新推演,再把结果ORM或SQL CRUD到查询数据库中,但事实上这个事件流在用户发起请求并得到结果期间,已经进行过一次,并将结果告知了用户,既然同样是要把事件顺序运行的结果保存到查询数据库中,为什么不直接将第一次的领域结果状态保留下来呢?难道重新查找事件并演算领域结果状态,要比传达一个领域的完整状态开销更小吗?
如果是要为了解耦,我觉得也牵强,因为将事件最终结果推向一条总线,并不见得比一串事件推向一条总线更不合理。
这个问题的前题是我们确实需要一个查询数据库,并满足用户诸如:查找30天内回贴量超过10个的贴子。这样的需求,显然不能靠纯事件流仓储去得到结果。

2、接下来的疑问就是事件仓储的存在价值了。仍然以论坛贴子为例,在论坛这个需求里,恐怕用户更关心这个贴子现在的内容,回了多少贴,总评价如何,回复了什么内容。而不是关心这个贴子冗长的回复过程,评分过程。这说明事件的保存价值不大,恐怕无论是版主还是用户都没什么兴趣去重放贴子的发展经历吧... 这些事件的价值也不是完全没有,就是出BUG的时候能够重放一下,找出错误原因,修正代码,然后进行一次完整重放得到正常数据,但如一个异常牵扯的领域太多,如一个订单对商品对商家对支付对优惠折扣之类的影响,经历的事件太多,我觉得这样大面积地重放正常领域事件(如订单牵涉到的一件商品)去修复一个数天前的异常订单,风险就太大了,还牵涉到第三方合作的数据记录问题。而且要准确重放还需要停下服务,不要让新的事件在得到正确结果继续加入。复杂性很让人头痛啊。
纵观常规应用的需求,对结果状态的需求,远多于对事件规迹的需求啊。

希望能就这两个疑问进行交流
[该贴被ahyyxx222于2011-12-11 12:02修改过]
[该贴被ahyyxx222于2011-12-11 12:05修改过]
[该贴被ahyyxx222于2011-12-11 12:12修改过]

2011年12月11日 12:01 "@ahyyxx222"的内容
首先CQRS在存储了事件流以后,还需要将事件结果同步到查询数据库里,而这个同步过程,我的理解应该是从事件仓储中取出事件流进行模型的重新推演,再把结果ORM或SQL CRUD到查询数据库中 ...

我的理解不应该是这么复杂,CQRS是Command和查询分离,所谓Command命令是驱使领域对象状态改变的一些事件。领域对象同时也负责状态的持久化,通常这个持久化介质是和查询是同一个数据库。

事件回溯回放是主要针对领域对象状态出现事务性混乱才必须的,我的理解不是日常必备的,事件回放在程序调试或数据挖掘分析中扮演重要作用。

关于事件的日志保存其实和现在Web中apache日志一样,这些日志保存不但有助于业务事务分析,也有助于用户习惯数据分析,也有助于CDN优化等等。

既然这样,那整个架构的执行流程,又和传统的MVC相去不远了,对同一个数据库进行交替性的更新和查询,以持久化结束为命令执行终点。最多也只是进行异步持久化
纵使在代码上是分离的,也只是类似于将以前的一个dao分成了两个,一个负责持久化,一个负责查询,同时对负责持久化的那个进行更领域化业务化的操作封装。

如果事件回溯也不是必须的,那事件保存也就不是必须的,那如果大部分的功能都不需要它,那它就只是一个比较特别的日志系统了,并不能在普通业务中发挥出特别的作用贡献,比如提高性能,简化开发,实现复杂逻辑之类的。

结论难道是,CQRS只是为了促进更合理的分层和封装?
[该贴被ahyyxx222于2011-12-11 12:43修改过]

2011年12月11日 12:39 "@ahyyxx222"的内容
结论难道是,CQRS只是为了促进更合理的分层和封装 ...

CQRS是DDD领域驱动设计的一种落地架构,因为根据DDD设计出的领域模型这样的封装不利于各种琐碎条件的查询,采取读写分离的架构,实际就是将读取和写进行分离。

Event Sourcing是另外一种架构,可以说是在CQRS的写架构上进一步深入,这样就大大提供内存中领域模型的处理能力,不再拘束于锁或同步等机制,通过事件记录后再次回放来获得一种事务能力。

2011年12月11日 14:51 "@banq"的内容
不再拘束于锁或同步等机制,通过事件记录后再次回放来获得一种事务能力 ...

这个是不是可以这样理解,假设现在有一堆事件要处理,每若干个事件为一事务。
如果我没有定义事件类,而是用传统mvc,那事件无法保存下来,只能立即处理到底,直到让这次请求完全持久化好了以后,才算确保成功,于是得借助于锁来防止各事务和线程互相冲突。造成了用户的长时间等待。
如果我有定义事件,可以保存事件,那我就不必管锁,因为我还不马上持久化,我先在内存中推演一遍,能得到结果,就可以告诉用户了,然后让整个事务中的若干事件都排到队列里去,让队列依次重新处理持久化过程。通过这样的异步加队列摊平了系统负荷防止扎堆等待,又尽可能保证不会出现和用户想要的不一样的结果。

2011年12月11日 20:47 "@ahyyxx222"的内容
通过这样的异步加队列摊平了系统负荷防止扎堆等待,又尽可能保证不会出现和用户想要的不一样的结果。 ...

是这样,传统的MVC是基于多线程的一种同步架构,容易堵塞;而异步模型则无堵塞流畅且并发性能好,是事件模型的最佳实现,这也是一种发展趋势。

过去我们一直把线程看成事件的合理实现,或者两者概念混同在一起,这其实是有误区的,因为多线程容易发生共享内存状态的锁争夺,这就会堵塞,并发性能很差。因此,我们必须把事件和事件的实现两个概念区分开来。

现在事件的实现有两种方式:
1. Actor模型,比如Akka或Java的Actor模型:Kilim

2. Disruptor。

特别Kilim和Disruptor是在现有Java多线程条件下解决了共享内存锁的问题,因此无锁无同步的多线程也是事件的最佳实现。

注意这里的锁,不只是指内存共享锁或同步锁,直接JDBC操作数据库也会带来锁,发展途径是这样:

JDBC/ORM操作数据库(JDBC锁或JTA锁) ---- > 内存锁(同步synchronized) --- > Actor/Disruptor(无锁 无同步)

现在实际中很多架构离CQRS很远,比如这个帖子层次架构的困惑中架构是:

JSP(表单参数[参数形式:model.propertyName])--->ACTION(引用一个实体对象作为成员属性,struts2框架自动封装请求参数到实体对象)
--->调用service层的业务逻辑方法(层之间的参数传递依然是ACTION中封装的实体对象)---->service方法处理实体对象(实体对象可能嵌套)并调用多个DAO操作,且封装事务
--->DAO操作(从service层传过来的参数依然是ACTION中的实体对象,只是由service处理过了)

要重构到:
Jsp --> Action ---> Service接口 -- >调用领域层中业务逻辑方法 --->仓储。

然后再引入异步事件 逐步走上CQRS和Event Sourcing步骤。

一个CQRS + Event Sourcing的架构PPT: CQRS and Event Sourcing, An Alternative Architecture for DDD,PPT中推荐的CQRS + Event Sourcing如下图,注意Event Store不一定是关系数据库,我推荐使用Cassandra非常合适:Cassandra作为事件存储的最佳选择之一

相关帖:重新设计已有系统的架构,看下可否行得通

[该贴被banq于2011-12-13 11:29修改过]


2011-12-11 20:47 "@ahyyxx222"的内容
这个是不是可以这样理解,假设现在有一堆事件要处理,每若干个事件为一事务。如果我没有定义事件类,而是用传统mvc,那事件无法保存下来,只能立即处理到底,直到让这次请求完全持久化好了以后,才算确保成功,于是得借助于锁来防止各事务和线程互相冲突。 ...

我也比较你这样的说法,如果在我发送了事件到源,并且这个事件还在队列中(队列给我无限放大了)而在这个时候有另 一个用户去查看统计,那我只是查询数据库(因为队列还没有持久)那我查出来的数据还是旧的,这个会不会造成数据的不统一呢?

2012-10-11 11:32 "@alexyhe"的内容
那我只是查询数据库(因为队列还没有持久)那我查出来的数据还是旧的,这个会不会造成数据的不统一呢? ...

是有不一致性了,这可按照CAP定律,如果要求完全的一致性,可通过引入Rabbitmq 或Zeromq等非常快速的消息总线作为事件总线,直接将结果推向用户浏览器:RabbitMQ-Web-Stomp:使用#RabbitMQ 与WebSockets通过Javascript可以直接将消息或事件发送到浏览器上。

MVC是UI与业务逻辑分离的框架,CQRS如果用在这个上面也会一种“鸡肋”感,
关键是要用对地方,除了整个架构方面有分层,其实就领域层本身根据业务也有分层,比如,领域层一般还可以细分为:“作业层”、“业务层”、”决策层”,把CQRS用这个这个方面就不会有实用性的疑问了。