第三十五期:当我们在讨论CQRS时,我们在讨论些神马?

thz 6月18日

当我写下这个标题的时候,我就有些后悔了,题目有点大,不太好控制。但我还是打算尝试一下,通过这篇内容来说清楚CQRS模式,以及和这个模式关联的其它东西。希望我能说得清楚,你能看得明白,如果觉得不错,右下角点个推荐!

先从CQRS说起,CQRS的全称是Command Query Responsibility Segregation,翻译成中文叫作命令查询职责分离。从字面上就能看出,这个模式要求开发者按照方法的职责是命令还是查询进行分离,什么是命令?什么是查询?我们来继续往下看。

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,不可避免的要说到这两个数据操作模型。为什么要说数据操作模型呢?因为数据操作严重影响性能,而我们分离的一个重要目的就是要提高性能。

CRUD

CRUD(Create、Read、Update、Delete)是面向数据的,它将对数据的操作分为创建、更新、删除和读取四类,这四个操作可以对应我们SQL语句中的insert、select、update、delete,非常直观明了,它的存在就是操作数据的。

因为存在即合理,我们不能片面的说CRUD是好或者坏,这里只简单说一下它存在的问题:

  • 并发冲突:这是个大问题,当A和B同时更新一行记录的时候,你的事务必然报错。

  • 丢失数据操作的上下文:这个问题也不小,对于开发者来说,我们通常要知道数据是谁在什么时候做了什么更新,但是CURD只存储了最终的状态,对数据操作的上下文一无所知。

好了,更多的问题不再列举,单是“并发冲突”这一个问题,在高并发的环境下就不适用。既然CRUD不适用,我们在构建高性能应用的时候,就只能寄希望于ES了。

Event Souring

Event Souring,翻译过来叫事件溯源。什么意思呢?它把对象的创建、修改、删除等一系列的操作都当作事件(注意:事件和命令还有区别,后面会讲到),持久化的时候只存储事件,存储事件的介质叫做EventStore,当要获取一个对象的最新状态时,通过EventStore检索该对象的所有Event并重新加载来获取对象的最新状态。EventStore可以是数据库、磁盘文件、MongoDB等,由于Event的存储都是新增的,所以不存在并发冲突的问题。

Command和Event

在CQRS+ES的方案中,我们要面对这两个概念,命令和事件。

  • 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是一个命令,它描述了用户更新个人资料的意图。当程序接收到这个命令以后,就需要对数据更改,从而引发数据状态变化,产生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.Id) {//生成并发送事件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 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的前辈,本文借鉴了他们的文章和思想,感谢他们的分享!

文章中有任何不准确或错误的地方,请不吝赐教!欢迎讨论!

参考文档

  • https://www.cnblogs.com/yangecnu/p/Introduction-CQRS.html

  • https://www.cnblogs.com/netfocus/p/4150084.html

  • http://www.ruanyifeng.com/blog/2018/07/cap.html

  • https://docs.microsoft.com/en-us/previous-versions/msp-n-p/dn589800(v=pandp.10)

  • https://msdn.microsoft.com/magazine/mt238399

阅读目录(置顶)(长期更新计算机领域知识)https://blog.csdn.net/weixin_43392489/article/details/102380691

阅读目录(置顶)(长期更新计算机领域知识)https://blog.csdn.net/weixin_43392489/article/details/102380882

阅读目录(置顶)(长期科技领域知识)https://blog.csdn.net/weixin_43392489/article/details/102600114

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/424814.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【数据结构与算法】【算法思想】回溯算法

贪心算法 回溯算法 分治算法 动态规划 回溯算法思想应用广泛&#xff0c;除了用来指导深度优先搜索这种经典算法设计之外&#xff0c;还可以用在如正则表达式匹配&#xff0c;编译原理中的语法分析等。 除此之外&#xff0c;很多经典的数学问题都可以用回溯算法解决&#xff…

90 Subsets II

90 Subsets II leetcode第90题&#xff0c;用深度优先搜索的思路写出了三种不同的代码。一道题目&#xff0c;同样是深度优先搜索的问题&#xff0c;因为看问题角度不同&#xff0c;思路不同&#xff0c;代码也不一样 /*** [1,2,2,3] [] [1] [1,2] [1,2](重复) 情况&#xff1a…

2019 DDCTF 部分writeup

网上的wp已经很多了&#xff0c;但wp普遍很简略。我尽量写的详细一点。 一、WEB 滴~ 拿到题目后首先右键查看源代码&#xff0c;发现图片是以base64传送的 而且看url发现里面应该是包含了文件名&#xff0c;并且用了某个编码。测试过后是转16进制ascii码后两层bases64 &#xf…

第三十六期:如果把线程当作一个人来对待,所有问题都瞬间明白了

7月8日 以下文章来源于编程新说 &#xff0c;作者编程新说李新杰 多线程的问题都曾经困扰过每个开发人员&#xff0c;今天将从全新视角来解说&#xff0c;希望读者都能明白。 强烈建议去运行下文章中的示例代码&#xff0c;自己体会下。 问题究竟出在哪里&#xff1f; 一个…

[Leetcode][第322题][JAVA][零钱兑换][回溯][记忆化搜索][动态规划]

【问题描述】[中等] 【解答思路】 1. 递归&#xff08;超时&#xff09; class Solution {int res Integer.MAX_VALUE;public int coinChange(int[] coins, int amount) {if(coins.length 0){return -1;}findWay(coins,amount,0);// 如果没有任何一种硬币组合能组成总金额&…

用BST解决729. My Calendar I 731. My Calendar II 732. My Calendar III

My Calendar的book方法实现指定开始时间、结束时间&#xff0c;在重叠次数要求不同的情况下怎么实现。 729 My Calendar I 要求任意两个事件之间不能重叠。如果要插入的事件和已经插入的事件不重叠&#xff0c;则插入&#xff1b;否则不插入。 731 MyCalendar II 要求任意三个…

[转载]抓大放小,要事为先

最近看到了swing框架&#xff0c;细节颇多。细细看来效率很低&#xff0c;偶然找到了这篇文章&#xff0c;恩确实有道理&#xff1b;以下&#xff1b; 对待人生的任何事情都要&#xff1a;抓大放小&#xff0c;要事为先 对于一个以前从来没有接触过java的人&#xff0c;java无疑…

第三十七期:如果你这样回答“什么是线程安全”,面试官都会对你刮目相看

6月12日 以下文章来源于编程新说 &#xff0c;作者编程新说李新杰 有读者跟我说&#xff0c;喜欢看我的文章&#xff0c;说很容易读&#xff0c;我确实在易读性上花费的心思不亚于在内容上。因为我不喜欢一上来就堆很多东西&#xff0c;而且把简单的东西搞得复杂人人都会&…

第三十八期:如何在Windows 10上使用Windows Update目录驱动程序安装打印机

尽管Win10能够自动设置大多数打印机&#xff0c;但有时Windows Update可能会在安装设备驱动程序时遇到问题-尤其是在添加较旧的打印机时。 作者&#xff1a;佚名来源&#xff1a;Win10系统之家 图片来源&#xff1a;伏天氏(m.futianshuwu.com) 伏天书屋(futianshuwu.com)10月…

MySQL 表和列的注释

像代码一样&#xff0c;可以为表以及表中的列添加注释&#xff0c;方便其他人知晓其功能。对于一些字段&#xff0c;在经过一定时间后&#xff0c;创建者未必也能想起其具体的含意&#xff0c;所以注释显得尤为重要。 注释的添加 注释的添加是通过在定义表或列的时候在末尾加上…

376 Wiggle Subsequence 贪心解法以及证明

376. Wiggle Subsequence 题目理解 给定一个数组&#xff0c;相邻两个数计算差值。差值排成的序列是正负相间的&#xff0c;那这个数组就是一个wiggle 数组。例如数组[1,7,4,9,2,5]&#xff0c;差值序列是(6,-3,5,-7,3)。原数组用坐标轴表示如下。 思路是&#xff1a;在一段…

【数据结构与算法】【算法思想】动态规划

贪心算法 回溯算法 分治算法 动态规划 贪心&#xff1a;一条路走到黑&#xff0c;就一次机会&#xff0c;只能哪边看着顺眼走哪边 回溯&#xff1a;一条路走到黑&#xff0c;无数次重来的机会&#xff0c;还怕我走不出来 (Snapshot View) 动态规划&#xff1a;拥有上帝视角&am…

第六十七期:Python爬虫44万条数据揭秘:如何成为网易音乐评论区的网红段子手

获取数据,其实逻辑并不复杂&#xff1a;爬取歌单列表里的所有歌单url、进入每篇歌单爬取所有歌曲url&#xff0c;去重、进入每首歌曲首页爬取热评&#xff0c;汇总。 作者&#xff1a;嗨学python来源&#xff1a;今日头条 获取数据 其实逻辑并不复杂&#xff1a; 爬取歌单列…

第一阶段SCRUM冲刺 08

昨天的成就&#xff1a;实现任务查找模块。感觉到了硬件支持对软件编程的重要性。 遇到的难题&#xff1a;电脑出现卡顿&#xff0c;编程工具十分卡&#xff0c;重启电脑好几次解决这个问题。电脑也是需要休息的。 今天的任务&#xff1a;进行资料上传模块&#xff1b;发布任务…

array专题7

714 Best Time to Buy and Sell Stock with Transaction Fee 思路 首先是暴力枚举。考虑在第idx天能做的操作&#xff1a;买&#xff1f;卖&#xff1f;不操作&#xff1f; /*** 暴力枚举* * param prices* param fee* return*/public int maxProfitV99(int[] prices, int f…

第三十九期:收藏 | 第一次有人把“分布式事务”讲的这么简单明了

不知道你是否遇到过这样的情况&#xff0c;去小卖铺买东西&#xff0c;付了钱&#xff0c;但是店主因为处理了一些其他事&#xff0c;居然忘记你付了钱&#xff0c;又叫你重新付。 作者&#xff1a;咖啡拿铁来源 又或者在网上购物明明已经扣款&#xff0c;但是却告诉我没有发…

【小技巧】字符char与整型int的相互转换

char转int char与int的相互转化&#xff0c;联想ASCII码&#xff0c;字符‘0’对应的值为48&#xff0c;所以不能直接加减‘ ’ char ch9; int ch_intch-0;//此时ch_int9int转char int i9&#xff1b; char i_chi0;//此时i_ch9必须牢记的ASCII

array专题8

670 Maximum Swap 思路&#xff1a;先把整数分解成一个一个的数&#xff0c;从0-n放着从最低位到最高位的数字。例如2376变成数组[6,7,3,2]。假设要替换的是最高位n-1,从0到n-2中查找是否有比nums[n-1]大的元素&#xff1b;如果有则替换&#xff0c;否则继续考虑替换n-2位。比…

nginx 编译安装

安装 Nginx 所依赖的基本服务&#xff1a; yum -y install gcc gcc-c automake pcre pcre-devel zlip zlib-devel openssl openssl-devel 官网 http://nginx.org 下载安装包 编译nginx make (可参考http://nginx.org/en/docs/configure.html) ./configure --prefix/data1/se…

第四十期:九个对Web开发者最有用的Python包,掌握这些,工资至少能涨涨

Matplotlib&#xff0c;正如其名称所暗示的那样&#xff0c;是一个用来绘制数学函数和模型的库;扩展了Numpy的作用&#xff0c;Matplotlib可以只用几行代码来创建图&#xff0c;条形图&#xff0c;散点图等诸多视觉表现。 作者&#xff1a;Python之眼来源&#xff1a;今日头条…