類似貼吧回帖模組設計經驗
本文主要分享了我在設計評論模組中的一些心得,希望對讀者有些許幫助。
需求分析
現階段評論做的最好的我想應該是網易新聞
(app)裡面的評論模組了,其“蓋樓”的方式讓人印象深刻,評論已經成為該app的核心功能之一了。市面上大部分app的評論模組設計的還是相對簡單的,這是可以理解的,因為評論模組不是這些app的核心功能之一。
在設計評論模組前可以和pd或者boss溝通,我們的評論功能是核心功能之一嗎?實際上,90%的app採用簡單的評論設計就可以了,
也就是採用一問一答
,類似於如下的設計。
一問一答
這種設計十分簡單、直接,也滿足了使用者評論、回覆的基本要求,對於沒有大量使用者評論或者評論不是核心功能的app來說就夠用了。
暫且把這種場景稱之為場景A
。
如果你是新聞類或者諮詢類的app,有著大量的使用者評論,那麼設計“蓋樓”的效果還是可取的,這樣能幫助使用者找到該條評論或者回復的上下文情景。但是根據“蓋樓”的顯示效果不同,設計上也是有很大的差別的。如果是以評論為主
的顯示方式,類似於下面的顯示方式。
評論為主這裡可以把評論分為評論
和回覆
,所有的回覆
均掛在評論
下面,類似於樹狀結構。把這種場景稱之為場景B
最後就是類似於網易新聞的評論設計了,貼一張截圖
同級顯示這種場景下設計最為複雜,因為回覆和評論是同等級的,回覆還可以引用完整的回覆路徑,就是可以溯源到最開始的評論上。這種場景我將至稱為場景C
。
資料庫設計
由於我 一直使用MySQL
,我就以mysql
為例談一下針對上面三種場景的設計。
場景A
這種場景下一般評論數量較少,評論不為活躍,可以把不區分評論和回覆,而統一看成評論。區別在於有的評論是直接評論主題
(每個評論都掛在某個主題下,如文章、帖子等),而有些評論是@
其他使用者的,為了能cover這兩張場景,使用一張表就可以達到效果,評論表如下設計:
表字段 | 欄位說明 |
---|---|
id | 主鍵 |
topic_id | 主題ID |
topic_type | 主題type |
content | 評論內容 |
from_uid | 評論使用者id |
to_uid | 評論目標使用者id |
為了能複用評論模組,我們引入一個topic_type
欄位來區分主題的類別。
from_uid
表示評論人的id,通過該id我們可以檢索到評論人的相關資訊。
to_uid
是評論目標人的id,如果沒有目標人,則該欄位為空。
出於效能的考慮,往往我們會冗餘評人的相關資訊到評論表中,比如評論人的nick、頭像等,目標使用者也是如此。這樣一來我們就只用查詢單表就可以達到顯示的效果。
有時,目標使用者有多個,那麼可以將to_uid
欄位修改為to_uids
,儲存時用分隔符來分割使用者id,而目標使用者的資訊再去查詢快取或者資料庫。也可以簡單的將多個目標使用者的資訊一起存成json格式,可以應付簡單的展現需求。
場景B
在以評論為主的樹形顯示的情況下,資料庫的設計十分靈活,可以使用單表,新增一個parent_id
欄位來指向父評論。如果資料庫本身支援巢狀查詢,那麼還是比較方便的,SqlServer、Oracle都支援,但是mysql不支援,那就只能通過儲存過程來實現。在網際網路應用中,能不使用觸發器
`儲存過程`的話,儘量不要去使用,因為其對效能有影響。
我們還可以將評論拆分為評論表
和回覆表
,評論
掛在各種主題
下面,而回覆
都掛在評論
下面。
評論表
的設計如下:
表字段 | 欄位說明 |
---|---|
id | 主鍵 |
topic_id | 主題ID |
topic_type | 主題type |
content | 評論內容 |
from_uid | 評論使用者id |
回覆表
的設計如下:
表字段 | 欄位說明 |
---|---|
id | 主鍵 |
comment_id | 評論ID |
reply_id | 回覆目標id |
reply_type | 回覆型別 |
content | 回覆內容 |
from_uid | 回覆使用者id |
to_uid | 目標使用者id |
由於我們拆分了評論和回覆,那麼評論表就不再需要目標使用者欄位了,因為評論均是使用者對主題的評論,評論表的設計更佳簡潔了。
回覆表我添加了一個comment_id
欄位來表示該回復掛在的根評論id,這樣設計也是出於效能方面的考慮,我們可以直接通過評論id一次性的撈出該評論下的所有回覆,然後通過程式來編排回覆的顯示結構。通過適當的冗餘來提高效能也是常用的優化手段之一。這裡給出一段我通過來評論id來查詢並組織所有回覆的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public List<ReplyDTO> getReplyListByRid(Long rid) { List<ReplyDO> replyDOList = replyDAO.queryReplyByCid(rid); if (replyDOList == null || replyDOList.size() == 0) { return new ArrayList<>(); } List<ReplyDTO> replyDTOList = new ArrayList<>(); List<ReplyDTO> parentList = new ArrayList<>(); for (ReplyDO replyDO : replyDOList) { ReplyDTO replyDTO = convertReplyToDTO(replyDO); if (replyDTO.getReplyType() == ReplyType.COMMENT) { replyDTOList.add(replyDTO); parentList.add(replyDTO); } else { boolean foundParent = false; if (replyDTOList.size() > 0) { for (ReplyDTO parent : parentList) { if (parent.getId().equals(replyDTO.getReplyId())) { if (parent.getNext() == null) { parent.setNext(new ArrayList<ReplyDTO>()); } parent.getNext().add(replyDTO); parentList.add(replyDTO); foundParent = true; break; } } } if (!foundParent) { throw new RuntimeException("sort reply error,should not go here."); } } } return replyDTOList; } |
reply_type
表示回覆的型別,因為回覆可以是針對評論的回覆(comment),也可以是針對回覆的回覆(reply),
通過這個欄位來區分兩種情景。
reply_id
表示回覆目標的id,如果reply_type是comment的話,那麼reply_id=commit_id,如果reply_type是reply的話,這表示這條回覆的父回覆。
在資料結構的設計上,我在replyDTO中設計了一個List<ReplyDTO>
next
屬性,這樣在形成了一個樹形的結構,類似如下結構。
客戶端可以直接根據該結構來進行樹形結構的顯示。
場景c
要達到網易新聞中評論的效果我還沒有特別好的建議。這種場景中評論和回覆是同級顯示的,回覆不在顯示結構上不用掛在一個評論下面。雙表的設計在這裡就不太合適了,因為涉及到評論和回覆的混排,使用雙表則會導致查詢的邏輯過於複雜。所以建議還是採用單表的設計,不區分評論和回覆會簡化應用層的邏輯。我們統一都看成評論,而有些評論是可以引用其他評論的。本人推薦採用閉包表的設計,例如:
comment表設計
表字段 | 欄位說明 |
---|---|
id | 主鍵 |
topic_id | 主題ID |
topic_type | 主題type |
content | 評論內容 |
from_uid | 評論使用者id |
parent_children表
表字段 | 欄位說明 |
---|---|
id | 主鍵 |
parent_id | 父id |
child_id | 子id |
comment表儲存所有評論內容,而parent_children表則記錄評論表中各個評論的父子關係。
查詢時往往會按照時間排序,我們可以直接按id或者建立時間降序排列查詢comment表即可。如果使用者想查詢一條評論的完整引用,則可以通過parent_children來找到對應的路徑。向上查詢到評論只需要可執行:
select parent_id from parent_children where child_id=${id} and parent_id != ${id}
向下查詢所有的子孫評論可執行:
select child_id from parent_children where parent_id = ${id} and parent_id != ${id}
閉包表在查詢時非常方便,但是插入的效能稍差,因為除了插入評論表以外,還需要把該條評論所有的父子關係插入到父子關係表中。插入效能會隨著評論層級的加深而線性下降。
海量資料優化
如果你的系統每天都會有成千上萬條評論,那麼單表的設計肯定是不行,優化的方式也有很多。
-
分庫分表。分庫分表是最為常用也最有效的優化方式,建議按照主題來分庫分表。這樣同一個主題下面的評論就會落到同一張表裡,避免了跨表查詢。
-
適當的資料冗餘。如果你需要顯示評論人的相關資訊,那麼在插入評論時就把這些資訊寫入評論表中,避免多次查詢。實際上,如果是紀錄資料,都可以冗餘對應的資料資訊,因為它們的資料的實時行和一致性要求並不高,使用者不會因為評論中的頭像沒更新而撕了你,哈哈。
-
附加冪等資料只允許單項操作。如果pd要求你能給評論點贊,那麼你可以告訴他只能點贊,不能取消。因為從冪等性的要求來說,每個贊都是一條記錄。評論的贊數如果都從點贊表中統計得出,那麼效能開銷會十分巨大,而且點贊如此輕量級的一個操作一定會加劇點贊表的競爭操作。所以建議直接在評論表中新增一個
like_count
的計數器,該欄位只增不減。熱門評論加快取。類似於網易新聞的熱門評論,讀取頻度非常高,可以專門開介面給客戶端,同時該介面做快取。