这两天重读了微软Patterns & Practices团队几年前写的CQRS Journey。这本书我几年前就看到过,只是当时我工作的重点不在应用架构上,当时没读下去。这两天重读,对CQRS和Event Sourcing (ES)模式有了新的认识。在云计算和微服务大行其道的当下,这两个模式在应用架构方面仍然是非常有参考价值的。这篇文章算是读书小结吧。
什么是CQRS模式?
CQRS是Command and Query Responsibility Segregation的缩写,中文直译过来,就是命令与查询责任分离的意思。这里涉及到两个定义,何为命令?何为查询?
- 命令会改变对象的状态,但不返回任何数据。
- 查询则相反,会返回数据,但并不改变对象的状态。
如果将查询和命令简化理解成对数据的读写操作,CQRS模式的含义就是,应用架构中负责模型读写的模块应当分离。这里的分离,不单是程序代码或逻辑上的分离,也包括数据模型,甚至是数据存储的分离。CQRS Journey的这张图描述了CQRS的一种典型应用。

虽然不是必须的,但CQRS模式通常与Domain Driven Design (DDD)同时使用。CQRS不是一种全局性的架构模式,它只适用于特定的bounded context。对于领域模型十分复杂的场景,CQRS模式可以增强架构的扩展性和灵活性,同时降低模块的复杂度。由于读和写的模型分离,可以分别针对读写操作优化,同时避免的数据锁定,对于性能提升也有帮助。这是CQRS模式带来的好处。
于此同时,CQRS模式也有其局限性。首先,它并不是一种易于实现的模式。因为读写责任的分离,它不如CRUD来的直观。而且读写数据同步和确保数据一致性会是一个问题。比较常见的作法,是通过事件(Event)来将写操作的结果同步到读操作的数据库中。为了确保数据的一致性,常常借用Event Sourcing模式来实现事件的存储和分发。同时由于通过事件实现的数据同步,其实是异步完成的,在分布式系统中,需要考虑数据的最终一致性(Eventual Consistency)。在CQRS模式中,通常写数据具有完全一致性(Full Consistency),而读数据则具有最终一致性。
这些局限决定了,CQRS模式并非适用于所有场景,或所有的bounded context。它通常只适用于复杂多变,涉及多方操作的场景。而对于业务简单,操作方单一,以及非核心的bounded context,使用CQRS模式可能会增加开销,但并不能带来明显的好处。
什么是Event Sourcing模式?
Event Sourcing (ES)模式是一个关于如何存储domain model状态的模式。这个模式不直接存储模型的状态,而是存储模型状态变化的历史。应用想要获取模型的当前状态时,需要重演整个历史来得到当前状态。一个常用的解释ES模式的场景,是银行账户。
账户余额的直观存储方式,是存储余额本身。当用户存入100元时,余额假定是100,其后用户取出10元,余额变为90,其后用户又存入50元,余额变为140。当用户查询余额时,系统直接获取当前余额。
同样的场景,使用ES模式时,系统不存储余额本身,而是存储用户的行为,即Event。当用户存入100元时,存储“存入100”这个Event,当用户取出10元时,存储“取出10”。当用户查询余额时,系统获得所有的Event,然后进行计算,得到余额。这实际上是金融机构存储交易信息的方式。Bitcoin也是使用同样的方式存储账户的send和receive操作,并且将所有的Event使用blockchain链接起来。
ES模式带来的好处显而易见,比如它能简化写操作,所有Event一旦发生,就变为immutable,写操作就变为简单的添加纪录,避免了复杂的锁定和冲突。同时ES模式保留了所有状态的历史,容易做audit,或纠错。而ES模式的一个问题是,随着历史数据的增加,查询操作的性能可能会降低。

虽然不是必须的,但通常CQRS模式和ES模式会被同时使用。因为通常CQRS模式使用Event来同步读写两端的数据,使用ES模式存储Event有助于这种数据同步,在数据出现不一致的情况时(根据CAP理论,这在分布式系统中是不可避免的),读操作端可以通过replay event的历史,来确保数据的最终一致性。