天天看點

DDD實踐問題之 - 關于論壇的文章回複統計資訊的更新的思考

論壇領域的核心概念是:文章、回複。大家都知道,一個文章可以有零個或多個回複。對同一個文章,不同的人可以并行發表回複。回複發表後,檢視文章詳情時,可以根據回複的發表時間排序顯示;此外,我們還關心某個文章的最新發表的回複、最新回複的作者、最新回複時間,以及總回複數。

我們設計的系統,應該在實作上述的領域問題的前提下,盡量做到發表回複時要快,且能保證文章能對它的所有回複的統計資訊能正确的統計出來。

把文章設計為聚合根,回複設計為文章的子實體。然後發表回複就是在文章聚合根裡添加一個回複實體。

優點:

模型清晰并符合人們對領域的一般認識,文章和回複1對多,很自然想到這個模型設計;

文章内部就已經聚合了所有的回複資訊,資料強一緻性,是以統計資訊自然不用擔心;

在db層面,當發表一個回複時,先插入回複,再更新文章回複統計資訊,兩個步驟在一個資料庫事務裡,確定了資料強一緻性。

缺點:

當很多人同時對同一個文章進行回複時,也就是回複的并發很高時,會被阻塞;因為每個回複都要同步更新文章的回複統計資訊;

文章由于聚合了所有的回複,是以會導緻文章本身比較重;必須要求orm架構支援延遲加載,否則擷取文章會成為一個問題;比如,我僅僅為了修改文章的内容,而要把整個文章的回複都加載出來,會付出很多不必要的代價。

這種方案,大部分情況下都沒問題。因為大部分論壇,對一個文章的回複的并發都不會太高。是以,我覺得設計總體來說是可行的。

把文章和回複都設計為聚合根,回複不再是文章内的子實體。發表回複就是新增了一個回複聚合根。

文章聚合根比較輕量級了,因為它内部不在維護回複了;

高并發建立回複時,不會再有性能問題。但前提是,建立回複後,更新文章的統計資訊必須采用異步的方案,否則如果也是像方案1那樣采用同步+事務的方式,那db層面還是成為瓶頸,并發上不去;

本質問題和方案1類似;如果采用同步更新統計資訊,那并發也是上不去,隻是模型的設計改變了一下;如果采用異步更新統計資訊,那就是消息驅動的架構,就需要考慮消息的丢失、幂等、亂序等問題;比如消息丢失,那統計資訊最終就不正确;假如消息重複被處理(分布式消息隊列一般不會保證消息絕對不會被重複投遞),那統計資訊也不正确;假如消息的處理順序亂序了,那最後的統計資訊也會不對;開發人員需要考慮到這些問題。當然,如果你不care,也可以,呵呵。

這種方案,模型層面做了一些變化,db層面,引入了異步更新統計資訊的思想,但要求技術上需要處理eda架構所帶來的典型問題。如果論壇的并發問題确實影響了使用者的體驗,則可以嘗試考慮次方案;

模型層面,設計為兩個聚合根還是一個聚合根無所謂。然後統計資訊決定不儲存,也就是不備援存儲統計資訊了。大家知道,統計資訊,一般隻是用來展示資料使用,并不會參與到業務邏輯中去。是以,理論上我們不儲存統計資訊也可以,因為我們總是可以在需要的時候動态查詢統計出所需要的資訊,sql的統計功能是很強大的,呵呵。

無需備援存儲統計資訊,設計簡單;

發表回複時也無并發問題;

需要付出更多的查詢代價,尤其是在論壇資料量大,查詢并發高的時候;而且還要根據統計資訊的結果進行分頁的話,資料量一大,性能一定比較糟糕。當然,我們還有辦法,比如分庫分表,減少單表的資料量,確定查詢性能;或者,總是隻支援查詢近期2周的資料,曆史資料不顯示,必須通過其他方式檢視。這樣的話,也可以控制活躍資料在一定的數量級之内;

上面提到的分庫分表,方案顯得有點重;隻保顯示近期活躍的資料相當于犧牲了一部分業務功能,換來更高的查詢性能;隻要業務上能接受,就可行;

這種方案,我覺得需要業務人員和設計人員仔細評估考量。大家覺得如何呢?

這種方案,一般老外的開源論壇中出現的較多。領域模型的設計有較大不同,因為對論壇核心領域的認識有所不同。當使用者發帖時,我們把文章的标題和内容看成兩個部分,标題叫thread,内容是屬于這個thread下的第一個message;然後對這個文章的回複,看成是第二個message。是以,通過這樣的了解,thread還是可以了解為文章,但其含義和我們通常所了解的文章稍微有點不同,因為這個文章僅僅隻有标題沒有内容,它的作用就是“穿針引線”,thread的英文意思就是線索、穿成串的意思。可見,一個thread就是對很多message的串聯,我們也可以把thread了解為一個主題,這個主題下有若幹個讨論内容。然後一個message,即消息,就表達了一個内容。是以,當使用者發帖時,就是會生成一個thread,以及一個message,兩個聚合根;當使用者發表回複時,就是隻是建立一個message聚合根。另外,也很明顯thread應該維護所有的回複的統計資訊,因為我們設計它的目的也在于此(串聯message,以及維護message統計資訊)。然後,message就是簡單的表達某個内容即可,同時message上記錄目前自己所屬的threadid即可。好像說的有點啰嗦,呵呵!

上面這個描述的是我們對論壇核心領域有不同的認識,最後設計出來的領域模型也完全不同。是以,我們發現,當我們在做ddd領域驅動設計時,往往每個人最後設計出來的領域模型是不同的,因為每個人對同一個領域的問題的本質了解不同。甚至可以說,這個是每個人的世界觀的不同導緻。雖然這樣,但ddd的最大價值在于會要求我們主動去思考領域,思考如何用模型,從oo的角度去思考問題,思考狀态的一緻性維護,狀态變化的規則,等。相比傳統資料庫驅動的方式、面向過程的方式,腦子裡隻有資料結構、關系、以及過程。沒有對象、互動、職責這方面的思考意識。

我承認,這個領域模型的設計确實不錯,甚至更好!但我們不能說,前面的領域模型的設計(文章包含标題和内容、回複隻有内容)是錯誤的,因為隻是對領域問題的不同了解而已。前面的領域模型的設計,也是自然合理的,我認為。

當然,讨論回來,在db層面,不管領域模型如何設計,我們在更新文章統計資訊時,還是會碰到并發的情況。這個其實是多使用者并發回帖導緻的技術問題,不是業務上的問題。技術問題就是需要通過技術手段去解決。當然,有時也可以把某些看似是技術問題的問題,提煉出合适的業務規則,通過聚合根封裝業務規則,確定資料一緻性的思路來解決。下面我們來看一下方案5。

基于enode架構實作,采用cqrs架構。領域模型的設計,也是設計為文章和回複兩個聚合根。不同的是文章中聚合了回複的統計資訊,設計一個值對象,表示文章的目前回複的統計資訊。包括:最近回複的id、作者、回複時間,以及總回複數。為什麼要這樣設計?因為經過對領域進一步的分析和思考,發現了領域内的一個潛在的業務規則。就是我們關心文章的“最後”的回複資訊。關鍵問題就是在這個“最後”兩個字上。既然是最後,那就必須要知道哪個是最後,根據什麼判斷?就是根據回複的建立時間。然後,在高并發建立回複的時候,我們需要有一個地方,可以準确的統計出這個最後的回複是哪個。前面幾個方案,要麼通過db的強一緻性事務保證,要麼依賴消息隊列的順序消息處理(必須依靠開發人員要處理好各種eda的問題才行)。這些方案雖然最終卻是能統計出這個最後的回複資訊;但我認為,這個是屬于領域内的一個業務規則;這個規則是由于我們所關心的統計資訊而必須引入的。是以,更好的方案應該是用聚合根來維護這個業務規則。

是以,文章本身可以不用聚合所有的回複資訊,因為文章本身确實不關心回複資訊本身,它隻是關心回複的統計資訊(最後一個回複的資訊以及總回複數);是以,我們設計文章聚合時,隻需要再讓其内聚一個包含回複統計資訊的值對象即可。

當有一個新的回複産生後,發送一個指令,通知回複的文章更新其統計資訊;然後文章處理這個指令時,内部判斷目前回複的時間是否是最新時間,如果是,則更新最後回複資訊和總回複數;如果不是,則隻更新回複數。然後文章的統計資訊更新後,産生領域事件,表示文章的統計資訊有更新,然後cqrs的event handler,根據領域事件,更新讀庫文章表的那幾個統計字段即可;

這種方案,通過enode提供的技術保證,可以確定消息至少投遞一次,確定消息的幂等處理,以及消息(領域事件)的順序處理,以及最重要的一點,最同一個聚合根,確定盡量做到了無并發操作;就算出現并發,也能架構層面自動重試。是以,開發人員就不用再關心這些技術相關的問題了。

個人認為,這種方案在基于enode架構引入cqrs架構的異步思路,解決高并發的問題的同時,進一步挖掘了業務需求,分析出了潛在的業務規則,通過用聚合根來維護這個業務規則,最終確定了資料的最終一緻性;缺點是,依賴了enode架構。對我個人來說,肯定是最喜歡這種方式,呵呵。目前enode forum案例,就是采用這種方式來實作文章的回複統計資訊的維護和存儲。

我覺得很多問題的解決思路都是類似的。我總喜歡使用大家都通俗易懂的案例來分析、讨論。因為隻有大家對這個業務都比較了解,才具有讨論的基礎。另外,我相信每個人都有舉一反三的能力。一旦我們把某個業務問題分析清楚,那也許遇到其他類似的業務問題時,我們曾經的業務問題分析經驗會幫助到我們,進而可以更加快速的了解和解決相似問題領域。這也是我為什麼要花精力寫出這麼多方案的原因。多思考一點,多深入一點。自己就會多成長一點,對未來的領域問題的分析就會更快速一點。

不知不覺又下半夜了,眼睛酸,不寫了,基本把能想到的都寫了,呵呵。