第三十五期:当我们在讨论CQRS时,我们在讨论些神马?
thz?6月18日
當(dāng)我寫下這個(gè)標(biāo)題的時(shí)候,我就有些后悔了,題目有點(diǎn)大,不太好控制。但我還是打算嘗試一下,通過這篇內(nèi)容來說清楚CQRS模式,以及和這個(gè)模式關(guān)聯(lián)的其它東西。希望我能說得清楚,你能看得明白,如果覺得不錯(cuò),右下角點(diǎn)個(gè)推薦!
先從CQRS說起,CQRS的全稱是Command Query Responsibility Segregation,翻譯成中文叫作命令查詢職責(zé)分離。從字面上就能看出,這個(gè)模式要求開發(fā)者按照方法的職責(zé)是命令還是查詢進(jìn)行分離,什么是命令?什么是查詢?我們來繼續(xù)往下看。
Query & Command
什么是命令?什么是查詢?
-
命令(Command):不返回任何結(jié)果(void),但會(huì)改變對(duì)象的狀態(tài)。
-
查詢(Query):返回結(jié)果,但是不會(huì)改變對(duì)象的狀態(tài),對(duì)系統(tǒng)沒有副作用。
對(duì)象的狀態(tài)是什么意思呢?
對(duì)象的狀態(tài),我們可以理解成它的屬性,例如我們定義一個(gè)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:屬性(狀態(tài))
-
Say(string): 方法(行為)
再回到本小節(jié)討論的內(nèi)容,是不是就很好理解了呢?當(dāng)我定義一個(gè)方法,要改變Person實(shí)例的Name或Age的時(shí)候,這個(gè)方法就屬于Command;如果定一個(gè)方法,只查詢Person實(shí)例信息的時(shí)候,這個(gè)方法就屬于Query。當(dāng)我們按照職責(zé)將Command和Query進(jìn)行分離的時(shí)候,你就在使用CQRS模式了。
其實(shí)這就是CQRS的全部。
有朋友可能要說了,如果這就是CQRS的全部,也太過于簡單了吧?是的,大道至簡!
讀寫分離
當(dāng)我們按照CQRS進(jìn)行分離以后,你是不是已經(jīng)看出來,這玩意兒太適合做讀寫分離了?當(dāng)我們的數(shù)據(jù)庫是主從模式的時(shí)候,主庫負(fù)責(zé)寫入、從庫負(fù)責(zé)讀取,完全匹配Command和Query,簡直完美。那么我們接下來就說一下讀寫分離。
現(xiàn)在主流的數(shù)據(jù)庫都支持主從模式,主從模式的好處是方便我做故障遷移,當(dāng)主庫宕機(jī)的時(shí)候,可以快速的啟用從庫,從而減小系統(tǒng)不可用時(shí)間。
當(dāng)我們?cè)谑褂脭?shù)據(jù)庫主從模式的時(shí)候,如果應(yīng)用程序不做讀寫分離,你會(huì)發(fā)現(xiàn)從庫基本上沒用,主庫每天忙的要死,既要負(fù)責(zé)寫入,又要負(fù)責(zé)查詢,遇見訪問量大的時(shí)候CPU飆升是常有的事。然而從庫就太閑了,除了接收主庫的變更記錄做數(shù)據(jù)同步,再?zèng)]有別的事情可做,不管主庫壓力多大,從庫的CPU一直跟心電圖似的0-1-0-1...當(dāng)我們讀寫分離以后,主庫負(fù)責(zé)寫入,從庫負(fù)責(zé)讀取,代碼要怎么改呢?我們只需要定義兩個(gè)Repository就可以了:
?
public interface IWritablePersonRepository {//寫入數(shù)據(jù)的方法 }public interface IReadonlyPersonRepository {//讀取數(shù)據(jù)的方法 }在IWritablePersonRepository中使用主庫的連接,IReadonlyPersonRepository中使用從庫的連接。然后,在Command里面使用IWritablePersonRepository, 在Query里面使用IReadonlyPersonRepository,這樣就在應(yīng)用層實(shí)現(xiàn)了讀寫分離。
CRUD和EventSourcing
說到CQRS,不可避免的要說到這兩個(gè)數(shù)據(jù)操作模型。為什么要說數(shù)據(jù)操作模型呢?因?yàn)閿?shù)據(jù)操作嚴(yán)重影響性能,而我們分離的一個(gè)重要目的就是要提高性能。
CRUD
CRUD(Create、Read、Update、Delete)是面向數(shù)據(jù)的,它將對(duì)數(shù)據(jù)的操作分為創(chuàng)建、更新、刪除和讀取四類,這四個(gè)操作可以對(duì)應(yīng)我們SQL語句中的insert、select、update、delete,非常直觀明了,它的存在就是操作數(shù)據(jù)的。
因?yàn)榇嬖诩春侠?#xff0c;我們不能片面的說CRUD是好或者壞,這里只簡單說一下它存在的問題:
-
并發(fā)沖突:這是個(gè)大問題,當(dāng)A和B同時(shí)更新一行記錄的時(shí)候,你的事務(wù)必然報(bào)錯(cuò)。
-
丟失數(shù)據(jù)操作的上下文:這個(gè)問題也不小,對(duì)于開發(fā)者來說,我們通常要知道數(shù)據(jù)是誰在什么時(shí)候做了什么更新,但是CURD只存儲(chǔ)了最終的狀態(tài),對(duì)數(shù)據(jù)操作的上下文一無所知。
好了,更多的問題不再列舉,單是“并發(fā)沖突”這一個(gè)問題,在高并發(fā)的環(huán)境下就不適用。既然CRUD不適用,我們?cè)跇?gòu)建高性能應(yīng)用的時(shí)候,就只能寄希望于ES了。
Event Souring
Event Souring,翻譯過來叫事件溯源。什么意思呢?它把對(duì)象的創(chuàng)建、修改、刪除等一系列的操作都當(dāng)作事件(注意:事件和命令還有區(qū)別,后面會(huì)講到),持久化的時(shí)候只存儲(chǔ)事件,存儲(chǔ)事件的介質(zhì)叫做EventStore,當(dāng)要獲取一個(gè)對(duì)象的最新狀態(tài)時(shí),通過EventStore檢索該對(duì)象的所有Event并重新加載來獲取對(duì)象的最新狀態(tài)。EventStore可以是數(shù)據(jù)庫、磁盤文件、MongoDB等,由于Event的存儲(chǔ)都是新增的,所以不存在并發(fā)沖突的問題。
Command和Event
在CQRS+ES的方案中,我們要面對(duì)這兩個(gè)概念,命令和事件。
-
Command:描述了用戶的意圖。
-
Event:描述了對(duì)象狀態(tài)的改變。
我們舉一個(gè)例子,比如說你要更新自己的個(gè)人資料,例如將Age由35修改為18,那么對(duì)應(yīng)的命令為:
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是一個(gè)命令,它描述了用戶更新個(gè)人資料的意圖。當(dāng)程序接收到這個(gè)命令以后,就需要對(duì)數(shù)據(jù)更改,從而引發(fā)數(shù)據(jù)狀態(tài)變化,產(chǎn)生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) {//生成并發(fā)送事件var event = new PersonAgeChangeEvent(Command.Id, Command.Age);EventBus.Send(event);}} }數(shù)據(jù)一致性
常見的數(shù)據(jù)一致性模型有兩種:強(qiáng)一致性和最終一致性。
-
強(qiáng)一致性:在任何時(shí)刻所有的用戶或者進(jìn)程查詢到的都是最近一次成功更新的數(shù)據(jù)。
-
最終一致性:和強(qiáng)一致性相對(duì),在某一時(shí)刻用戶或者進(jìn)程查詢到的數(shù)據(jù)可能有不同,但是最終成功更新的數(shù)據(jù)都會(huì)被所有用戶或者進(jìn)程查詢到。
說到一致性的問題,我們就不得不說一下CAP定理。
CAP定理
1998年,加州大學(xué)的計(jì)算機(jī)科學(xué)家 Eric Brewer 提出,分布式系統(tǒng)有三個(gè)指標(biāo)。
-
Consistency:一致性
-
Availability:可用性
-
Partition tolerance:分區(qū)容錯(cuò)
它們的第一個(gè)字母分別是 C、A、P,這三個(gè)指標(biāo)不可能同時(shí)做到。這個(gè)結(jié)論就叫做 CAP 定理。
對(duì)于分布式系統(tǒng)來說,受CAP定理的約束,最終一致性就成了唯一的選擇。實(shí)現(xiàn)最終一致性要考慮以下問題:
-
重試策略:在分布式系統(tǒng)中,我們無法保證每一次操作都能被成功的執(zhí)行,例如網(wǎng)絡(luò)中斷、服務(wù)器宕機(jī)等臨時(shí)性的錯(cuò)誤,都會(huì)導(dǎo)致操作執(zhí)行失敗,那么我們就要等待故障恢復(fù)后進(jìn)行重試。重試的操作對(duì)于系統(tǒng)來說可能會(huì)造成一些副作用,例如你正在支付的時(shí)候網(wǎng)絡(luò)中斷了,這個(gè)時(shí)候你不知道是否支付成功,聯(lián)網(wǎng)以后再次重試,可能就會(huì)造成重復(fù)扣款。如果要避免重試造成的系統(tǒng)危害,就要將操作設(shè)計(jì)為冪等操作。
-
冪等性:簡單的說,就是一個(gè)操作執(zhí)行一次和執(zhí)行多次產(chǎn)生的結(jié)果是一樣的,不會(huì)產(chǎn)生副作用。
-
-
撤銷策略:與重試策略相對(duì)應(yīng)的,如果一個(gè)操作最終確定執(zhí)行失敗,那么我們需要撤銷這個(gè)操作,將系統(tǒng)還原到執(zhí)行該操作之前的狀態(tài)。撤銷操作有兩種,一種是直接將對(duì)象修改為執(zhí)行前的狀態(tài),這種情況將造成數(shù)據(jù)審計(jì)不一致的問題;另一種是類似于財(cái)務(wù)上的紅沖操作,新增一個(gè)命令,沖掉上一個(gè)操作,從而保證數(shù)據(jù)的完整性,并能夠滿足數(shù)據(jù)審計(jì)的要求。
Messaging
通過上面的介紹,我們已經(jīng)知道在一個(gè)系統(tǒng)中所有的改變都是基于操作和由操作產(chǎn)生的事件所引發(fā)的。消息可以是一個(gè)Command,也可以是一個(gè)Event。當(dāng)我們基于消息來實(shí)現(xiàn)CQRS中的命令和事件發(fā)布的時(shí)候,我們的系統(tǒng)將會(huì)更加的靈活可擴(kuò)展。
如果你的系統(tǒng)基于消息,那么我猜你離不開消息總線,我在《手?jǐn)]一套純粹的CQRS實(shí)現(xiàn)》中寫了一個(gè)基于內(nèi)存的CommandBus的實(shí)現(xiàn),感興趣的朋友可以去看一下,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("未找到對(duì)應(yīng)的處理程序");}handler.Execute(command);} }基于內(nèi)存的消息總線只能用于開發(fā)環(huán)境,在生產(chǎn)環(huán)境下不能夠滿足我們分布式部署的需要,這個(gè)時(shí)候就需要采用基于消息隊(duì)列的方式來實(shí)現(xiàn)了。消息隊(duì)列有很多,例如Redis的訂閱發(fā)布、RabbitMQ等,消息總線的實(shí)現(xiàn)也有很多優(yōu)秀的開源框架,例如Rebus、Masstransit等,選一個(gè)你熟悉的框架即可。
數(shù)據(jù)審計(jì)
數(shù)據(jù)審計(jì)是CQRS帶給我們的另一個(gè)便利。由于我們存儲(chǔ)了所有事件,當(dāng)我們要獲取對(duì)象變更記錄的時(shí)候,只需要將EventStore中的記錄查詢出來,便可以看到整個(gè)的生命周期。這種操作,簡直比打開了你青春期的日記本還要清晰明了。
當(dāng)然,如果你要想知道對(duì)象的操作審計(jì)日志怎么辦?同樣的道理,我們記錄下所有的Command就可以了。那所有查詢?nèi)罩灸?#xff1f;哈哈,不要調(diào)皮了。記錄的東西越多,你的存儲(chǔ)就越大,如果你的存儲(chǔ)空間允許的話,當(dāng)然是越詳細(xì)越好的,主要還是看業(yè)務(wù)需求。
如果我們記錄了所有Command,我們還可以有針對(duì)性的進(jìn)行分析,哪些命令使用量大、哪些命令執(zhí)行時(shí)間長。。這些數(shù)據(jù)將對(duì)我們的擴(kuò)容提供數(shù)據(jù)支撐。
分組部署
在分布式系統(tǒng)中,Command和Query的使用比例是不一樣的,Command和Command之間、Query和Query之間的權(quán)重也存在差異,如果單純的將這些服務(wù)平均的部署在每一個(gè)節(jié)點(diǎn)上,那純粹就是瞎搞。一個(gè)比較靠譜的實(shí)踐是將不同權(quán)重的Command和Query進(jìn)行分組,然后進(jìn)行有針對(duì)性的部署。
總結(jié)
CQRS很簡單,如何用好CQRS才是關(guān)鍵。CQRS更像是一種思想,它為我們提供了系統(tǒng)分離的基本思路,結(jié)合ES、Messaging等模式,為構(gòu)建分布式高可用可擴(kuò)展的系統(tǒng)提供了良好的理論依據(jù)。
園子里有很多鉆研CQRS+ES的前輩,本文借鑒了他們的文章和思想,感謝他們的分享!
文章中有任何不準(zhǔn)確或錯(cuò)誤的地方,請(qǐng)不吝賜教!歡迎討論!
參考文檔
-
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
閱讀目錄(置頂)(長期更新計(jì)算機(jī)領(lǐng)域知識(shí))https://blog.csdn.net/weixin_43392489/article/details/102380691
閱讀目錄(置頂)(長期更新計(jì)算機(jī)領(lǐng)域知識(shí))https://blog.csdn.net/weixin_43392489/article/details/102380882
閱讀目錄(置頂)(長期科技領(lǐng)域知識(shí))https://blog.csdn.net/weixin_43392489/article/details/102600114
總結(jié)
以上是生活随笔為你收集整理的第三十五期:当我们在讨论CQRS时,我们在讨论些神马?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在Linux下禁用键盘、鼠标、触摸板(笔
- 下一篇: 介绍:native2ascii命令用法详