先從CQRS說起,CQRS的全稱是Command Query Responsibility Segregation,翻譯成中文叫作命令查詢職責分離,從字面上就能看出,這個模式要求開發者按照方法的職責是命令還是查詢進行分離,什么是命令?什么是查詢?我們來繼續往下看,
Query & Command
什么是命令?什么是查詢?
- 命令(Command):不回傳任何結果(void),但會改變物件的狀態,
- 查詢(Query):回傳結果,但是不會改變物件的狀態,對系統沒有副作用,
物件的狀態是什么意思呢?
物件的狀態,我們可以理解成它的屬性,例如我們定義一個Person類,定義如下:
Copypublic 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就可以了:
Copypublic 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,那么對應的命令為:
Copypublic 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:
Copypublic 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的代碼定義如下:
Copypublic 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://www.uj5u.com/qita/107611.html
標籤:其他
上一篇:一文解讀ITIL (轉)
下一篇:Locust 教程
