以 B 站爲例,聊聊站內消息系統的設計

本文來自 guang19 投稿(Github 同名,歡迎關注)。前端

使用過簡書,知乎或 b 站的小夥伴應該都有這樣的使用體驗:當有其餘用戶關注咱們或者私信咱們的行爲時,咱們會收到相關的消息。 雖然這些功能看上去簡單,但其背後的設計是很是複雜的,幾乎是一個完成的系統,能夠稱之爲 站內消息系統web

我以 b 站舉例(我的認爲 b 站的消息系統是我見過的很是完美的,UI 也最爲人性化的):後端

b站站內消息

能夠看到 b 站把消息大體分爲了三類:微信

  1. 系統推送的通知(System Notice);
  2. 回覆、@、點贊等用戶行爲產生的提醒(Remind);
  3. 用戶之間的私信(Chat)。

這樣設計不只分類明確,且處於同一個主體的事件提醒還會作一個聚合,極大的提升了用戶體驗,不讓用戶收到太多分散的消息。網絡

舉個例子:好比你在某個視頻或某篇文章下發表了評論,有 100 我的給你的評論點了贊,那麼你但願消息頁面呈現的是一個一個用戶給你點讚的提醒,仍是像如下聚合以後的提醒:框架

消息的聚合

我相信你大機率會選擇後者。編輯器

我認爲對於不少應用來講,這樣的設計都是很是合理的,接下來我寫寫我對於消息系統的設計。ide

系統通知(System Notice)

系統通知通常是由後臺管理員發出,而後指定某一類(全體,我的等)用戶接收。基於此設想,能夠把系統通知大體分爲兩張表:性能

  1. t_manager_system_notice(管理員系統通知表) :記錄管理員發出的通知 ;
  2. t_user_system_notice(用戶系統通知表) : 存儲用戶接受的通知。

t_manager_system_notice 結構以下:flex

字段名 類型 描述
system_notice_id LONG 系統通知 ID
title VARCHAR 標題
content TEXT 內容
type VARCHAR 發給哪些用戶:單用戶 single;全體用戶 all,vip 用戶,具體類型各位小夥伴能夠根據本身的需求選擇
state BOOLEAN 是否已被拉取過,若是已經拉取過,就無需再次拉取
recipient_id LONG 接受通知的用戶的 ID,若是 type 爲單用戶,那麼 recipient 爲該用戶的 ID;不然 recipient 爲 0
manager_id LONG 發佈通知的管理員 ID
publish_time TIMESTAMP 發佈時間

t_user_system_notice 結構以下:

字段名 類型 描述
user_notice_id LONG 主鍵 ID
state BOOLEAN 是否已讀
system_notice_id LONG 系統通知的 ID
recipient_id LONG 接受通知的用戶的 ID
pull_time TIMESTAMP 拉取通知的時間

當管理員發佈一條通知後,將通知插入 t_manager_system_notice 表中,而後系統定時的從 t_manager_system_notice 表中拉取通知,而後根據通知的 type 將通知插入 t_user_system_notice 表中。

若是通知的 type 是 single 的,那就只須要插入一條記錄到 t_user_system_notice 中。若是是全體用戶,那麼就須要將一個通知批量根據不一樣的用戶 ID 插入到 t_user_system_notice 中,這個數據量就須要根據平臺的用戶量來計算。

舉個例子: 管理員 A 發佈了一個活動的通知,他須要將這個通知發佈給全體用戶,當拉取時間到來時,系統會將這一條通知取出。隨後系統到用戶表中查詢選取全部用戶的 ID,而後將這一條通知的信息根據全部用戶的 ID,批量插入 t_user_system_notice 中。用戶須要查看系統通知時,從 t_user_system_notice 表中查詢就好了。

注意:

  1. 由於一次拉取的數據量可能很大,因此兩次拉取的時間間隔能夠設置的長一些。
  2. 拉取 t_manager_system_notice 表中的通知時,須要判斷 state,若是已經拉取過,就不須要重複拉取, 不然會形成重複消費。
  3. 當一條通知須要發佈給全體用戶時,咱們應該考慮到用戶的活躍度。由於若是有些用戶長期不活躍, 咱們還將通知推送給他(她),這顯然會形成空間的浪費。 因此在選取用戶 ID 時,咱們能夠將用戶上次 登陸的時間與推送時間作一個比較,若是用戶一年未登錄或幾個月未登陸,咱們就不選取其 ID,進而避免 無謂的推送。
  4. 有的小夥伴可能有疑問: 某條通知已經被拉取過的話,在其後註冊的用戶是否是不能再接收到這條通知? 是的。但若是你想將已拉取過的通知推送給那些後註冊的用戶,也不是特別大的問題。 只須要再寫一個定時任務,這個 定時任務能夠將通知的 push_time 與用戶的註冊時間比較一下,從新推送便可。

以上就是系統通知的設計了,接下來再看看較難的提醒類型的消息。

事件提醒(EventRemind)

之因此稱提醒類型的消息爲事件提醒,是由於此類消息均是經過用戶的行爲產生的,以下:

  • xxx 在某個評論中@了你;
  • xxx 點讚了你的文章;
  • xxx 點讚了你的評論;
  • xxx 回覆了你的文章;
  • xxx 回覆了你的評論。

諸如此類事件,咱們以單詞 action 形容不一樣的事件(點贊,回覆,at)。 能夠看到除了事件以外,咱們還須要瞭解用戶是在哪一個地方產生的事件,以便當咱們收到提醒時, 點擊這條消息就能夠去到事件現場,從而加強用戶體驗,我以事件源 source 來形容事件發生的地方。

  • 當 action 爲點贊,source 爲文章時,我就知道:有用戶點讚了個人某篇文章;
  • 當 action 爲點贊,source 爲評論時,我就知道:有用戶點讚了個人某條評論;
  • 當 action 爲@(at), source 爲評論時,我就知道:有用戶在某條評論裏@了我;
  • 當 action 爲回覆,source 爲文章時,我就知道:有用戶回覆了個人某篇文章;
  • 當 action 爲回覆,source 爲評論時,我就知道:有用戶回覆了個人某條評論;

由此能夠設計出事件提醒表 t_event_remind,其結構以下:

字段名 類型 描述
event_remind_id LONG 消息 ID
action VARCHAR 動做類型,如點贊、at(@)、回覆等
source_id LONG 事件源 ID,如評論 ID、文章 ID 等
source_type VARCHAR 事件源類型:"Comment"、"Post"等
source_content VARCHAR 事件源的內容,好比回覆的內容,回覆的評論等等
url VARCHAR 事件所發生的地點連接 url
state BOOLEAN 是否已讀
sender_id LONG 操做者的 ID,即誰關注了你,at 了你
recipient_id LONG 接受通知的用戶的 ID
remind_time TIMESTAMP 提醒的時間

消息聚合

消息聚合只適用於事件提醒,以聚合以後的點贊消息來講:

  • 100 人 {點贊} 了你的 {文章 ID = 1} :《A》;
  • 100 人 {點贊} 了你的 {文章 ID = 2} :《B》;
  • 100 人 {點贊} 了你的 {評論 ID = 3} :《C》;

聚合以後的消息明顯有兩個特徵,即:action 和 source type,這是系統消息和私信都不具有的, 因此我我的認爲事件提醒的設計要稍微比系統消息和私信複雜。

如何聚合?

稍稍觀察下聚合的消息就能夠發現:某一類的聚合消息之間是按照 source type 和 source id 來分組的, 所以咱們能夠得出如下僞 SQL:

SELECT * FROM t_event_remind WHERE recipient_id = 用戶ID
AND action = 點贊 AND state = FALSE GROUP BY source_id , source_type;

固然,SQL 層面的結果集處理仍是很麻煩的,因此個人想法先把用戶全部的點贊消息先查出來, 而後在程序裏面進行分組,這樣會簡單很多。

拓展

其實還有一種設計提醒表的作法,即按業務分類,不一樣的提醒存入不一樣的表,這樣能夠分爲:

  1. 點贊提醒表
  2. 回覆提醒表
  3. at(@)提醒表。

我認爲這種設計比第一種的更鬆耦合,沒必要全部類型的提醒都擠在一張表裏,可是這也會帶來表數量的膨脹。 因此各位小夥伴能夠自行選擇方案。

私信

站內私信通常都是點到點的,且要求是實時的,服務端能夠採用 Netty 等高性能網絡通訊框架完成請求。 咱們仍是以 b 站爲例,看看它是怎麼設計的:

站內消息系統的設計

b 站的私信部分能夠分爲兩部分:

  1. 左邊的與不一樣用戶的聊天室;
  2. 與當前正在對話的用戶的對話框,顯示了當前用戶與目標用戶的全部消息。

按照這個設計,咱們能夠先設計出聊天室表 t_private_chat,由於是一對一,因此聊天室表會包含對話的兩個用戶的信息:

字段名 類型 描述
private_chat_id LONG 聊天室 ID
user1_id LONG 用戶 1 的 ID
user2_id LONG 用戶 2 的 ID
last_message VARCHAR 最後一條消息的內容

這裏 user1_id 和 user2_id 表明兩個用戶的 ID,並沒有特定的前後順序。

接下來是私信表 t_private_message 了,私信天然和所屬的聊天室有聯繫,且考慮到私信能夠在記錄中刪除(刪除了只是不顯示記錄,可是對方會有記錄,撤回纔是真正的刪除),就還須要記錄私信的狀態,如下是個人設計:

字段名 類型 描述
private_message_id LONG 私信 ID
content TEXT 私信內容
state BOOLEAN 是否已讀
sender_remove BOOLEAN 發送消息的人是否把這條消息從聊天記錄中刪除了
recipient_remove BOOLEAN 接受人是否把這條消息從聊天記錄刪除了
sender_id LONG 發送者 ID
recipient_id LONG 接受者 ID
send_time TIMESTAMP 發送時間

消息設置

消息設置通常都是針對提醒類型的消息的,且確定是由用戶本身設置的。因此我想到通常有如下設置選項:

  1. 是否開啓點贊提醒;
  2. 是否開啓回覆提醒;
  3. 是否開啓@提醒;

下面是 b 站的消息設置:

消息設置

能夠看到 b 站還添加了陌生人選項,也就是說若是給你發送私信的用戶不是你關注的用戶,那麼視之爲陌生人私信,就不接受。

如下是我對於消息設置的設計:

字段名 類型 描述
user_id LONG 用戶 ID
like_message BOOLEAN 是否接收點贊消息
reply_message BOOLEAN 是否接收回復消息
at_message BOOLEAN 是否接收 at 消息
stranger_message BOOLEAN 是否接收陌生人的私信

總結

以上就是我對於整個站內消息系統的大概設計了,我參考了不少文章的內容以及不少網站的設計,但實際項目的需求確定與我所介紹的有不少出入,因此各位小夥伴能夠酌情參考。

最後

文章有幫助能夠點個「在看」或「分享」,都是支持,我都喜歡!

我是 Guide 哥,Java後端開發,會一點前端知識,喜歡烹飪,自由的少年。一個三觀比主角還正的技術人。咱們下期再見!


往期推薦



來吧!手寫一個 RPC 框架。畢設/項目經驗穩了!

我在華爲外包一年的經歷分享。

我還在生產玩 JDK7,JDK 15 卻要來了!|新特性嚐鮮

朋友入職中軟一個月(外包華爲)就離職了!

線上頻出MySQL死鎖問題!分享一下本身教科書般的排查和分析過程!

6 個珍藏已久 IDEA 小技巧,這一波所有分享給你!


本文分享自微信公衆號 - JavaGuide(JavaGuide)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索