当我写下这个标题的时候,我就有些后悔了,题目有点大,不太好控制。但我仍是打算尝试一下,经过这篇内容来讲清楚CQRS模式,以及和这个模式关联的其它东西。但愿我能说得清楚,你能看得明白,若是以为不错,右下角点个推荐!html
先从CQRS提及,CQRS的全称是Command Query Responsibility Segregation,翻译成中文叫做命令查询职责分离。从字面上就能看出,这个模式要求开发者按照方法的职责是命令仍是查询进行分离,什么是命令?什么是查询?咱们来继续往下看。数据库
什么是命令?什么是查询?服务器
对象的状态是什么意思呢?网络
对象的状态,咱们能够理解成它的属性,例如咱们定义一个Person类,定义以下:并发
public class Person { public string Id { get; set; } public string Name { get; set; } public int Age { get; set; } public void Say(string word) { Console.WriteLine($"{Name} Say: {word}"); } }
在Person类中:框架
再回到本小节讨论的内容,是否是就很好理解了呢?当我定义一个方法,要改变Person实例的Name或Age的时候,这个方法就属于Command;若是定一个方法,只查询Person实例信息的时候,这个方法就属于Query。当咱们按照职责将Command和Query进行分离的时候,你就在使用CQRS模式了。分布式
其实这就是CQRS的所有。高并发
有朋友可能要说了,若是这就是CQRS的所有,也太过于简单了吧?是的,大道至简!性能
当咱们按照CQRS进行分离之后,你是否是已经看出来,这玩意儿太适合作读写分离了?当咱们的数据库是主从模式的时候,主库负责写入、从库负责读取,彻底匹配Command和Query,简直完美。那么咱们接下来就说一下读写分离。this
如今主流的数据库都支持主从模式,主从模式的好处是方便我作故障迁移,当主库宕机的时候,能够快速的启用从库,从而减少系统不可用时间。
当咱们在使用数据库主从模式的时候,若是应用程序不作读写分离,你会发现从库基本上没用,主库天天忙的要死,既要负责写入,又要负责查询,碰见访问量大的时候CPU飙升是常有的事。然而从库就太闲了,除了接收主库的变动记录作数据同步,再没有别的事情可作,无论主库压力多大,从库的CPU一直跟心电图似的0-1-0-1...当咱们读写分离之后,主库负责写入,从库负责读取,代码要怎么改呢?咱们只须要定义两个Repository就能够了:
public interface IWritablePersonRepository { //写入数据的方法 } public interface IReadonlyPersonRepository { //读取数据的方法 }
在IWritablePersonRepository中使用主库的链接,IReadonlyPersonRepository中使用从库的链接。而后,在Command里面使用IWritablePersonRepository, 在Query里面使用IReadonlyPersonRepository,这样就在应用层实现了读写分离。
说到CQRS,不可避免的要说到这两个数据操做模型。为何要说数据操做模型呢?由于数据操做严重影响性能,而咱们分离的一个重要目的就是要提升性能。
CRUD(Create、Read、Update、Delete)是面向数据的,它将对数据的操做分为建立、更新、删除和读取四类,这四个操做能够对应咱们SQL语句中的insert、select、update、delete,很是直观明了,它的存在就是操做数据的。
由于存在即合理,咱们不能片面的说CRUD是好或者坏,这里只简单说一下它存在的问题:
好了,更多的问题再也不列举,单是“并发冲突”这一个问题,在高并发的环境下就不适用。既然CRUD不适用,咱们在构建高性能应用的时候,就只能寄但愿于ES了。
Event Souring,翻译过来叫事件溯源。什么意思呢?它把对象的建立、修改、删除等一系列的操做都看成事件(注意:事件和命令还有区别,后面会讲到),持久化的时候只存储事件,存储事件的介质叫作EventStore,当要获取一个对象的最新状态时,经过EventStore检索该对象的全部Event并从新加载来获取对象的最新状态。EventStore能够是数据库、磁盘文件、MongoDB等,因为Event的存储都是新增的,因此不存在并发冲突的问题。
在CQRS+ES的方案中,咱们要面对这两个概念,命令和事件。
咱们举一个例子,好比说你要更新本身的我的资料,例如将Age由35修改成18,那么对应的命令为:
public class PersonUpdateCommand { public string Id { get; set; } public int Age{ get; set; } public PersonUpdateCommand(string id, int age){ this.Id = id; this.Age = age; } }
PersonUpdateCommand是一个命令,它描述了用户更新我的资料的意图。当程序接收到这个命令之后,就须要对数据更改,从而引起数据状态变化,产生Event:
public class PersonAgeChangeEvent { public string Id { get; private set; } public int Age{ get; private set; } public PersonAgeChangeEvent(string id, int age){ this.Id = id; this.Age = age; } } public class PersonUpdateCommandHandler { private PersonUpdateCommand Command; public PersonUpdateCommandHandler(PersonUpdateCommand command) { this.Command = command; } public void Handle() { var person = GetPersonById(Command.Id); if(person.Age != Command.Age) { //生成并发送事件 var @event = new PersonAgeChangeEvent(Command.Id, Command.Age); EventBus.Send(@event); } } }
常见的数据一致性模型有两种:强一致性和最终一致性。
说到一致性的问题,咱们就不得不说一下CAP定理。
1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。
它们的第一个字母分别是 C、A、P,这三个指标不可能同时作到。这个结论就叫作 CAP 定理。
对于分布式系统来讲,受CAP定理的约束,最终一致性就成了惟一的选择。实现最终一致性要考虑如下问题:
经过上面的介绍,咱们已经知道在一个系统中全部的改变都是基于操做和由操做产生的事件所引起的。消息能够是一个Command,也能够是一个Event。当咱们基于消息来实现CQRS中的命令和事件发布的时候,咱们的系统将会更加的灵活可扩展。
若是你的系统基于消息,那么我猜你离不开消息总线,我在《手撸一套纯粹的CQRS实现》中写了一个基于内存的CommandBus的实现,感兴趣的朋友能够去看一下,CommandBus的代码定义以下:
public class CommandBus : ICommandBus { private readonly ICommandHandlerFactory handlerFactory; public CommandBus(ICommandHandlerFactory handlerFactory) { this.handlerFactory = handlerFactory; } public void Send<T>(T command) where T : ICommand { var handler = handlerFactory.GetHandler<T>(); if (handler == null) { throw new Exception("未找到对应的处理程序"); } handler.Execute(command); } }
基于内存的消息总线只能用于开发环境,在生产环境下不可以知足咱们分布式部署的须要,这个时候就须要采用基于消息队列的方式来实现了。消息队列有不少,例如Redis的订阅发布、RabbitMQ等,消息总线的实现也有不少优秀的开源框架,例如Rebus、Masstransit等,选一个你熟悉的框架便可。
数据审计是CQRS带给咱们的另外一个便利。因为咱们存储了全部事件,当咱们要获取对象变动记录的时候,只须要将EventStore中的记录查询出来,即可以看到整个的生命周期。这种操做,简直比打开了你青春期的日记本还要清晰明了。
固然,若是你要想知道对象的操做审计日志怎么办?一样的道理,咱们记录下全部的Command就能够了。那全部查询日志呢?哈哈,不要调皮了。记录的东西越多,你的存储就越大,若是你的存储空间容许的话,固然是越详细越好的,主要仍是看业务需求。
若是咱们记录了全部Command,咱们还能够有针对性的进行分析,哪些命令使用量大、哪些命令执行时间长。。这些数据将对咱们的扩容提供数据支撑。
在分布式系统中,Command和Query的使用比例是不同的,Command和Command之间、Query和Query之间的权重也存在差别,若是单纯的将这些服务平均的部署在每个节点上,那纯粹就是瞎搞。一个比较靠谱的实践是将不一样权重的Command和Query进行分组,而后进行有针对性的部署。
CQRS很简单,如何用好CQRS才是关键。CQRS更像是一种思想,它为咱们提供了系统分离的基本思路,结合ES、Messaging等模式,为构建分布式高可用可扩展的系统提供了良好的理论依据。
园子里有不少钻研CQRS+ES的前辈,本文借鉴了他们的文章和思想,感谢他们的分享!
文章中有任何不许确或错误的地方,请不吝赐教!欢迎讨论!