消息系統設計與實現


文/JC_Huang(簡書作者)
原文鏈接:http://www.jianshu.com/p/f4d7827821f1
著作權歸作者所有,轉載請聯系作者獲得授權,並標注“簡書作者”。

產品分析

首先我們來看一下市場上關於消息的實現是怎么樣的。

簡書

簡書的消息系統主要分了兩種

  • 簡信
  • 提醒

簡信
簡信的性質其實跟私信是一樣的,是用戶發送給用戶的一則消息,有具體的信息內容。


簡書簡信

提醒
而提醒,則是系統發送的一則消息,其文案格式是固定的,並且對特殊對象一般擁有超鏈接。


簡書提醒

知乎

知乎跟簡書一樣,主要分了兩種:

  • 私信
  • 消息

私信
跟簡書一樣,使用戶發送給用戶的一則消息,也可以是管理員發送給用戶的消息。


知乎私信

消息
知乎的消息比簡書的提醒有過之而無不及,知乎會對多條相似的消息進行聚會,以達到減輕用戶閱讀壓力的體驗。


知乎消息

消息的三種分類

通過兩種產品的簡單分析,得出他們的消息有兩種分類,在這基礎上,我們再加上一種:公告。
公告的主要性質是系統發送一則含有具體內容的消息,站內所有用戶都能讀取到這條消息。
所以,消息有三種分類:

  1. 公告 Announce
  2. 提醒 Remind
  3. 私信 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的提醒設置


Knewone提醒設置

所以我們需要再維護一個表:SubscriptionConfig,來存放用戶的提醒設置。
並且,當用戶沒有提醒設置的時候,可以使用系統提供的一套默認設置:defaultSubscriptionConfig

聚合

如果我發布了一篇文章《XXX》,在我不在線的時候,被評論了10遍,當我一上線的時候,應該是收到十條信息類似於:「誰誰誰評論了你的文章《XXX》」?
還是應該收到一條信息:「甲、乙、丙、丁...評論了你的文章《XXX》」?

知乎在聚合上做的很優秀,要知道他們要實現這個還是挺有技術的:
知乎的消息機制,在技術上如何設計與規划?
網站的消息(通知)系統一般是如何實現的?

關於這部分功能,我們還沒有具體的實現方法,暫時也無法講得更加詳細。⊙﹏⊙

五個實體

通過上面的分析,大概知道做這個消息系統,需要哪些實體類:

  1. 用戶消息隊列 UserNotify
  2. 用戶 User
  3. 訂閱 Subscription
  4. 訂閱設置 SubscriptionConfig
  5. 消息 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
消息表,我們需要targettargetType字段,來記錄該條提醒所關聯的對象。而action字段,則記錄該條提醒所關聯的動作。
比如消息:「小明喜歡了文章」
則:

target = 123, // 文章ID targetType = 'post', // 指明target所屬類型是文章 sender = 123456 // 小明ID

Save Announce and Message
當然,Notify還支持存儲公告和信息。它們會用到content字段,而不會用到targettargetTypeaction字段。

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的創建,主要通過兩個途徑:

  1. 遍歷訂閱(Subscription)表拉取公告(Announce)和提醒(Remind)的時候創建
  2. 新建信息(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, // 喜歡 }

在這套模型中,targetTypeaction是可以根據需求來擴展的,例如我們還可以增加多幾個動作的提醒: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)

  1. 往Notify表中插入一條公告記錄

createRemind(target, targetType, action, sender, content)

  1. 往Notify表中插入一條提醒記錄

createMessage(content, sender, receiver)

  1. 往Notify表中插入一條信息記錄
  2. 往UserNotify表中插入一條記錄,並關聯新建的Notify

pullAnnounce(user)

  1. 從UserNotify中獲取最近的一條公告信息的創建時間: lastTime
  2. lastTime作為過濾條件,查詢Notify的公告信息
  3. 新建UserNotify並關聯查詢出來的公告信息

pullRemind(user)

  1. 查詢用戶的訂閱表,得到用戶的一系列訂閱記錄
  2. 通過每一條的訂閱記錄的targettargetTypeactioncreatedAt去查詢Notify表,獲取訂閱的Notify記錄。(注意訂閱時間必須早於提醒創建時間)
  3. 查詢用戶的配置文件SubscriptionConfig,如果沒有則使用默認的配置DefaultSubscriptionConfig
  4. 使用訂閱配置,過濾查詢出來的Notify
  5. 使用過濾好的Notify作為關聯新建UserNotify

subscribe(user, target, targetType, reason)

  1. 通過reason,查詢NotifyConfig,獲取對應的動作組:actions
  2. 遍歷動作組,每一個動作新建一則Subscription記錄

cancelSubscription(user, target ,targetType)

  1. 刪除usertargettargetType對應的一則或多則記錄

getSubscriptionConfig(userID)

  1. 查詢SubscriptionConfig表,獲取用戶的訂閱配置

updateSubscriptionConfig(userID)

  1. 更新用戶的SubscriptionConfig記錄

getUserNotify(userID)

  1. 獲取用戶的消息列表

read(user, notifyIDs)

  1. 更新指定的notify,把isRead屬性設置為true

時序圖

提醒的訂閱、創建、拉取


提醒的訂閱、創建、拉取


我們可以在產品創建之后,調用NotifyService.subscribe方法,
然后在產品被評論之后調用NotifyService.createRemind方法,
再就是用戶登錄系統或者其他的某一個時刻調用NotifyService.pullRemind方法,
最后在用戶查詢消息隊列的時候調用NotifyService.getUserNotify方法。

公告的創建、拉取


公告的創建、拉取


在管理員發送了一則公告的時候,調用NotifyService.createAnnounce方法,
然后在用戶登錄系統或者其他的某一個時刻調用NotifyService.pullAnnounce方法,
最后在用戶查詢消息隊列的時候調用NotifyService.getUserNotify方法。

信息的創建


信息的創建


信息的創建,只需要直接調用NotifyService.createMessage方法就可以了,
在下一次用戶查詢消息隊列的時候,就會查詢這條信息。


 

 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM