欧美性猛交XXXX免费看蜜桃,成人网18免费韩国,亚洲国产成人精品区综合,欧美日韩一区二区三区高清不卡,亚洲综合一区二区精品久久

打開(kāi)APP
userphoto
未登錄

開(kāi)通VIP,暢享免費電子書(shū)等14項超值服

開(kāi)通VIP
CQRS架構如何實(shí)現高性能

CQRS架構簡(jiǎn)介

前不久,看到博客園一位園友寫(xiě)了一篇文章,其中的觀(guān)點(diǎn)是,要想高性能,需要盡量:避開(kāi)網(wǎng)絡(luò )開(kāi)銷(xiāo)(IO),避開(kāi)海量數據,避開(kāi)資源爭奪。對于這3點(diǎn),我覺(jué)得很有道理。所以也想談一下,CQRS架構下是如何實(shí)現高性能的。

關(guān)于CQRS(Command Query Responsibility Segration)架構,大家應該不會(huì )陌生了。簡(jiǎn)單的說(shuō),就是一個(gè)系統,從架構上把它拆分為兩部分:命令處理(寫(xiě)請求)+查詢(xún)處理(讀請求)。然后讀寫(xiě)兩邊可以用不同的架構實(shí)現,以實(shí)現CQ兩端(即Command Side,簡(jiǎn)稱(chēng)C端;Query Side,簡(jiǎn)稱(chēng)Q端)的分別優(yōu)化。CQRS作為一個(gè)讀寫(xiě)分離思想的架構,在數據存儲方面,沒(méi)有做過(guò)多的約束。所以,我覺(jué)得CQRS可以有不同層次的實(shí)現,比如:

  1. CQ兩端數據庫共享,CQ兩端只是在上層代碼上分離;這種做法,帶來(lái)的好處是可以讓我們的代碼讀寫(xiě)分離,更好維護,且沒(méi)有CQ兩端的數據一致性問(wèn)題,因為是共享一個(gè)數據庫的。我個(gè)人認為,這種架構很實(shí)用,既兼顧了數據的強一致性,又能讓代碼好維護。
  2. CQ兩端數據庫和上層代碼都分離,然后Q的數據由C端同步過(guò)來(lái),一般是通過(guò)Domain Event進(jìn)行同步。同步方式有兩種,同步或異步,如果需要CQ兩端的強一致性,則需要用同步;如果能接受CQ兩端數據的最終一致性,則可以使用異步。采用這種方式的架構,個(gè)人覺(jué)得,C端應該采用Event Sourcing(簡(jiǎn)稱(chēng)ES)模式才有意義,否則就是自己給自己找麻煩。因為這樣做你會(huì )發(fā)現會(huì )出現冗余數據,同樣的數據,在C端的db中有,而在Q端的db中也有。和上面第一種做法相比,我想不到什么好處。而采用ES,則所有C端的最新數據全部用Domain Event表達即可;而要查詢(xún)顯示用的數據,則從Q端的ReadDB(關(guān)系型數據庫)查詢(xún)即可。

我覺(jué)得要實(shí)現高性能,可以談的東西還有很多。下面我想重點(diǎn)說(shuō)說(shuō)我想到的一些設計思路:

避開(kāi)資源爭奪

秒殺活動(dòng)的例子分析

我覺(jué)得這是很重要的一點(diǎn)。什么是資源爭奪?我想就是多個(gè)線(xiàn)程同時(shí)修改同一個(gè)數據。就像阿里秒殺活動(dòng)一樣,秒殺開(kāi)搶時(shí),很多人同時(shí)搶一個(gè)商品,導致商品的庫存會(huì )被并發(fā)更新減庫存,這就是一個(gè)資源爭奪的例子。一般如果資源競爭不激烈,那無(wú)所謂,不會(huì )影響性能;但是如果像秒殺這種場(chǎng)景,那db就會(huì )抗不住了。在秒殺這種場(chǎng)景下,大量線(xiàn)程需要同時(shí)更新同一條記錄,進(jìn)而導致MySQL內部大量線(xiàn)程堆積,對服務(wù)性能、穩定性造成很大傷害。那怎么辦呢?我記得阿里的丁奇寫(xiě)過(guò)一個(gè)分享,思路就是當MySQL的服務(wù)端多個(gè)線(xiàn)程同時(shí)修改一條記錄時(shí),可以對這些修改請求進(jìn)行排隊,然后對于InnoDB引擎層,就是串行的。這樣排隊后,不管上層應用發(fā)過(guò)來(lái)多少并行的修改同一行的請求,對于MySQL Server端來(lái)說(shuō),內部總是會(huì )聰明的對同一行的修改請求都排隊處理;這樣就能確保不會(huì )有并發(fā)產(chǎn)生,從而不會(huì )導致線(xiàn)程浪費堆積,導致數據庫性能下降。這個(gè)方案可以見(jiàn)下圖所示:

如上圖所示,當很多請求都要修改A記錄時(shí),MySQL Server內部會(huì )對這些請求進(jìn)行排隊,然后一個(gè)個(gè)將對A的修改請求提交到InnoDB引擎層。這樣看似在排隊,實(shí)際上會(huì )確保MySQL Server不會(huì )死掉,可以保證對外提供穩定的TPS。

但是,對于商品秒殺這個(gè)場(chǎng)景,還有優(yōu)化的空間,就是Group Commit技術(shù)。Group Commit就是對多個(gè)請求合并為一次操作進(jìn)行處理。秒殺時(shí),大家都在購買(mǎi)這個(gè)商品,A買(mǎi)2件,B買(mǎi)3件,C買(mǎi)1件;其實(shí)我們可以把A,B,C的這三個(gè)請求合并為一次減庫存操作,就是一次性減6件。這樣,對于A(yíng),B,C的這三個(gè)請求,在InnoDB層我們只需要做一次減庫存操作即可。假設我們Group Commit的每一批的size是50,那就是可以將50個(gè)減操作合并為一次減操作,然后提交到InnoDB。這樣,將大大提高秒殺場(chǎng)景下,商品減庫存的TPS。但是這個(gè)Group Commit的每批大小不是越大越好,而是要根據并發(fā)量以及服務(wù)器的實(shí)際情況做測試來(lái)得到一個(gè)最優(yōu)的值。通過(guò)Group Commit技術(shù),根據丁奇的PPT,商品減庫存的TPS性能從原來(lái)的1.5W提高到了8.5W。

從上面這個(gè)例子,我們可以看到阿里是如何在實(shí)際場(chǎng)景中,通過(guò)優(yōu)化MySQL Server來(lái)實(shí)現高并發(fā)的商品減庫存的。但是,這個(gè)技術(shù)一般人還真的不會(huì )!因為沒(méi)多少人有能力去優(yōu)化MySQL的服務(wù)端,排隊也不行,更別說(shuō)Group Commit了。這個(gè)功能并不是MySQL Server自帶的,而是需要自己實(shí)現的。但是,這個(gè)思路我想我們都可以借鑒。

CQRS如何實(shí)現避免資源競爭

那么對于CQRS架構,如何按照這個(gè)思路來(lái)設計呢?我想重點(diǎn)說(shuō)一下我上面提到的第二種CQRS架構。對于C端,我們的目標是盡可能的在1s內處理更多的Command,也就是數據寫(xiě)請求。在經(jīng)典DDD的四層架構中,我們會(huì )有一個(gè)模式叫工作單元模式,即Unit of Work(簡(jiǎn)稱(chēng)UoW)模式。通過(guò)該模式,我們能在應用層,一次性以事務(wù)的方式將當前請求所涉及的多個(gè)對象的修改提交到DB。微軟的EF實(shí)體框架的DbContext就是一個(gè)UoW模式的實(shí)現。這種做法的好處是,一個(gè)請求對多個(gè)聚合根的修改,能做到強一致性,因為是事務(wù)的。但是這種做法,實(shí)際上,沒(méi)有很好的遵守避開(kāi)資源競爭的原則。試想,事務(wù)A要修改a1,a2,a3三個(gè)聚合根;事務(wù)B要修改a2,a3,a4;事務(wù)C要修改a3,a4,a5三個(gè)聚合根。那這樣,我們很容易理解,這三個(gè)事務(wù)只能串行執行,因為它們要修改相同的資源。比如事務(wù)A和事務(wù)B都要修改a2,a3這兩個(gè)聚合根,那同一時(shí)刻,只能由一個(gè)事務(wù)能被執行。同理,事務(wù)B和事務(wù)C也是一樣。如果A,B,C這種事務(wù)執行的并發(fā)很高,那數據庫就會(huì )出現嚴重的并發(fā)沖突,甚至死鎖。那要如何避免這種資源競爭呢?我覺(jué)得我們可以采取三個(gè)措施:

讓一個(gè)Command總是只修改一個(gè)聚合根

這個(gè)做法其實(shí)就是縮小事務(wù)的范圍,確保一個(gè)事務(wù)一次只涉及一條記錄的修改。也就是做到,只有單個(gè)聚合根的修改才是事務(wù)的,讓聚合根成為數據強一致性的最小單位。這樣我們就能最大化的實(shí)現并行修改。但是你會(huì )問(wèn),但是我一個(gè)請求就是會(huì )涉及多個(gè)聚合根的修改的,這種情況怎么辦呢?在CQRS架構中,有一個(gè)東西叫Saga。Saga是一種基于事件驅動(dòng)的思想來(lái)實(shí)現業(yè)務(wù)流程的技術(shù),通過(guò)Saga,我們可以用最終一致性的方式最終實(shí)現對多個(gè)聚合根的修改。對于一次涉及多個(gè)聚合根修改的業(yè)務(wù)場(chǎng)景,一般總是可以設計為一個(gè)業(yè)務(wù)流程,也就是可以定義出要先做什么后做什么。比如以銀行轉賬的場(chǎng)景為例子,如果是按照傳統事務(wù)的做法,那可能是先開(kāi)啟一個(gè)事務(wù),然后讓A賬號扣減余額,再讓B賬號加上余額,最后提交事務(wù);如果A賬號余額不足,則直接拋出異常,同理B賬號如果加上余額也遇到異常,那也拋出異常即可,事務(wù)會(huì )保證原子性以及自動(dòng)回滾。也就是說(shuō),數據一致性已經(jīng)由DB幫我們做掉了。

但是,如果是Saga的設計,那就不是這樣了。我們會(huì )把整個(gè)轉賬過(guò)程定義為一個(gè)業(yè)務(wù)流程。然后,流程中會(huì )包括多個(gè)參與該流程的聚合根以及一個(gè)用于協(xié)調聚合根交互的流程管理器(ProcessManager,無(wú)狀態(tài)),流程管理器負責響應流程中的每個(gè)聚合根產(chǎn)生的領(lǐng)域事件,然后根據事件發(fā)送相應的Command,從而繼續驅動(dòng)其他的聚合根進(jìn)行操作。

轉賬的例子,涉及到的聚合根有:兩個(gè)銀行賬號聚合根,一個(gè)交易(Transaction)聚合根,它用于負責存儲流程的當前狀態(tài),它還會(huì )維護流程狀態(tài)變更時(shí)的規則約束;然后當然還有一個(gè)流程管理器。轉賬開(kāi)始時(shí),我們會(huì )先創(chuàng )建一個(gè)Transaction聚合根,然后它產(chǎn)生一個(gè)TransactionStarted的事件,然后流程管理器響應事件,然后發(fā)送一個(gè)Command讓A賬號聚合根做減余額的操作;A賬號操作完成后,產(chǎn)生領(lǐng)域事件;然后流程管理器響應事件,然后發(fā)送一個(gè)Command通知Transaction聚合根確認A賬號的操作;確認完成后也會(huì )產(chǎn)生事件,然后流程管理器再響應,然后發(fā)送一個(gè)Command通知B賬號做加上余額的操作;后續的步驟就不詳細講了。大概意思我想已經(jīng)表達了??傊?,通過(guò)這樣的設計,我們可以通過(guò)事件驅動(dòng)的方式,來(lái)完成整個(gè)業(yè)務(wù)流程。如果流程中的任何一步出現了異常,那我們可以在流程中定義補償機制實(shí)現回退操作?;蛘卟换赝艘矝](méi)關(guān)系,因為T(mén)ransaction聚合根記錄了流程的當前狀態(tài),這樣我們可以很方便的后續排查有狀態(tài)沒(méi)有正常結束的轉賬交易。具體的設計和代碼,有興趣的可以去看一下ENode源代碼中的銀行轉賬的例子,里面有完整的實(shí)現。

對修改同一個(gè)聚合根的Command進(jìn)行排隊

和上面秒殺的設計一樣,我們可以對要同時(shí)修改同一個(gè)聚合根的Command進(jìn)行排隊。只不過(guò)這里的排隊不是在MySQL Server端,而是在我們自己程序里做這個(gè)排隊。如果我們是單臺服務(wù)器處理所有的Command,那排隊很容易做。就是只要在內存中,當要處理某個(gè)Command時(shí),判斷當前Command要修改的聚合根是否前面已經(jīng)有Command在處理,如果有,則排隊;如果沒(méi)有,則直接執行。然后當這個(gè)聚合根的前一個(gè)Command執行完后,我們就能處理該聚合根的下一個(gè)Command了;但是如果是集群的情況下呢,也就是你不止有一臺服務(wù)器在處理Command,而是有十臺,那要怎么辦呢?因為同一時(shí)刻,完全有可能有兩個(gè)不同的Command在修改同一個(gè)聚合根。這個(gè)問(wèn)題也簡(jiǎn)單,就是我們可以對要修改聚合根的Command根據聚合根的ID進(jìn)行路由,根據聚合根的ID的hashcode,然后和當前處理Command的服務(wù)器數目取模,就能確定當前Command要被路由到哪個(gè)服務(wù)器上處理了。這樣我們能確保在服務(wù)器數目不變的情況下,針對同一個(gè)聚合根實(shí)例修改的所有Command都是被路由到同一臺服務(wù)器處理。然后加上我們前面在單個(gè)服務(wù)器里面內部做的排隊設計,就能最終保證,對同一個(gè)聚合根的修改,同一時(shí)刻只有一個(gè)線(xiàn)程在進(jìn)行。

通過(guò)上面這兩個(gè)設計,我們可以確保C端所有的Command,都不會(huì )出現并發(fā)沖突。但是也要付出代價(jià),那就是要接受最終一致性。比如Saga的思想,就是在最終一致性的基礎上而實(shí)現的一種設計。然后,基于以上兩點(diǎn)的這種架構的設計,我覺(jué)得最關(guān)鍵的是要做到:1)分布式消息隊列的可靠,不能丟消息,否則Saga流程就斷了;2)消息隊列要高性能,支持高吞吐量;這樣才能在高并發(fā)時(shí),實(shí)現整個(gè)系統的整體的高性能。我開(kāi)發(fā)的EQueue就是為了這個(gè)目標而設計的一個(gè)分布式消息隊列,有興趣的朋友可以去了解下哦。

Command和Event的冪等處理

CQRS架構是基于消息驅動(dòng)的,所以我們要盡量避免消息的重復消費。否則,可能會(huì )導致某個(gè)消息被重復消費而導致最終數據無(wú)法一致。對于CQRS架構,我覺(jué)得主要考慮三個(gè)環(huán)節的消息冪等處理。

Command的冪等處理

這一點(diǎn),我想不難理解。比如轉賬的例子中,假如A賬號扣減余額的命令被重復執行了,那會(huì )導致A賬號扣了兩次錢(qián)。那最后就數據無(wú)法一致了。所以,我們要保證Command不能被重復執行。那怎么保證呢?想想我們平時(shí)一些判斷重復的操作怎么做的?一般有兩個(gè)做法:1)db對某一列建唯一索引,這樣可以嚴格保證某一列數據的值不會(huì )重復;2)通過(guò)程序保證,比如插入前先通過(guò)select查詢(xún)判斷是否存在,如果不存在,則insert,否則就認為重復;顯然通過(guò)第二種設計,在并發(fā)的情況下,是不能保證絕對的唯一性的。然后CQRS架構,我認為我們可以通過(guò)持久化Command的方式,然后把CommandId作為主鍵,確保Command不會(huì )重復。那我們是否要每次執行Command前線(xiàn)判斷該Command是否存在呢?不用。因為出現Command重復的概率很低,一般只有是在我們服務(wù)器機器數量變動(dòng)時(shí)才會(huì )出現。比如增加了一臺服務(wù)器后,會(huì )影響到Command的路由,從而最終會(huì )導致某個(gè)Command會(huì )被重復處理,關(guān)于這里的細節,我這里不想多展開(kāi)了,呵呵。有問(wèn)題到回復里討論吧。這個(gè)問(wèn)題,我們也可以最大程度上避免,比如我們可以在某一天系統最空的時(shí)候預先增加好服務(wù)器,這樣可以把出現重復消費消息的情況降至最低。自然也就最大化的避免了Command的重復執行。所以,基于這個(gè)原因,我們沒(méi)有必要在每次執行一個(gè)Command時(shí)先判斷該Command是否已執行。而是只要在Command執行完之后,直接持久化該Command即可,然后因為db中以CommandId為主鍵,所以如果出現重復,會(huì )主鍵重復的異常。我們只要捕獲該異常,然后就知道了該Command已經(jīng)存在,這就說(shuō)明該Command之前已經(jīng)被處理過(guò)了,那我們只要忽略該Command即可(當然實(shí)際上不能直接忽略,這里我由于篇幅問(wèn)題,我就不詳細展開(kāi)了,具體我們可以再討論)。然后,如果持久化沒(méi)有問(wèn)題,說(shuō)明該Command之前沒(méi)有被執行過(guò),那就OK了。這里,還有個(gè)問(wèn)題也不能忽視,就是某個(gè)Command第一次執行完成了,也持久化成功了,但是它由于某種原因沒(méi)有從消息隊列中刪除。所以,當它下次再被執行時(shí),Command Handler里可能會(huì )報異常,所以,健壯的做法時(shí),我們要捕獲這個(gè)異常。當出現異常時(shí),我們要檢查該Command是否之前已執行過(guò),如果有,就要認為當前Command執行正確,然后要把之前Command產(chǎn)生的事件拿出來(lái)做后續的處理。這個(gè)問(wèn)題有點(diǎn)深入了,我暫時(shí)不細化了。有興趣的可以找我私聊。

Event持久化的冪等處理

然后,因為我們的架構是基于ES的,所以,針對新增或修改聚合根的Command,總是會(huì )產(chǎn)生相應的領(lǐng)域事件(Domain Event)。我們接下來(lái)的要做的事情就是要先持久化事件,再分發(fā)這些事件給所有的外部事件訂閱者。大家知道,聚合根有生命周期,在它的生命周期里,會(huì )經(jīng)歷各種事件,而事件的發(fā)生總有確定的時(shí)間順序。所以,為了明確哪個(gè)事件先發(fā)生,哪個(gè)事件后發(fā)生,我們可以對每個(gè)事件設置一個(gè)版本號,即version。聚合根第一個(gè)產(chǎn)生的事件的version為1,第二個(gè)為2,以此類(lèi)推。然后聚合根本身也有一個(gè)版本號,用于記錄當前自己的版本是什么,它每次產(chǎn)生下一個(gè)事件時(shí),也能根據自己的版本號推導出下一個(gè)要產(chǎn)生的事件的版本號是什么。比如聚合根當前的版本號為5,那下一個(gè)事件的版本號則為6。通過(guò)為每個(gè)事件設計一個(gè)版本號,我們就能很方便的實(shí)現聚合根產(chǎn)生事件時(shí)的并發(fā)控制了,因為一個(gè)聚合根不可能產(chǎn)生兩個(gè)版本號一樣的事件,如果出現這種情況,那說(shuō)明一定是出現并發(fā)沖突了。也就是一定是出現了同一個(gè)聚合根同時(shí)被兩個(gè)Command修改的情況了。所以,要實(shí)現事件持久化的冪等處理,也很好做了,就是db中的事件表,對聚合根ID+聚合根當前的version建唯一索引。這樣就能在db層面,確保Event持久化的冪等處理。另外,對于事件的持久化,我們也可以像秒殺那樣,實(shí)現Group Commit。就是Command產(chǎn)生的事件不用立馬持久化,而是可以先積累到一定的量,比如50個(gè),然后再一次性Group Commit所有的事件。然后事件持久化完成后,再修改每個(gè)聚合根的狀態(tài)即可。如果Group Commit事件時(shí)遇到并發(fā)沖突(由于某個(gè)聚合根的事件的版本號有重復),則退回為單個(gè)一個(gè)個(gè)持久化事件即可。為什么可以放心的這樣做?因為我們已經(jīng)基本做到確保一個(gè)聚合根同一時(shí)刻只會(huì )被一個(gè)Command修改。這樣就能基本保證,這些Group Commit的事件也不會(huì )出現版本號沖突的情況。所以,大家是否覺(jué)得,很多設計其實(shí)是一環(huán)套一環(huán)的。Group Commit何時(shí)出發(fā)?我覺(jué)得可以只要滿(mǎn)足兩個(gè)條件了就可以觸發(fā):1)某個(gè)定時(shí)的周期到了就可以觸發(fā),這個(gè)定時(shí)周期可以根據自己的業(yè)務(wù)場(chǎng)景進(jìn)行配置,比如每隔50ms觸發(fā)一次;2)要Commit的事件到達某個(gè)最大值,即每批可以持久化的事件個(gè)數的最大值,比如每50個(gè)事件為一批,這個(gè)BatchSize也需要根據實(shí)際業(yè)務(wù)場(chǎng)景和你的存儲db的性能綜合測試評估來(lái)得到一個(gè)最適合的值;何時(shí)可以使用Group Commit?我覺(jué)得只有是在并發(fā)非常高,當單個(gè)持久化事件遇到性能瓶頸時(shí),才需要使用。否則反而會(huì )降低事件持久化的實(shí)時(shí)性,Group Commit提高的是高并發(fā)下單位時(shí)間內持久化的事件數。目的是為了降低應用和DB之間交互的次數,從而減少I(mǎi)O的次數。不知不覺(jué)就說(shuō)到了最開(kāi)始說(shuō)的那3點(diǎn)性能優(yōu)化中的,盡量減少I(mǎi)O了,呵呵。

Event消費時(shí)的冪等處理

CQRS架構圖中,事件持久化完成后,接下來(lái)就是會(huì )把這些事件發(fā)布出去(發(fā)送到分布式消息隊列),給消費者消費了,也就是給所有的Event Handler處理。這些Event Handler可能是更新Q端的ReadDB,也可能是發(fā)送郵件,也可能是調用外部系統的接口。作為框架,應該有職責盡量保證一個(gè)事件盡量不要被某個(gè)Event Handler重復消費,否則,就需要Event Handler自己保證了。這里的冪等處理,我能想到的辦法就是用一張表,存儲某個(gè)事件是否被某個(gè)Event Handler處理的信息。每次調用Event Handler之前,判斷該Event Handler是否已處理過(guò),如果沒(méi)處理過(guò),就處理,處理完后,插入一條記錄到這個(gè)表。這個(gè)方法相信大家也都很容易想到。如果框架不做這個(gè)事情,那Event Handler內部就要自己做好冪等處理。這個(gè)思路就是select if not exist, then handle, and at last insert的過(guò)程??梢钥吹竭@個(gè)過(guò)程不像前面那兩個(gè)過(guò)程那樣很?chē)乐?,因為在并發(fā)的情況下,理論上還是會(huì )出現重復執行Event Handler的情況?;蛘呒幢悴皇遣l(fā)時(shí)也可能會(huì )造成,那就是假如event handler執行成功了,但是last insert失敗了,那框架還是會(huì )重試執行event handler。這里,你會(huì )很容易想到,為了做這個(gè)冪等支持,Event Handler的一次完整執行,需要增加不少時(shí)間,從而會(huì )最后導致Query Side的數據更新的延遲。不過(guò)CQRS架構的思想就是Q端的數據由C端通過(guò)事件同步過(guò)來(lái),所以Q端的更新本身就是有一定的延遲的。這也是CQRS架構所說(shuō)的要接收最終一致性的原因。

關(guān)于冪等處理的性能問(wèn)題的思考

關(guān)于CommandStore的性能瓶頸分析

大家知道,整個(gè)CQRS架構中,Command,Event的產(chǎn)生以及處理是非常頻繁的,數據量也是非常大的。那如何保證這幾步冪等處理的高性能呢?對于Command的冪等處理,如果對性能要求不是很高,那我們可以簡(jiǎn)單使用關(guān)系型DB即可,比如Sql Server, MySQL都可以。要實(shí)現冪等處理,只需要把主鍵設計為CommandId即可。其他不需要額外的唯一索引。所以這里的性能瓶頸相當于是對單表做大量insert操作的最大TPS。一般MySQL數據庫,SSD硬盤(pán),要達到2W TPS應該沒(méi)什么問(wèn)題。對于這個(gè)表,我們基本只有寫(xiě)入操作,不需要讀取操作。只有是在Command插入遇到主鍵沖突,然后才可能需要偶爾根據主鍵讀取一下已經(jīng)存在的Command的信息。然后,如果單表數據量太大,那怎么辦,就是分表分庫了。這就是最開(kāi)始談到的,要避開(kāi)海量數據這個(gè)原則了,我想就是通過(guò)sharding避開(kāi)大數據來(lái)實(shí)現繞過(guò)IO瓶頸的設計了。不過(guò)一旦涉及到分庫,分表,就又涉及到根據什么分庫分表了,對于存儲Command的表,我覺(jué)得比較簡(jiǎn)單,我們可以先根據Command的類(lèi)型(相當于根據業(yè)務(wù)做垂直拆分)做第一級路由,然后相同Command類(lèi)型的Command,根據CommandId的hashcode路由(水平拆分)即可。這樣就能解決Command通過(guò)關(guān)系型DB存儲的性能瓶頸問(wèn)題。其實(shí)我們還可以通過(guò)流行的基于key/value的NoSQL來(lái)存儲,比如可以選擇本地運行的leveldb,或者支持分布式的ssdb,或者其他的,具體選擇哪個(gè),可以結合自己的業(yè)務(wù)場(chǎng)景來(lái)選擇??傊?,Command的存儲可以有很多選擇。

關(guān)于EventStore的性能瓶頸分析

通過(guò)上面的分析,我們知道Event的存儲唯一需要的是AggregateRootId+Version的唯一索引,其他就無(wú)任何要求了。那這樣就和CommandStore一樣好辦了。如果也是采用關(guān)系型DB,那只要用AggregateRootId+Version這兩個(gè)作為聯(lián)合主鍵即可。然后如果要分庫分表,我們可以先根據AggregateRootType做第一級垂直拆分,即把不同的聚合根類(lèi)型產(chǎn)生的事件分開(kāi)存儲。然后和Command一樣,相同聚合根產(chǎn)生的事件,可以根據AggregateRootId的hashcode來(lái)拆分,同一個(gè)AggregateRootId的所有事件都放一起。這樣既能保證AggregateRootId+Version的唯一性,又能保證數據的水平拆分。從而讓整個(gè)EventStore可以無(wú)限制水平伸縮。當然,我們也完全可以采用基于key/value的NoSQL來(lái)存儲。另外,我們查詢(xún)事件,也都是會(huì )確定聚合根的類(lèi)型以及聚合根的ID,所以,這和路由機制一直,不會(huì )導致我們無(wú)法知道當前要查詢(xún)的聚合根的事件在哪個(gè)分區上。

聚合根的內存模式(In-Memory)

In-Memory模式也是一種減少網(wǎng)絡(luò )IO的一種設計,通過(guò)讓所有生命周期還沒(méi)結束的聚合根一直常駐在內存,從而實(shí)現當我們要修改某個(gè)聚合根時(shí),不必再像傳統的方式那樣,先從db獲取聚合根,再更新,完成后再保存到db了。而是聚合根一直在內存,當Command Handler要修改某個(gè)聚合根時(shí),直接從內存拿到該聚合根對象即可,不需要任何序列化反序列化或IO的操作?;贓S模式,我們不需要直接保存聚合根,而是只要簡(jiǎn)單的保存聚合根產(chǎn)生的事件即可。當服務(wù)器斷電要恢復聚合根時(shí),則只要用事件溯源(Event Sourcing, ES)的方式恢復聚合根到最新?tīng)顟B(tài)即可。

了解過(guò)actor的人應該也知道actor也是整個(gè)集群中就一個(gè)實(shí)例,然后每個(gè)actor自己都有一個(gè)mailbox,這個(gè)mailbox用于存放當前actor要處理的所有的消息。只要服務(wù)器不斷電,那actor就一直存活在內存。所以,In-Memory模式也是actor的一個(gè)設計思想之一。像之前很轟動(dòng)的國外的一個(gè)LMAX架構,號稱(chēng)每秒單機單核可以處理600W訂單,也是完全基于in-memory模式。不過(guò)LMAX架構我覺(jué)得只要作為學(xué)習即可,要大范圍使用,還是有很多問(wèn)題要解決,老外他們使用這種架構來(lái)處理訂單,也是基于特定場(chǎng)景的,并且對編程(代碼質(zhì)量)和運維的要求都非常高。具體有興趣的可以去搜一下相關(guān)資料。

關(guān)于in-memory架構,想法是好的,通過(guò)將所有數據都放在內存,所有持久化都異步進(jìn)行。也就是說(shuō),內存的數據才是最新的,db的數據是異步持久化的,也就是某個(gè)時(shí)刻,內存中有些數據可能還沒(méi)有被持久化到db。當然,如果你說(shuō)你的程序不需要持久化數據,那另當別論了。那如果是異步持久化,主要的問(wèn)題就是宕機恢復的問(wèn)題了。我們看一下akka框架是怎么持久化akka的狀態(tài)的吧。

  1. 多個(gè)消息同時(shí)發(fā)送給actor時(shí),全部會(huì )先放入該actor的mailbox里排隊;
  2. 然后actor單線(xiàn)程從mailbox順序消費消息;
  3. 消費一個(gè)后產(chǎn)生事件;
  4. 持久化事件,akka-persistence也是采用了ES的方式持久化;
  5. 持久化完成后,更新actor的狀態(tài);
  6. 更新?tīng)顟B(tài)完成后,再處理mailbox中的下一個(gè)消息;

從上面的過(guò)程,我們可以看出,akka框架本質(zhì)上也實(shí)現了避免資源競爭的原則,因為每個(gè)actor是單線(xiàn)程處理它的mailbox中的每個(gè)消息的,從而就避免了并發(fā)沖突。然后我們可以看到akka框架也是先持久化事件之后,再更新actor的狀態(tài)的。這說(shuō)明,akka采用的也叫保守的方式,即必須先確保數據落地,再更新內存,再處理下一個(gè)消息。真正理想的in-memory架構,應該是可以忽略持久化,當actor處理完一個(gè)消息后,立即修改自己的狀態(tài),然后立即處理下一個(gè)消息。然后actor產(chǎn)生的事件的持久化,完全是異步的;也就是不用等待持久化事件完成后再更新actor的狀態(tài),然后處理下一個(gè)消息。

我認為,是不是異步持久化不重要,因為既然大家都要面臨一個(gè)問(wèn)題,就是要在宕機后,恢復actor的狀態(tài),那持久化事件是不可避免的。所以,我也是認為,事件不必異步持久化,完全可以像akka框架那樣,產(chǎn)生的事件先同步持久化,完成后再更新actor的狀態(tài)即可。這樣做,在宕機恢復actor的狀態(tài)到最新時(shí),就只要簡(jiǎn)單的從db獲取所有事件,然后通過(guò)ES得到actor最新?tīng)顟B(tài)即可。然后如果擔心事件同步持久化有性能瓶頸,那這個(gè)總是不可避免,這塊不做好,那整個(gè)系統的性能就上不去,所以我們可以采用SSD,sharding, Group Commit, NoSQL等方法,優(yōu)化持久化的性能即可。當然,如果采用異步持久化事件的方式,確實(shí)能大大提高actor的處理性能。但是要做到這點(diǎn),還需要有一些前提的。比如要確保整個(gè)集群中一個(gè)actor只有一個(gè)實(shí)例,不能有兩個(gè)一樣的actor在工作。因為如果出現這種情況,那這兩個(gè)一樣的actor就會(huì )同時(shí)產(chǎn)生事件,導致最后事件持久化的時(shí)候必定會(huì )出現并發(fā)沖突(事件版本號相同)的問(wèn)題。但要保證急群眾一個(gè)actor只有一個(gè)實(shí)例,是很困難的,因為我們可能會(huì )動(dòng)態(tài)往集群中增加服務(wù)器,此時(shí)必定會(huì )有一些actor要遷移到新服務(wù)器。這個(gè)遷移過(guò)程也很復雜,一個(gè)actor從原來(lái)的服務(wù)器遷移到新的服務(wù)器,意味著(zhù)要先停止原服務(wù)器的actor的工作。然后還要把actor再新服務(wù)器上啟動(dòng);然后原服務(wù)器上的actor的mailbox中的消息還要發(fā)給新的actor,然后后續可能還在發(fā)給原actor的消息也要轉發(fā)到新的actor。然后新的actor重啟也很復雜,因為要確保啟動(dòng)之后的actor的狀態(tài)一定是最新的,而我們知道這種純in-memory模式下,事件的持久化時(shí)異步的,所以可能還有一些事件還在消息隊列,還沒(méi)被持久化。所以重啟actor時(shí)還要檢查消息隊列中是否還有未消費的事件。如果還有,就需要等待。否則,我們恢復的actor的狀態(tài)就不是最新的,這樣就無(wú)法保證內存數據是最新的這個(gè)目的,這樣in-memory也就失去了意義。這些都是麻煩的技術(shù)問(wèn)題??傊?,要實(shí)現真正的in-memory架構,沒(méi)那么容易。當然,如果你說(shuō)你可以用數據網(wǎng)格之類(lèi)的產(chǎn)品,無(wú)分布式,那也許可行,不過(guò)這是另外一種架構了。

上面說(shuō)了,akka框架的核心工作原理,以及其他一些方面,比如akka會(huì )確保一個(gè)actor實(shí)例在集群中只有一個(gè)。這點(diǎn)其實(shí)也是和本文說(shuō)的一樣,也是為了避免資源競爭,包括它的mailbox也是一樣。之前我設計ENode時(shí),沒(méi)了解過(guò)akka框架,后來(lái)我學(xué)習后,發(fā)現和ENode的思想是如此接近,呵呵。比如:1)都是集群中只有一個(gè)聚合根實(shí)例;2)都對單個(gè)聚合根的操作的Command做排隊處理;3)都采用ES的方式進(jìn)行狀態(tài)持久化;4)都是基于消息驅動(dòng)的架構。雖然實(shí)現方式有所區別,但目的都是相同的。

小結

本文,從CQRS+Event Sourcing的架構出發(fā),結合實(shí)現高性能的幾個(gè)要注意的點(diǎn)(避開(kāi)網(wǎng)絡(luò )開(kāi)銷(xiāo)(IO),避開(kāi)海量數據,避開(kāi)資源爭奪),分析了這種架構下,我所想到的一些可能的設計。整個(gè)架構中,一個(gè)Command在被處理時(shí),一般是需要做兩次IO,1)持久化Command;2)持久化事件;當然,這里沒(méi)有算上消息的發(fā)送和接收的IO。整個(gè)架構完全基于消息驅動(dòng),所以擁有一個(gè)穩定可擴展高性能的分布式消息隊列中間件是比不可少的,EQueue正是在向這個(gè)目標努力的一個(gè)成果。目前EQueue的TCP通信層,可以做到發(fā)送100W消息,在一臺i7 CPU的普通機器上,只需3s;有興趣的同學(xué)可以看一下。最后,ENode框架就是按照本文中所說(shuō)的這些設計來(lái)實(shí)現的,有興趣的朋友歡迎去下載并和我交流哦!不早了,該睡了。

 

本站僅提供存儲服務(wù),所有內容均由用戶(hù)發(fā)布,如發(fā)現有害或侵權內容,請點(diǎn)擊舉報。
打開(kāi)APP,閱讀全文并永久保存 查看更多類(lèi)似文章
猜你喜歡
類(lèi)似文章
淺談12306 核心模型設計思路和架構設計
微服務(wù)架構下的分布式數據管理
對CQRS架構的幾點(diǎn)疑問(wèn)
CQRS(命令查詢(xún)職責分離)和 EDA(事件驅動(dòng)架構)
Axon Framework架構概述
軟件各種系統架構圖(二)
更多類(lèi)似文章 >>
生活服務(wù)
分享 收藏 導長(cháng)圖 關(guān)注 下載文章
綁定賬號成功
后續可登錄賬號暢享VIP特權!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服

欧美性猛交XXXX免费看蜜桃,成人网18免费韩国,亚洲国产成人精品区综合,欧美日韩一区二区三区高清不卡,亚洲综合一区二区精品久久