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

浙公網(wǎng)安備 33010602011771號