聲明:本文來自于微信公眾號 JavaGuide (ID: JavaGuide ),作者:guang19,授權轉載發布。
使用過簡書,知乎或 b 站的小伙伴應該都有這樣的使用體驗:當有其他用戶關注我們或者私信我們的行為時,我們會收到相關的消息。 雖然這些功能看上去簡單,但其背后的設計是非常復雜的,幾乎是一個完成的系統,可以稱之為 站內消息系統。
我以 b 站舉例(個人認為 b 站的消息系統是我見過的非常完美的,UI 也最為人性化的):
可以看到 b 站把消息大致分為了三類:
系統推送的通知(System Notice);
回復、@、點贊等用戶行為產生的提醒(Remind);
用戶之間的私信(Chat)。
這樣設計不僅分類明確,且處于同一個主體的事件提醒還會做一個聚合,極大的提高了用戶體驗,不讓用戶收到太多分散的消息。
舉個例子:比如你在某個視頻或某篇文章下發表了評論,有 100 個人給你的評論點了贊,那么你希望消息頁面呈現的是一個一個用戶給你點贊的提醒,還是像以下聚合之后的提醒:
我相信你大概率會選擇后者。
我認為對于很多應用來說,這樣的設計都是非常合理的,接下來我寫寫我對于消息系統的設計。
系統通知(System Notice)
系統通知一般是由后臺管理員發出,然后指定某一類(全體,個人等)用戶接收。基于此設想,可以把系統通知大致分為兩張表:
t_manager_system_notice(管理員系統通知表) :記錄管理員發出的通知 ;
t_user_system_notice(用戶系統通知表) : 存儲用戶接受的通知。
t_manager_system_notice 結構如下:
字段名 | 類型 | 描述 |
---|---|---|
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
當然,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 | 是否接收陌生人的私信 |
總結
以上就是我對于整個站內消息系統的大概設計了,我參考了很多文章的內容以及很多網站的設計,但實際項目的需求肯定與我所介紹的有很多出入,所以各位小伙伴可以酌情參考。