云计算设计模式(三)CQRS模式
模式描述
通过接口隔离,将数据的读取操作和更新操作隔离开。这可以最大化系统的性能,伸缩性和安全性。随着系统的演化,CQRS可以提供更高的灵活性,并且防止更新操作导致领域级的合并冲突。
上下文和问题
在传统数据管理系统中,命令(更新数据)和查询(请求数据),使用同一个数据容器,对同样的实体集进行操作。这些实体都是关系型数据库一个或者多个表的数据行的子集,比如SQL Server。
通常在这些系统中,create,read,update和delete(CRUD)都被应用到相同的数据实体。例如,一个数据访问对象(DTO),描述了一个通过数据访问层从数据存储中抽取的客户,并且显示在屏幕上。用户更新了DTO的某些字段(也许是通过数据绑定),然后DTO将数据变更通过DAL(数据访问层)写回到数据存储中。同一个DTO被用来同时执行读写操作。下图列举了一个传统的CRUD架构。
在有限的业务逻辑应用到数据操作上时,传统的CRUD设计工作的很好。而且由开发工具提供的脚手架程序可以快速的创建数据访问代码,并根据需要定制。
然而,传统的CRUD架构也有不少缺陷:
1)读写使用同一个模型意味着,数据的读写展现之间,经常有不匹配的情况。例如,一个新增加的行或者属性必须立即正确的更新,即使它在某个操作中不是必须的。
2)当数据存储中的数据被锁定时,如果多个参与者并发操作相同的数据集时,可能有数据竞争的风险。或者使用乐观锁进行并发的更新时,可能发生更新冲突。这些增加了系统提高复杂性和吞吐量的风险。另外,传统的方式可能对数据存储和数据访问层的性能,以及需要从数据存储中查询数据的复杂性,造成负面的影响。
3)它使安全和权限管理更复杂,因为每个实体同时从属于读写操作,这可能使数据暴露在错误的上下文中。
解决方案
命令职责分离模式通过接口分离,将读取数据的操作(Queries)和更新数据的操作(Commands)隔离开来;这意味着Query和Command使用的数据模型是不同的,这些模型可以被隔离开来,如下图所示,虽然这不是一个完全的需求。
与CRUD系统中的单一模型相比,在使用CQRS模型的系统中,使用分开的读写模型,从而简化了设计和实现。然而,不像CRUD,CQRS的代码不能使用脚手架工具自动生成。
读取数据的查询模型,和更新数据的命令模型,可以访问同一个物理存储,这也许或许通过使用SQL视图,或者通过生成投影来访问。然而,更通用的使用方式是,将数据分隔到不同的物理存储中,以最大化系统的性能,伸缩性和安全性。如下图所示:
读存储是写存储的一个只读副本,或者,读写存储可以有完全不同的结构。使用读存储的多个不用的副本,可以极大的提升查询性能和应用UI的响应性。特别是在分布式的场景下,只读副本可以定位到应用实例的附近。一些数据库系统(SQLServer)提供了额外的特性,比如故障转移副本来最大化系统的可用性。
读写存储的分离,也允许每个存储根据负载分别弹性伸缩。例如读存储通常会比写存储有更高的负载。
当读取/查询模型包含非标准化数据(参考物化视图模式)时,读取每个视图的数据时,性能是最优的。
问题和注意事项
实现这个模式你需要考虑以下几点:
1)将读写数据分别保存到不同的物理存储中,这可以提高系统的性能和安全性;但是,这将增加系统实现弹性(容错性)和最终一致性的复杂度。读模型必须被更新以反应写存储模型的变更。当用户基于过时的读数据发出请求时,很难检测到这种情况,这意味着操作无法完成。
关于数据最终一致性的描述,请参考Data Consistency Primer.
2)考虑将CQRS应用到你系统中它最有价值的那些部分。
3)部署最终一致性的一个典型的方法是,Event Sourcing结合CQRS使用,这样写模型是由一系列Append-only的执行命令组成事件驱动。这些事件被用于更新读模型的物化视图。更多的信息请参考Event Sourcing and CQRS
何时使用这个模式
1)多个领域模型的多个操作在同一份数据上并发的执行。CQRS允许你定义足够粒度的命令,以最小化领域级别的合并冲突(任何可能出现的冲突都可以通过命令来合并),及时更新的看起来似乎是相同类型的数据。
2)基于任务的用户界面,用户被引导使用包含一系列复杂步骤的流程,或者使用复杂的领域模型来完成这些任务。这对于非常熟悉领域驱动设计的团队来说非常有用。写模型包含一个完整的命令处理栈,包括业务逻辑,输入校验,业务校验,使得写模型集合中的所有事情总是保持一致性(每个集群相关的对象,都被视为数据更新的一个单元)。读模型中没有业务逻辑或者校验栈,仅仅是返回视图模型用到的一个数据访问队形(DTO)。读模型和写模型保持最终一致性。
3)数据的读取性能必须和数据的更新性能分别调优的场景。特别是读写比很高,需要垂直扩容的时候。例如,在很多系统中,读操作的次数,是写操作次数的很多倍。为了满足性能需要,可以考虑扩容度模型,但仅仅在一个或者很少几个写实例上执行写操作。很少的写实例也可以帮助降低合并冲突的概率。
4)适合于,一个开发者团队聚焦于基于写模型的复杂领域模型开发任务,而另一个团队负责度模型和用户界面。
5)适合于,系统随着时间的推移,可能包含多个版本的模型;或者业务规则会定期改变。
6)和其他系统集成,特别是与Event Sourcing集合使用。这样一个子系统的临时失败,不会影响其他系统的可用性。
在下面这些场景下,不建议使用CQRS:
1)模型或者业务规则非常简单
2)简单的CRUD风格的用户界面和相关的数据访问操作已经足够
3)要实现整个系统,在某些场景下CQRS可能是有用的,但与它带来的预期的复杂性相比,这不是必须的。