消息隊列與領域事件

一年前做過兩個比較膚淺的消息隊列的總結(jié),消息隊列作用,消息隊列應用-使用異步隊列就解耦了嗎
。如今回過頭來再梳理一下對消息隊列的認知。
某些大廠的云服務文檔對了解一些消息隊列基本知識還是比較有幫助比如亞馬遜消息隊列文檔
本文不梳理技術細節(jié),僅總結(jié)自己在應用層面的認知。

消息隊列模型

消息隊列主要是兩種模型,隊列模型與主題模型。

  1. 隊列模型(queue)
    也即點對點或者一對一模式,1個消息只會被一個消費者消費??梢杂幸粋€或者多個消費者同時消費一個隊列,但是被消費的消息不會重復。
  2. 主題模型(topic)
    也即發(fā)布訂閱模式,領域設計中也常說扇出設計模式。消息可以被多個消費者消費。此類隊列一般被稱為一個topic,所有訂閱該topic的consumer都可消費所有消息。

消息隊列具體應用

消息隊列功能

部分內(nèi)容參考消息隊列功能
消息隊列通常具備的功能如下:

  1. 異步通信
    消息的生產(chǎn)者在消息成功發(fā)送到隊列中即可立即返回,不需等待消費者接收并處理消息,生產(chǎn)和消費完全異步。該功能在隊列模型和主題模型均適用。
  2. 延遲隊列
    許多消息隊列都支持為消息設置特定的傳送時間。如果需要為所有消息設置相同延遲,可以設置一個延遲隊列。
  3. 死信隊列
    死信隊列可以接收其他隊列發(fā)來的未成功處理的消息。這便于將此類消息放在一邊以進行深入檢測,而不會妨礙隊列處理或?qū)?CPU 周期耗費在可能永遠無法成功處理的消息上。
  4. FIFO (先進先出) 隊列
    消息本身是有時間的先后順序的。生產(chǎn)者生產(chǎn)的消息到隊列也有到達順序,消息隊列通常會提供盡力確保消息大致按其發(fā)送的順序進行傳送,且消息至少傳送一次。消費者的消費順序受網(wǎng)絡、確切到達時間、處理速度等的影響,但有些消息隊列(比如kafka),采用特定規(guī)則(比如相同用戶的消息發(fā)送到一個partition,線程封閉的消費)可以保證消息的有序消費。
  5. 至少一次 & 最多一次 & 精確一次
    通常消息隊列都是支持至少一次消費的。
    精確一次可以通過至少一次+冪等梳理。
    最多一次會丟消息,除非消息可以丟失,不然一般不用。
  6. 隊列模型&主題模型
    上文已討論
  7. 路由功能
    topic模型下可以支持消息的路由。比如指定不同的路由key,topic根據(jù)路由key分發(fā)到不同的consumer。再比如kafka中的partition其實也是一種路由方式,消息經(jīng)過hash后到達特定的partition,由某一個consumer線程消費。
  8. 消息復用
    topic模型下,同一個消息可以分發(fā)給多個consumer

消息隊列使用場景

消息隊列的作用大概可以用這么幾個詞來概括:異步、削峰、解耦、提高性能。
異步,這是消息隊列最核心的功能,其他功能都依賴異步性實現(xiàn)。生產(chǎn)者將消息發(fā)送到消息隊列后可以立即返回,不用等待消費者的響應。
削峰,可以理解為異步帶來的好處之一,消息隊列能夠平衡生產(chǎn)和消費的速度。生產(chǎn)的消息快可以暫時將消息留在隊列中,消費者慢慢消費。
解耦,這里主要指的是系統(tǒng)組件之間的通信方式的解耦,也是異步帶來的好處之一,生產(chǎn)者不需要等待消費者響應,在組件通信中,消息生產(chǎn)和消費方都依賴消息隊列,兩者直接就無需直接耦合了。還有另外一種業(yè)務上的耦合,下文討論。
提高性能,通過異步化,一方面可以對消息在消費者里做聚合計算或者批量更新等處理,提高性能。另一方面topic模型可以提高不同組件的并行度,下文討論。


圍繞著這幾個作用,消息隊列的使用場景很多,舉幾個例子說下消息隊列的部分使用場景:

  1. 提高通信的可靠性
    網(wǎng)絡通信不可靠或者下游不可靠的情況下,可以使用消息隊列來確保消息的可靠投遞,增加系統(tǒng)的可靠性。消息生產(chǎn)者不需要過多關注下游或通信鏈路的穩(wěn)定性,交給消息隊列解決。為了提高消息投遞可靠性,可能會造成消費者對消息的重復消費。
  2. 一對多消息發(fā)布
    topic模型的應用,生產(chǎn)者向多個訂閱者提供消息。比如一個訂單支付成功后,可能要很多其他組件做相應處理,比如供應鏈發(fā)貨、贈送優(yōu)惠券、核銷優(yōu)惠券、發(fā)送短信通知??梢杂捎唵蜗到y(tǒng)發(fā)送一個topic消息,供應鏈組件、優(yōu)惠券組件、通知組件訂閱消息,并行處理即可,解耦并且提高性能。另外多用于領域事件發(fā)布場景,下文討論。
    這個訂單的例子除了技術上的解耦外,也是一種業(yè)務上的解耦。訂單服務,不再依賴供應鏈、優(yōu)惠券、通知服務來完成訂單了。
  3. 屏蔽語言差異
    有些公司存在多種技術棧并行的情況,比如java、c#、Python等。用消息隊列,只需要與消息中間件交互即可,不存在語言差異。
  4. 系統(tǒng)組件的解耦
    比如這么兩方面,生產(chǎn)者不需要等待消費者響應、上文提到的訂單和其他組件的解耦。
  5. 削峰
    上文有提到。具體的case比如:秒殺場景下接口請求量巨大,除了用緩存等措施有效增強計算性能外,還可以通過消息隊列對接口請求做削峰處理。


    削峰
  6. 提高計算性能
    消費方可以將消息聚合后做批量計算,比如批量寫入數(shù)據(jù)庫,可以緩解數(shù)據(jù)庫壓力。另外如上文訂單例子,提高了計算并行度。
  7. 優(yōu)先級隊列
    根據(jù)topic的路由功能,可以針對某個特定的路由key加大計算量,優(yōu)先處理。比如根據(jù)kafka的partition實現(xiàn)。

消息隊列與解耦

通常情況下,我們說的消息隊列的解耦功能,主要是由于其異步的特性帶來的技術上的解耦,消息生產(chǎn)者不需要關注消費者的消費狀態(tài),可直接返回。
業(yè)務上來看,還是存在耦合的。比如業(yè)務耦合例子。耦合是不能完全避免的,需要我們做設計的過程中去權衡。這里根據(jù)經(jīng)驗做下總結(jié):


  1. 如果消息隊列僅涉及到兩個系統(tǒng)組件,使用隊列模型,以消費方為主和生產(chǎn)方協(xié)商好格式即可。
  2. 如果消息需要被重復消費,或者對接該消息的組件多于1個,通常采用主題模型,由消息生產(chǎn)者定義格式,消費者接收消息轉(zhuǎn)換成自己的邏輯語言處理。
  3. 涉及到領域事件的發(fā)布,通常使用主題模型,事件消費方將所依賴的上下文的領域事件轉(zhuǎn)換自己所在的上下文通用語言做處理。

總結(jié)來看。消息隊列的解耦可以包括兩方面,技術上的解耦和業(yè)務上的解耦。業(yè)務上的耦合是不可避免的,盡量做到降低耦合性以及消息的復用,我們的設計不能脫離具體的業(yè)務場景。

消息隊列與領域模型

DDD(領域驅(qū)動設計)中的有一個概念叫領域事件。消息中間件是領域事件常用的存儲方式。
本文僅討論和消息隊列相關的內(nèi)容,其他領域事件的用途,比如用來做bug跟蹤、預測分析、操作的撤銷(很多分布式存儲中的undo),這里不做討論。
關于領域事件可以參考《實現(xiàn)領域驅(qū)動設計》這本書??吹揭黄诲e的文章,也是對該書內(nèi)容的總結(jié) ,同時擴展了一些實現(xiàn)方式領域事件

領域事件

暫時沒有找到特別明確的定義,《實現(xiàn)領域驅(qū)動設計》中有過一些概念:領域事件是領域?qū)<宜P心的發(fā)生在領域中的一些事件等。

可以這么理解,對于限界上下文中發(fā)生的每一件事,我們都用事件的形式予以捕獲并發(fā)布給訂閱方處理。領域事件也應該作為領域模型的通用語言的一部分。(限界上下文和通用語言是領域模型戰(zhàn)略設計的核心)

領域事件的主要用途

參考此文

  1. 保證聚合間的數(shù)據(jù)一致性
  2. 替換批量處理
  3. 實現(xiàn)事件源模式
  4. 進行限界上下文集成

這些也可以理解為消息隊列的一些使用場景,當然領域事件是不限于實現(xiàn)方式的。

領域事件用途

領域事件的實現(xiàn)

最簡單的領域事件的發(fā)布模式就是觀察者模式,消息隊列或者領域設計中我們通常叫做發(fā)布-訂閱模式。
領域事件的消費者可以是本地模塊也可以是遠程模塊,對應的領域事件存儲的方式包括共享內(nèi)存形式、restful資源形式、以及消息中間件等。

  1. 共享內(nèi)存的形式比較好理解,也即用代碼實現(xiàn)一個觀察者模式,將捕獲到的事件通過接口調(diào)用通知給訂閱方接口。
  2. restful資源的形式,可以這么理解,消息發(fā)布者通過restful的接口的形式來發(fā)布資源,消費者根據(jù)消息區(qū)間(可以是時間或者id范圍)來拉取信息,是一種拉的模式。這種方式,我在實踐過程中,在不同公司之間的數(shù)據(jù)同步場景用的比較多。這種情況下
  3. 消息中間件的方式就是本文要討論的重點內(nèi)容了。在實踐過程中,公司內(nèi)部多個系統(tǒng)組件之間的領域事件發(fā)布多采用這種形式。

這里主要討論共享內(nèi)存的形式無法處理的向遠程限界上下文發(fā)布領域事件的方法。在不同的限界上下文見采用領域事件的形式通信時,必須要保證最終一致性。領域事件的發(fā)布通常是一個異步的過程,受限于各系統(tǒng)吞吐量等因素,一個模型的改變可能要過一段時間才能提現(xiàn)到另外一個模型中。
《實現(xiàn)領域驅(qū)動設計》書中討論了向遠程限界上下文發(fā)布領域事件的三個問題:

  1. 消息設施的一致性
    無論是什么形式發(fā)布領域事件,要想保證最終一致性,我們至少要保證兩個存儲的最終一致性:領域模型所使用的的持久化存儲和消息設施所使用的的持久化存儲。這樣保證了持久化領域模型是,領域事件也得以存儲并發(fā)布成功。如果這兩者不一致,會導致最終兩個限界上下文中的狀態(tài)錯誤。
    保證這個一致性有幾個辦法:
    —領域模型和消息設施共享存儲。在這種情況下,模型和事件的提交在一個事務中完成,從而保證兩種的一致性。
    —領域模型的持久化和消息的持久化采用XA事務,有個概念叫做事務消息。這種情況下,模型和消息所用的持久化存儲可以分離,但會降低系統(tǒng)性能。
    —領域模型的存儲中留一塊區(qū)域存儲領域事件,從而在本地事務中完成領域和事件的存儲。然后,通過后臺服務將事件異步發(fā)送到消息隊列中。該方式和與消息設施共享存儲很像,區(qū)別在于該方式可以保證在一個本地事務提交,還可通過restful的方式暴露事件資源。

一般情況下,第三種,是比較優(yōu)雅的解決方案。

  1. 自治服務和系統(tǒng)
    自治可以理解成沒有對遠程RPC服務的調(diào)用,具備高度的獨立性。RPC調(diào)用是具有可靠性、穩(wěn)定性風險。
    通過領域事件的方式,自治服務將領域事件轉(zhuǎn)化為自己的通用語言進行存儲。這里要注意,消費者不是對事件生產(chǎn)者的簡單復制,而是要轉(zhuǎn)化成自己限界上下文的通用語言。
  2. 容許延時
    事件的方式必然存在延時,數(shù)據(jù)的延時可能有比較嚴重的影響,也可能幾無影響,只需要保證最終一致性即可,這塊使我們設計系統(tǒng)要考慮的重要因素。

事務消息

消息生產(chǎn)者本地事務處理與消息發(fā)送可能存在不一致的情況,事務消息是用來實現(xiàn)消息生產(chǎn)者本地事務與消息發(fā)送的原子性,保證消息生產(chǎn)者本地事務處理成功與消息發(fā)送成功的最終一致。事務消息是分布式事務的一種解決方案。
事務消息有多種實現(xiàn)方式,
有些是用戶配合消息中間件實現(xiàn)類似 X/Open XA 的分布事務功能。
有些是利用數(shù)據(jù)庫本地事務,比如去哪兒開源的qmq事務消息
也就是上文提到的消息一致性的第三種方案。

冪等性與消息去重

領域事件通??梢岳斫鉃槭且环N值對象。值對象可以沒有唯一標識。
但是在有些情況下,消息系統(tǒng)可能多次向消費者發(fā)送重復的消息,這時候就需要做好消息的冪等性處理,也就是消息的去重。
做消息去重可以在消息隊列處理也就是保證精確發(fā)送一次,也可以在訂閱方(消費者)處理。利用消息隊列處理將會很麻煩。通常情況下我們在訂閱方在消費時基于自己的領域模型做冪等處理。而一種簡單的方式便是發(fā)布方在發(fā)布事件消息時設置一個唯一的消息id作為事件的唯一標識。
事件唯一標識,將它作為通用屬性進行管理,本身對領域建模影響不大,但對技術處理好處巨大。當我們需要將領域事件發(fā)布到外部的限界上下文時,唯一標識就是一種必然。為了保證事件投遞的冪等性,在發(fā)送端,我們可能會進行多次發(fā)送嘗試,直至明確發(fā)送成功為止;而在接收端,當接收到事件后,需要對事件進行重復性檢測,以保障事件處理的冪等性。此時,事件的唯一標識便可以作為事件去重的依據(jù)。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容