文/JC_Huang(簡書作者)原文鏈接:http://www.jianshu.com/p/f4d7827821f1著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。 產品分析 首先我們來看一下市場上關於消息的實現是怎麼樣的。 簡書 簡書的消息系統主要分了兩種 簡信 提醒 簡信簡信的性質 ...
文/JC_Huang(簡書作者)
原文鏈接:http://www.jianshu.com/p/f4d7827821f1
著作權歸作者所有,轉載請聯繫作者獲得授權,並標註“簡書作者”。
產品分析
首先我們來看一下市場上關於消息的實現是怎麼樣的。
簡書
簡書的消息系統主要分了兩種
- 簡信
- 提醒
簡信
簡信的性質其實跟私信是一樣的,是用戶發送給用戶的一則消息,有具體的信息內容。
![](http://upload-images.jianshu.io/upload_images/79702-1c35256f22f530e4.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
簡書簡信
提醒
而提醒,則是系統發送的一則消息,其文案格式是固定的,並且對特殊對象一般擁有超鏈接。
![](http://upload-images.jianshu.io/upload_images/79702-d8e9bcfcbde089ec.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
簡書提醒
知乎
知乎跟簡書一樣,主要分了兩種:
- 私信
- 消息
私信
跟簡書一樣,使用戶發送給用戶的一則消息,也可以是管理員發送給用戶的消息。
![](http://upload-images.jianshu.io/upload_images/79702-4c98190143481f7a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
知乎私信
消息
知乎的消息比簡書的提醒有過之而無不及,知乎會對多條相似的消息進行聚會,以達到減輕用戶閱讀壓力的體驗。
![](http://upload-images.jianshu.io/upload_images/79702-9c189d0ea208b71d.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
知乎消息
消息的三種分類
通過兩種產品的簡單分析,得出他們的消息有兩種分類,在這基礎上,我們再加上一種:公告。
公告的主要性質是系統發送一則含有具體內容的消息,站內所有用戶都能讀取到這條消息。
所以,消息有三種分類:
- 公告 Announce
- 提醒 Remind
- 私信 Message
提醒的語言分析
我們從簡書取一組提醒樣本:
- 3dbe1bd90774 關註了你
- magicdawn 喜歡了你的文章 《單點登錄的三種實現方式》
- 無良程式 喜歡了你的文章 《基於RESTful API 怎麼設計用戶許可權控制?》
- alexcc4 喜歡了你的文章 《在Nodejs中貫徹單元測試》
- 你在《基於RESTful API 怎麼設計用戶許可權控制?》中收到一條 cnlinjie 的評論
- 你的文章《Session原理》已被加入專題 《ios開發》
分析句子結構,提醒的內容無非就是
「誰對一樣屬於誰的事物做了什麼操作」
「someone do something in someone's something」
someone = 提醒的觸發者,或者發送者,標記為sender
do something = 提醒的動作,評論、喜歡、關註都屬於一個動作,標記為action
something = 提醒的動作作用對象,這就具體到是哪一篇文章,標記為target
someone's = 提醒的動作作用對象的所有者,標記為targetOwner
這就清楚了,sender和targetOwner就是網站的用戶,而target是具體到哪一篇文章,如果提醒的對象不僅僅局限於文章,還有其他的話,就需要增加一項targetType,來標記目標是文章還是其他的什麼。而action,則是固定的,整個網站會觸發提醒的動作可能就只有那幾樣:評論、喜歡、關註.....(或者其他業務需要提醒的動作)
消息的兩種獲取方式
- 推 Push
- 拉 Pull
以知乎為例
推的比較常見,需要針對某一個問題維護著一張關註者的列表,每當觸發這個問題推送的條件時(例如有人回答問題),就把這個通知發送給每個關註者。
拉的相對麻煩一點,就是推的反向,例如每個用戶都有一張關註問題的列表,每當用戶上線的時候,對每個問題進行輪詢,當問題的事件列表出現了比我原本時間戳大的信息就進行拉取。
而我們則根據消息的不同分類採用不同的獲取方式:
通告和提醒,適合使用拉取的方式,消息產生之後,會存在消息表中,用戶在某一特定的時間根據自己關註問題的表進行消息的拉取,然後添加到自己的消息隊列中,
信息,適合使用推的方式,在發送者建立一條信息之後,同時指定接收者,把消息添加到接收者的消息隊列中。
訂閱
根據提醒使用拉取的方式,需要維護一個關註某一事物的列表。
這種行為,我們稱之為:「訂閱」Subscribe
一則訂閱有以下三個核心屬性:
- 訂閱的目標 target
- 訂閱的目標類型 targetType
- 訂閱的動作 action
比如我發佈了一篇文章,那麼我會訂閱文章《XXX》的評論動作,所以文章《XXX》每被人評論了,就需要發送一則提醒告知我。
訂閱的規則還可以擴展
我喜歡了一篇文章,和我發佈了一篇文章,訂閱的動作可能不一樣。
喜歡了一篇文章,我希望我訂閱這篇文章更新、評論的動作。
而發佈了一篇文章,我希望我只是訂閱這篇文章的評論動作。
這時候就需要多一個參數:subscribReason
不同的subscribReason,對應著一個動作數組,
subscribReason = 喜歡,對應著 actions = [更新,評論]
subscribReason = 發佈,對應著 actions = [評論]
訂閱的規則還還可以擴展
用戶可能會有一個自己的訂閱設置,比如對於所有的喜歡的動作,我都不希望接收。
比如Knewone的提醒設置
![](http://upload-images.jianshu.io/upload_images/79702-aa831d93990cff2e.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
Knewone提醒設置
所以我們需要再維護一個表:SubscriptionConfig,來存放用戶的提醒設置。
並且,當用戶沒有提醒設置的時候,可以使用系統提供的一套預設設置:defaultSubscriptionConfig
聚合
如果我發佈了一篇文章《XXX》,在我不線上的時候,被評論了10遍,當我一上線的時候,應該是收到十條信息類似於:「誰誰誰評論了你的文章《XXX》」?
還是應該收到一條信息:「甲、乙、丙、丁...評論了你的文章《XXX》」?
知乎在聚合上做的很優秀,要知道他們要實現這個還是挺有技術的:
知乎的消息機制,在技術上如何設計與規劃?
網站的消息(通知)系統一般是如何實現的?
關於這部分功能,我們還沒有具體的實現方法,暫時也無法講得更加詳細。⊙﹏⊙
五個實體
通過上面的分析,大概知道做這個消息系統,需要哪些實體類:
- 用戶消息隊列 UserNotify
- 用戶 User
- 訂閱 Subscription
- 訂閱設置 SubscriptionConfig
- 消息 Notify
- 通告 Announce
- 提醒 Remind
- 信息 Message
行為分解
說了這麼多,整理一下整個消息流程的一些行為:
- 系統或者管理員,創建消息
- createNotify (make announce | remind | message)
- 用戶,訂閱消息,取消訂閱
- subscribe, cancelSubscription
- 用戶管理訂閱設置
- getSubscriptionConfig, updateSubscriptionConfig
- 用戶,拉取消息
- pullNotify (pull announce | remind | message | all)
- 用戶,查詢消息隊列
- getUserNotify(get announce | remind | message | all)
- 用戶閱讀消息
- read
模型設計
Notify
id : {type: 'integer', primaryKey: true}, // 主鍵
content : {type: 'text'}, // 消息的內容
type : {type: 'integer', required: true, enum: [1, 2, 3]}, // 消息的類型,1: 公告 Announce,2: 提醒 Remind,3:信息 Message
target : {type: 'integer'}, // 目標的ID
targetType : {type: 'string'}, // 目標的類型
action : {type: 'string'}, // 提醒信息的動作類型
sender : {type: 'integer'}, // 發送者的ID
createdAt : {type: 'datetime', required: true}
Save Remind
消息表,我們需要target
、targetType
欄位,來記錄該條提醒所關聯的對象。而action
欄位,則記錄該條提醒所關聯的動作。
比如消息:「小明喜歡了文章」
則:
target = 123, // 文章ID
targetType = 'post', // 指明target所屬類型是文章
sender = 123456 // 小明ID
Save Announce and Message
當然,Notify還支持存儲公告和信息。它們會用到content
欄位,而不會用到target
、targetType
、action
欄位。
UserNotify
id : {type: 'integer', primaryKey: true}, // 主鍵
isRead : {type: 'boolean', required: true},
user : {type: 'integer', required: true}, // 用戶消息所屬者
notify : {type: 'integer', required: true} // 關聯的Notify
createdAt : {type: 'datetime', required: true}
我們用UserNotify來存儲用戶的消息隊列,它關聯一則提醒(Notify)的具體內容。
UserNotify的創建,主要通過兩個途徑:
- 遍歷訂閱(Subscription)表拉取公告(Announce)和提醒(Remind)的時候創建
- 新建信息(Message)之後,立刻創建。
Subscription
target : {type: 'integer', required: true}, // 目標的ID
targetType : {type: 'string', required: true}, // 目標的類型
action : {type: 'string'}, // 訂閱動作,如: comment/like/post/update etc.
user : {type: 'integer'},
createdAt : {type: 'datetime', required: true}
訂閱,是從Notify表拉取消息到UserNotify的前提,用戶首先訂閱了某一個目標的某一個動作,在此之後產生這個目標的這個動作的消息,才會被通知到該用戶。
如:「小明關註了產品A的評論」,數據表現為:
target: 123, // 產品A的ID
targetType: 'product',
action: 'comment',
user: 123 // 小明的ID
這樣,產品A下產生的每一條評論,都會產生通知給小明瞭。
SubscriptionConfig
action: {type: 'json', required: true}, // 用戶的設置
user: {type: 'integer'}
不同用戶可能會有不一樣的訂閱習慣,在這個表中,用戶可以統一針對某種動作進行是否訂閱的設置。而預設是使用系統提供的預設配置:
defaultSubscriptionConfig: {
'comment' : true, // 評論
'like' : true, // 喜歡
}
在這套模型中,
targetType
、action
是可以根據需求來擴展的,例如我們還可以增加多幾個動作的提醒:hate
被踩、update
被更新....諸如此類。
配置文件 NotifyConfig
// 提醒關聯的目標類型
targetType: {
PRODUCT : 'product', // 產品
POST : 'post' // 文章
},
// 提醒關聯的動作
action: {
COMMENT : 'comment', // 評論
LIKE : 'like', // 喜歡
},
// 訂閱原因對應訂閱事件
reasonAction: {
'create_product' : ['comment', 'like']
'like_product' : ['comment'],
'like_post' : ['comment'],
},
// 預設訂閱配置
defaultSubscriptionConfig: {
'comment' : true, // 評論
'like' : true, // 喜歡
}
服務層 NotifyService
NotifyService擁有以下方法:
- createAnnounce(content, sender)
- createRemind(target, targetType, action, sender, content)
- createMessage(content, sender, receiver)
- pullAnnounce(user)
- pullRemind(user)
- subscribe(user, target, targetType, reason)
- cancelSubscription(user, target ,targetType)
- getSubscriptionConfig(userID)
- updateSubscriptionConfig(userID)
- getUserNotify(userID)
- read(user, notifyIDs)
各方法的處理邏輯如下:
createAnnounce(content, sender)
- 往Notify表中插入一條公告記錄
createRemind(target, targetType, action, sender, content)
- 往Notify表中插入一條提醒記錄
createMessage(content, sender, receiver)
- 往Notify表中插入一條信息記錄
- 往UserNotify表中插入一條記錄,並關聯新建的Notify
pullAnnounce(user)
- 從UserNotify中獲取最近的一條公告信息的創建時間:
lastTime
- 用
lastTime
作為過濾條件,查詢Notify的公告信息 - 新建UserNotify並關聯查詢出來的公告信息
pullRemind(user)
- 查詢用戶的訂閱表,得到用戶的一系列訂閱記錄
- 通過每一條的訂閱記錄的
target
、targetType
、action
、createdAt
去查詢Notify表,獲取訂閱的Notify記錄。(註意訂閱時間必須早於提醒創建時間) - 查詢用戶的配置文件SubscriptionConfig,如果沒有則使用預設的配置DefaultSubscriptionConfig
- 使用訂閱配置,過濾查詢出來的Notify
- 使用過濾好的Notify作為關聯新建UserNotify
subscribe(user, target, targetType, reason)
- 通過reason,查詢NotifyConfig,獲取對應的動作組:
actions
- 遍歷動作組,每一個動作新建一則Subscription記錄
cancelSubscription(user, target ,targetType)
- 刪除
user
、target
、targetType
對應的一則或多則記錄
getSubscriptionConfig(userID)
- 查詢SubscriptionConfig表,獲取用戶的訂閱配置
updateSubscriptionConfig(userID)
- 更新用戶的SubscriptionConfig記錄
getUserNotify(userID)
- 獲取用戶的消息列表
read(user, notifyIDs)
- 更新指定的notify,把isRead屬性設置為true
時序圖
提醒的訂閱、創建、拉取
![](http://upload-images.jianshu.io/upload_images/79702-61d218212571d556.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
提醒的訂閱、創建、拉取
我們可以在產品創建之後,調用NotifyService.subscribe
方法,
然後在產品被評論之後調用NotifyService.createRemind
方法,
再就是用戶登錄系統或者其他的某一個時刻調用NotifyService.pullRemind
方法,
最後在用戶查詢消息隊列的時候調用NotifyService.getUserNotify
方法。
公告的創建、拉取
![](http://upload-images.jianshu.io/upload_images/79702-122a2d09bd4ef238.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
公告的創建、拉取
在管理員發送了一則公告的時候,調用NotifyService.createAnnounce
方法,
然後在用戶登錄系統或者其他的某一個時刻調用NotifyService.pullAnnounce
方法,
最後在用戶查詢消息隊列的時候調用NotifyService.getUserNotify
方法。
信息的創建
![](http://upload-images.jianshu.io/upload_images/79702-859cd50888877d67.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
信息的創建
信息的創建,只需要直接調用NotifyService.createMessage
方法就可以了,
在下一次用戶查詢消息隊列的時候,就會查詢這條信息。