當咱們在討論CQRS時,咱們在討論些神馬?

當我寫下這個標題的時候,我就有些後悔了,題目有點大,不太好控制。但我仍是打算嘗試一下,經過這篇內容來講清楚CQRS模式,以及和這個模式關聯的其它東西。但願我能說得清楚,你能看得明白,若是以爲不錯,右下角點個推薦!html

先從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,簡直完美。那麼咱們接下來就說一下讀寫分離。this

如今主流的數據庫都支持主從模式,主從模式的好處是方便我作故障遷移,當主庫宕機的時候,能夠快速的啓用從庫,從而減少系統不可用時間。

當咱們在使用數據庫主從模式的時候,若是應用程序不作讀寫分離,你會發現從庫基本上沒用,主庫天天忙的要死,既要負責寫入,又要負責查詢,碰見訪問量大的時候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.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 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的前輩,本文借鑑了他們的文章和思想,感謝他們的分享!

文章中有任何不許確或錯誤的地方,請不吝賜教!歡迎討論!

參考文檔

相關文章
相關標籤/搜索