本文來自 guang19 投稿(Github 同名,歡迎關注)。前端
使用過簡書,知乎或 b 站的小夥伴應該都有這樣的使用體驗:當有其餘用戶關注咱們或者私信咱們的行爲時,咱們會收到相關的消息。 雖然這些功能看上去簡單,但其背後的設計是很是複雜的,幾乎是一個完成的系統,能夠稱之爲 站內消息系統。web
我以 b 站舉例(我的認爲 b 站的消息系統是我見過的很是完美的,UI 也最爲人性化的):後端
能夠看到 b 站把消息大體分爲了三類:微信
-
系統推送的通知(System Notice); -
回覆、@、點贊等用戶行爲產生的提醒(Remind); -
用戶之間的私信(Chat)。
這樣設計不只分類明確,且處於同一個主體的事件提醒還會作一個聚合,極大的提升了用戶體驗,不讓用戶收到太多分散的消息。網絡
舉個例子:好比你在某個視頻或某篇文章下發表了評論,有 100 我的給你的評論點了贊,那麼你但願消息頁面呈現的是一個一個用戶給你點讚的提醒,仍是像如下聚合以後的提醒:框架
我相信你大機率會選擇後者。編輯器
我認爲對於不少應用來講,這樣的設計都是很是合理的,接下來我寫寫我對於消息系統的設計。ide
系統通知(System Notice)
系統通知通常是由後臺管理員發出,而後指定某一類(全體,我的等)用戶接收。基於此設想,能夠把系統通知大體分爲兩張表:性能
-
t_manager_system_notice(管理員系統通知表) :記錄管理員發出的通知 ; -
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 表中查詢就好了。
注意:
-
由於一次拉取的數據量可能很大,因此兩次拉取的時間間隔能夠設置的長一些。 -
拉取 t_manager_system_notice 表中的通知時,須要判斷 state,若是已經拉取過,就不須要重複拉取, 不然會形成重複消費。 -
當一條通知須要發佈給全體用戶時,咱們應該考慮到用戶的活躍度。由於若是有些用戶長期不活躍, 咱們還將通知推送給他(她),這顯然會形成空間的浪費。 因此在選取用戶 ID 時,咱們能夠將用戶上次 登陸的時間與推送時間作一個比較,若是用戶一年未登錄或幾個月未登陸,咱們就不選取其 ID,進而避免 無謂的推送。 -
有的小夥伴可能有疑問: 某條通知已經被拉取過的話,在其後註冊的用戶是否是不能再接收到這條通知? 是的。但若是你想將已拉取過的通知推送給那些後註冊的用戶,也不是特別大的問題。 只須要再寫一個定時任務,這個 定時任務能夠將通知的 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 層面的結果集處理仍是很麻煩的,因此個人想法先把用戶全部的點贊消息先查出來, 而後在程序裏面進行分組,這樣會簡單很多。
拓展
其實還有一種設計提醒表的作法,即按業務分類,不一樣的提醒存入不一樣的表,這樣能夠分爲:
-
點贊提醒表 -
回覆提醒表 -
at(@)提醒表。
我認爲這種設計比第一種的更鬆耦合,沒必要全部類型的提醒都擠在一張表裏,可是這也會帶來表數量的膨脹。 因此各位小夥伴能夠自行選擇方案。
私信
站內私信通常都是點到點的,且要求是實時的,服務端能夠採用 Netty 等高性能網絡通訊框架完成請求。 咱們仍是以 b 站爲例,看看它是怎麼設計的:
b 站的私信部分能夠分爲兩部分:
-
左邊的與不一樣用戶的聊天室; -
與當前正在對話的用戶的對話框,顯示了當前用戶與目標用戶的全部消息。
按照這個設計,咱們能夠先設計出聊天室表 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 | 發送時間 |
消息設置
消息設置通常都是針對提醒類型的消息的,且確定是由用戶本身設置的。因此我想到通常有如下設置選項:
-
是否開啓點贊提醒; -
是否開啓回覆提醒; -
是否開啓@提醒;
下面是 b 站的消息設置:
能夠看到 b 站還添加了陌生人選項,也就是說若是給你發送私信的用戶不是你關注的用戶,那麼視之爲陌生人私信,就不接受。
如下是我對於消息設置的設計:
字段名 | 類型 | 描述 |
---|---|---|
user_id | LONG | 用戶 ID |
like_message | BOOLEAN | 是否接收點贊消息 |
reply_message | BOOLEAN | 是否接收回復消息 |
at_message | BOOLEAN | 是否接收 at 消息 |
stranger_message | BOOLEAN | 是否接收陌生人的私信 |
總結
以上就是我對於整個站內消息系統的大概設計了,我參考了不少文章的內容以及不少網站的設計,但實際項目的需求確定與我所介紹的有不少出入,因此各位小夥伴能夠酌情參考。
最後
文章有幫助能夠點個「在看」或「分享」,都是支持,我都喜歡!
我是 Guide 哥,Java後端開發,會一點前端知識,喜歡烹飪,自由的少年。一個三觀比主角還正的技術人。咱們下期再見!
往期推薦
本文分享自微信公衆號 - JavaGuide(JavaGuide)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。