
Query Command什么是命令什么是查询命令(Command):不返回任何结果(void)但会改变对象的状态。查询(Query):返回结果但是不会改变对象的状态对系统没有副作用。对象的状态是什么意思呢对象的状态我们可以理解成它的属性例如我们定义一个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类中Name、Age属性状态Say(string): 方法行为再回到本小节讨论的内容是不是就很好理解了呢当我定义一个方法要改变Person实例的Name或Age的时候这个方法就属于Command如果定一个方法只查询Person实例信息的时候这个方法就属于Query。当我们按照职责将Command和Query进行分离的时候你就在使用CQRS模式了。其实这就是CQRS的全部。有朋友可能要说了如果这就是CQRS的全部也太过于简单了吧是的大道至简读写分离当我们按照CQRS进行分离以后你是不是已经看出来这玩意儿太适合做读写分离了当我们的数据库是主从模式的时候主库负责写入、从库负责读取完全匹配Command和Query简直完美。那么我们接下来就说一下读写分离。现在主流的数据库都支持主从模式主从模式的好处是方便我做故障迁移当主库宕机的时候可以快速的启用从库从而减小系统不可用时间。当我们在使用数据库主从模式的时候如果应用程序不做读写分离你会发现从库基本上没用主库每天忙的要死既要负责写入又要负责查询遇见访问量大的时候CPU飙升是常有的事。然而从库就太闲了除了接收主库的变更记录做数据同步再没有别的事情可做不管主库压力多大从库的CPU一直跟心电图似的0-1-0-1...当我们读写分离以后主库负责写入从库负责读取代码要怎么改呢我们只需要定义两个Repository就可以了public interface IWritablePersonRepository {//写入数据的方法}public interface IReadonlyPersonRepository {//读取数据的方法}在IWritablePersonRepository中使用主库的连接IReadonlyPersonRepository中使用从库的连接。然后在Command里面使用IWritablePersonRepository 在Query里面使用IReadonlyPersonRepository这样就在应用层实现了读写分离。CRUD和EventSourcing说到CQRS不可避免的要说到这两个数据操作模型。为什么要说数据操作模型呢因为数据操作严重影响性能而我们分离的一个重要目的就是要提高性能。CRUDCRUDCreate、Read、Update、Delete是面向数据的它将对数据的操作分为创建、更新、删除和读取四类这四个操作可以对应我们SQL语句中的insert、select、update、delete非常直观明了它的存在就是操作数据的。因为存在即合理我们不能片面的说CRUD是好或者坏这里只简单说一下它存在的问题并发冲突这是个大问题当A和B同时更新一行记录的时候你的事务必然报错。丢失数据操作的上下文这个问题也不小对于开发者来说我们通常要知道数据是谁在什么时候做了什么更新但是CURD只存储了最终的状态对数据操作的上下文一无所知。好了更多的问题不再列举单是“并发冲突”这一个问题在高并发的环境下就不适用。既然CRUD不适用我们在构建高性能应用的时候就只能寄希望于ES了。Event SouringEvent Souring翻译过来叫事件溯源。什么意思呢它把对象的创建、修改、删除等一系列的操作都当作事件注意事件和命令还有区别后面会讲到持久化的时候只存储事件存储事件的介质叫做EventStore当要获取一个对象的最新状态时通过EventStore检索该对象的所有Event并重新加载来获取对象的最新状态。EventStore可以是数据库、磁盘文件、MongoDB等由于Event的存储都是新增的所以不存在并发冲突的问题。Command和Event在CQRSES的方案中我们要面对这两个概念命令和事件。Command描述了用户的意图。Event描述了对象状态的改变。我们举一个例子比如说你要更新自己的个人资料例如将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是一个命令它描述了用户更新个人资料的意图。当程序接收到这个命令以后就需要对数据更改从而引发数据状态变化产生Eventpublic 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定理。CAP定理1998年加州大学的计算机科学家 Eric Brewer 提出分布式系统有三个指标。Consistency一致性Availability可用性Partition tolerance分区容错它们的第一个字母分别是 C、A、P这三个指标不可能同时做到。这个结论就叫做 CAP 定理。对于分布式系统来说受CAP定理的约束最终一致性就成了唯一的选择。实现最终一致性要考虑以下问题重试策略在分布式系统中我们无法保证每一次操作都能被成功的执行例如网络中断、服务器宕机等临时性的错误都会导致操作执行失败那么我们就要等待故障恢复后进行重试。重试的操作对于系统来说可能会造成一些副作用例如你正在支付的时候网络中断了这个时候你不知道是否支付成功联网以后再次重试可能就会造成重复扣款。如果要避免重试造成的系统危害就要将操作设计为幂等操作。幂等性简单的说就是一个操作执行一次和执行多次产生的结果是一样的不会产生副作用。撤销策略与重试策略相对应的如果一个操作最终确定执行失败那么我们需要撤销这个操作将系统还原到执行该操作之前的状态。撤销操作有两种一种是直接将对象修改为执行前的状态这种情况将造成数据审计不一致的问题另一种是类似于财务上的红冲操作新增一个命令冲掉上一个操作从而保证数据的完整性并能够满足数据审计的要求。Messaging通过上面的介绍我们已经知道在一个系统中所有的改变都是基于操作和由操作产生的事件所引发的。消息可以是一个Command也可以是一个Event。当我们基于消息来实现CQRS中的命令和事件发布的时候我们的系统将会更加的灵活可扩展。如果你的系统基于消息那么我猜你离不开消息总线我在《手撸一套纯粹的CQRS实现》中写了一个基于内存的CommandBus的实现感兴趣的朋友可以去看一下CommandBus的代码定义如下public class CommandBus : ICommandBus{private readonly ICommandHandlerFactory handlerFactory;public CommandBus(ICommandHandlerFactory handlerFactory){this.handlerFactory handlerFactory;}public void SendT(T command) where T : ICommand{var handler handlerFactory.GetHandlerT();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等模式为构建分布式高可用可扩展的系统提供了良好的理论依据。园子里有很多钻研CQRSES的前辈本文借鉴了他们的文章和思想感谢他们的分享文章中有任何不准确或错误的地方请不吝赐教欢迎讨论参考文档