本篇主要明確消息通知系統的概念和具體實現,包括數據庫設計、技術方案、邏輯關系分析等。消息通知系統是一個比較復雜的系統,這里主要分析站內消息如何設計和實現。
我們常見的消息推送渠道有以下幾種:
- 設備推送
- 站內推送
- 短信推送
- 郵箱推送
我們常見的站內通知有以下幾種類別:
- 公告 Announcement
-
提醒 Remind
- 資源訂閱提醒「我關注的資源有更新、評論等事件時通知我」
- 資源發布提醒「我發布的資源有評論、收藏等事件時通知我」
- 系統提醒「平台會根據一些算法、規則等可能會對你的資源做一些事情,這時你會收到系統通知」
- 私信 Mailbox
以上三種消息有各自特點,實現也各不相同,其中「提醒」類通知是最復雜的,下面會詳細講。
數據模型設計
公告
公告是指平台發送一條含有具體內容的消息,站內所有用戶都能收到這條消息。
方案一:【適合活躍用戶在5萬左右】
公告表「notify_announce」
表結構如下:
-
id: { type: 'integer', primaryKey: true, autoIncrement:true} //公告編號;
-
senderID: { type: 'string', required: true} //發送者編號,通常為系統管理員;
-
title: { type: 'string', required: true} //公告標題;
-
content: { type: ’text', required: true} //公告內容;
-
createdAt: {type: 'timestamp', required: true} //發送時間;
用戶公告表「notify_announce_user」
表結構如下:
-
id: { type: 'integer', primaryKey: true, autoIncrement:true} //用戶公告編號;
-
announceID: { type: 'integer'} //公告編號;
-
recipientID: { type: 'string', required: true} //接收用戶編號;
-
createdAt:{ type: 'timestamp', required: true} //拉取公告時間;
-
state: { type: 'integer', required: true} //狀態,已讀|未讀;
-
readAt:{ type: 'timestamp', required: true} //閱讀時間;
平台發布一則公告之后,當用戶登錄的時候去拉取站內公告並插入notify_announce_user表,這樣那些很久都沒登陸的用戶就沒必要插入了。「首次拉取,根據用戶的注冊時間;否則根據notify_announce_user.createdAt即上一次拉取的時間節點獲取公告」
方案二:【適合活躍用戶在百萬-千萬左右】
和方案一雷同,只是需要把notify_announce_user表進行哈希分表,需事先生成表:notify_announce_<hash(uid)>。
用戶公告表「notify_announce_<hash(uid)>」
表結構如下:
-
id: { type: 'integer', primaryKey: true, autoIncrement:true} //用戶公告編號;
-
announceID: { type: 'integer'} //公告編號;
-
recipientID: { type: 'string', required: true} //接收用戶編號;
-
createdAt:{ type: 'timestamp', required: true} //拉取公告時間;
-
state: { type: 'integer', required: true} //狀態,已讀|未讀;
-
readAt:{ type: 'timestamp', required: true} //閱讀時間;
提醒
提醒是指「我的資源」或「我關注的資源」有新的動態產生時通知我。提醒的內容無非就是:
「someone do something in someone's something」
「誰對一樣屬於誰的事物做了什么操作」
常見的提醒消息例子,如:
XXX 關注了你 - 「這則屬於資源發布提醒」
XXX 喜歡了你的文章 《消息通知系統模型設計》 - 「這則屬於資源發布提醒」
你喜歡的文章《消息通知系統模型設計》有新的評論 - 「這則屬於資源訂閱提醒」
你的文章《消息通知系統模型設計》已被加入專題 《系統設計》 - 「這則屬於系統提醒」
小明贊同了你的回答 XXXXXXXXX -「這則屬於資源發布提醒」
最后一個例子中包含了消息的生產者(小明),消息記錄的行為(贊同),行為的對象(你的回答內容)
分析提醒類消息的句子結構:
someone = 動作發起者,標記為「sender」
do something = 對資源的操作,如:評論、喜歡、關注都屬於一個動作,標記為「action」
something = 被作用對象,如:一篇文章,文章的評論等,標記為「object」
someone's = 動作的目標對象或目標資源的所有者,標記為「objectOwner」
總結:sender 和 objectOwner 就是網站的用戶,object 就是網站資源,可能是一篇文章,一條文章的評論等等。action 就是動作,可以是贊、評論、收藏、關注、捐款等等。
提醒設置
提醒通常是可以在「設置-通知」里自定義配置的,用戶可以選擇性地「訂閱」接收和不接收某類通知。
呈現在界面上是這樣的:
-
通知設置
-
-
我發布的 publish
-
文章
-
被 評論 是/否 通知我
-
被 收藏 是/否 通知我
-
被 點贊 是/否 通知我
-
被 喜歡 是/否 通知我
-
被 捐款 是/否 通知我
-
-
我訂閱的 follow
-
文章
-
有 更新 是/否 通知我
-
被 評論 是/否 通知我
訂閱
一般系統默認是訂閱了所有通知的。系統在給用戶推送消息的時候必須查詢通知「訂閱」模塊,以獲取某一事件提醒消息應該推送到哪些用戶。
也就是說「事件」和「用戶」之間有一個訂閱關系。
那么接下來我們分析下「訂閱」有哪些關鍵元素:
比如我發布了一篇文章,那么我會訂閱文章《XXX》的評論動作,所以文章《XXX》每被人評論了,就需要發送一則提醒告知我。
分析得出以下關鍵元素:
- 訂閱者「subscriber」
- 訂閱的對象「object」
- 訂閱的動作「action」
- 訂閱對象和訂閱者的關系「objectRelationship」
什么是訂閱的目標關系呢?
拿知乎來說,比如我喜歡了一篇文章,我希望我訂閱這篇文章的更新、評論動作。那這篇文章和我什么關系?不是所屬關系,只是喜歡。
- objectRelationship = 我發布的,對應着 actions = [評論,收藏]
- objectRelationship = 我喜歡的,對應着 actions = [更新,評論]
講了那么多,現在來構建「提醒」的數據結構該吧!
提醒表「notify_remind」
表結構如下:
-
id: { type: 'integer', primaryKey: true, autoIncrement:true} //主鍵;
-
remindID: { type: 'string', required: true} //通知提醒編號;
-
senderID: { type: 'string', required: true} //操作者的ID,三個0代表是系統發送的;
-
senderName: { type: 'string’, required: true} //操作者用戶名;
-
senderAction: {type: 'string', required: true} //操作者的動作,如:贊了、評論了、喜歡了、捐款了、收藏了;
-
objectID: {type: 'string', required: true}, //目標對象ID;
-
object: {type: 'string', required: false}, //目標對象內容或簡介,比如:文章標題;
-
objectType: {type: 'string', required: true} //被操作對象類型,如:人、文章、活動、視頻等;
-
recipientID: {type: 'string’} //消息接收者;可能是對象的所有者或訂閱者;
-
message: { type: 'text', required: true} //消息內容,由提醒模版生成,需要提前定義;
-
createdAt:{ type: 'timestamp', required: true} //創建時間;
-
status:{ type: 'integer', required: false} //是否閱讀,默認未讀;
-
readAt:{ type: 'timestamp', required: false} //閱讀時間;
假如:特朗普關注了金正恩,以下字段的值是這樣的
-
senderID = 特朗普的ID
-
senderName = 特朗普
-
senderAction = 關注
-
objectID = 金正恩的ID
-
object = 金正恩
-
objectType = 人
-
recipientID = 金正恩的ID
-
message = 特朗普關注了金正恩
-
-
這種情況objectID 和 recipientID是一樣的。
這里需要特別說下消息模版,模版由「對象」、「動作」和「對象關系」構成唯一性。
通知提醒訂閱表「notify_remind_subscribe」
表結構如下:
-
id: { type: 'integer', primaryKey: true, autoIncrement:true} //訂閱ID;
-
userID: { type: 'string', required: true},//用戶ID,對應 notify_remind 中的 recipientID;
-
objectType: { type: 'string', required: true} //資源對象類型,如:文章、評論、視頻、活動、用戶;
-
action: { type: 'string', required: true} //資源訂閱動作,多個動作逗號分隔如: comment,like,post,update etc.
-
objectRelationship: { type: 'string', required: true} //用戶與資源的關系,用戶發布的published,用戶關注的followed;
-
createdAt:{ type: 'timestamp', required: true} //創建時間;
特別說下「objectRelationship」字段的作用,這個字段用來區分「消息模版」,為什么呢?因為同一個「資源對象」和「動作」會有兩類訂閱者,一類是該資源的Owner,另一類是該資源的Subscriber,這兩類人收到的通知消息內容應該是不一樣的。
聚合
假如我在抖音上發布了一個短視頻,在我不在線的時候,被評論了1000遍,當我一上線的時候,應該是收到一千條消息,類似於:「* 評論了你的文章《XXX》」? 還是應該收到一條信息:「有1000個人評論了你的文章《XXX》」?
當然是后者更好些,要盡可能少的騷擾用戶。
消息推送
是不是感覺有點暈了,還是先上一張消息通知的推送流程圖吧:
訂閱表一共有兩張噢,一張是「通知訂閱表」、另一張是用戶對資源的「對象訂閱表」。
具體實現就不多講了,配合這張圖,理解上面講的應該不會有問題了。
私信
通常私信有這么幾種需求:
- 點到點:用戶發給用戶的站內信,系統發給用戶的站內信。「1:1」
- 點到多:系統發給多個用戶的站內信,接收對象較少,而且接收對象無特殊共性。「1:N」
- 點到面:系統發給用戶組的站內信,接收對象同屬於某用戶組之類的共同屬性。「1:N」
- 點到全部:系統發給全站用戶的站內信,接收對象為全部用戶,通常為系統通知。「1:N」
這里主要講「點到點」的站內信。
私信表「notify_mailbox」
表結構如下:
-
id: { type: 'integer', primaryKey: true, autoIncrement:true} //編號;
-
dialogueID: { type: 'string', required: true} //對話編號;
-
senderID: { type: 'string', required: true} //發送者編號;
-
recipientID: { type: 'string', required: true} //接收者編號;
-
messageID: { type: 'integer', required: true} //私信內容ID;
-
createdAt:{ type: 'timestamp', required: true} //發送時間;
-
state: { type: 'integer', required: true} //狀態,已讀|未讀;
-
readAt:{ type: 'timestamp', required: true} //閱讀時間;
Inbox
-
私信列表
-
select * from notify_inbox where recipientID="uid" order by createdAt desc
-
-
對話列表
-
select * from notify_inbox where dialogueID=“XXXXXXXXXXXX” and (recipientID=“uid” or senderID="uid") order by createdAt asc
私信回復時,回復的是dialogueID
Outbox
-
私信列表
-
select * from notify_inbox where senderID="uid" order by createdAt desc
-
-
對話列表
-
select * from notify_inbox where dialogueID=“XXXXXXXXXXXX” and (senderID=“uid” or recipientID="uid") order by createdAt asc
私信內容表「notify_inbox_message」
表結構如下:
-
id: { type: 'integer', primaryKey: true, autoIncrement:true} //編號;
-
senderID: { type: 'string', required: true} //發送者編號;
-
content: { type: 'string', required: true} //私信內容;
-
createdAt:{ type: 'timestamp', required: true}