兩年前,萬倉一黍在博客園發了兩篇關於站內信的設計實現博文,《群發“站內信”的實現》、《群發“站內信”的實現(續)》,其中闡述了他關於站內信群發的設計思想,很具有借鑒意義。他在設計時考慮到用戶量和存儲空間的占用等問題。當然,在他的兩篇博文中強調了站內信的設計要考慮具體情況,沒有理想的設計方案,他的設計只是對於群發(點到面)的解決方案。 在此簡述一下他的設計方案,詳細的可以移步萬倉一黍的博客。
萬倉一黍的設計方案:
站內信分為“點到點”和“點到面”,“點到點”屬於私信,用戶之間傳遞的信息,一對一傳遞。“點到面”,屬於系統消息或者公共信息,屬於一對多發送。
站內信的設計既要考慮到投遞的准確性(也就是該收到的人能收到信息),也要考慮信息持久化存儲空間占用問題,在他的第一篇博文中詳細進行了介紹。
我們在此僅把第三種情況拿出來說明,也就是用戶量為百萬級,活躍用戶只占其中的一部分。
數據庫的設計:
表名:Message
ID:編號;SendID:發送者編號;RecID:接受者編號(如為0,則接受者為所有人);MessageID:站內信編號;Statue:站內信的查看狀態;
表名:MessageText
ID:編號;Message:站內信的內容;PDate:站內信發送時間;
將一封Message分為兩部分,一是存儲內容,另一個是存儲用戶的查看狀態。也就解決了關於群發信息的存儲空間占用問題,不需要為每個用戶插入相關數據。
另外考慮到百萬級用戶量,活躍用戶只占其中的一部分,不可能在發送一封系統消息時,在Message表中為每一個用戶插入一條狀態(標記為未讀),如果一百萬用戶,那么發送一條消息,就得往Message表中插入一百萬條標記狀態的數據,顯然不具有可行性。所以萬倉一黍提出換下思路:
在用戶登錄時檢索Message和MessgaeText,將MessgaeText的ID 和Messgae 的MessageID 相匹配, 這樣就有兩種情況:
一、沒有找到 RecID= 自己ID 並且MessageText中的消息ID不包含在Messgae的MessageID中
將此部分消息取出來,顯示為該用戶未讀,在用戶點擊閱讀的時候,將消息閱讀狀態寫入Messgae表,Status=已讀。
二、找到RecID=自己 並且 MessageText中的消息ID包含在Messgae的MessageID中,Status標記為已讀
將此部分消息提前出來,顯示為用戶已讀,如果想“刪除”(當然是邏輯上的刪除,並非物理數據庫刪除),設置該Status=刪除。
對於上面的設計方案,設計系統消息群發全部用戶,是很適合的。但是受眾面越小(即“點到面”的面越小),就不太合適,所以我們需要在此設計方案上進行擴展。
只對上面提到的用戶百萬級且活躍用戶只占一部分這種情況探討。還是采用將消息內容和閱讀狀態分開設計。
我們將點到點和點到面綜合到一起考慮,並且精細化這個“面”,不再是籠統的全部用戶。“面”可以是具有某一角色的用戶、某一用戶組的用戶甚至一些不具有任何公共特征的散列用戶。
設計思路
概述如下:我們將消息分為私信(Private)、公共消息(Public)、系統消息(Global)(或者將公共消息和系統消息合並為公共消息也可以),視情況而定。
- 點到點:一對一發送,屬於私信Private
- 點到個別:(接收面為百位用戶)一對多(幾百)發送,采用私信方式(Private)
- 點到局部:(接收面為具有某些公共特征如用戶組、用戶角色),屬於公共消息(Public)
- 點到全部:一對全部發送,屬於系統消息(Global)
數據庫設計
表名:Message
ID:編號;RecID:接收者編號;MessageID:站內信編號;Statue:站內信的查看狀態
表名:MessageText
ID:編號;SendID:發送者編號;Message:站內信的內容;Type:信息類型;Group:用戶組ID; PostDate:站內信發送時間
其中Status狀態有未讀、已讀、刪除
Type類型有Private(私信)、Public(公共消息)、Global(系統消息)
第一種 點到點
點到點發送屬於私信,比如A用戶發送給B用戶,首先在MessageText表中插入消息內容並且設置Type=Private,同時在Message表中插入一條記錄設置RecID=B,Status=未讀
用戶B查找RecID=B,並且Staus為未讀,Type=Private,顯示為私信未讀,點擊閱讀后改變Status=已讀
用戶B查找RecID=B,並且Staus為已讀,Type=Private,顯示為私信已讀,刪除設置Status=刪除
第二種 點到個別
采用和私信相同的方式,在發送一條消息時在MessageText表中插入消息內容並且設置Type=Private,同時在Message表中插入多條記錄設置RecID=各接收者ID,Status=未讀
每個接收采用和私信一樣的方式讀取處理。
第三種 點到局部
點到局部是一對某角色或某用戶組發送,例如管理員向普通用戶組發送,在MessageText表插入消息內容,且設置Type=Public 和Group為用戶組ID
用戶登錄后分兩種情況:
1、未找到RecId=自己ID 且 MessageText中(Type=Public 和Group=自己所在組 ) 的消息ID不包含在Messgae的MessageID中
提取出來顯示為用戶公共消息未讀,在用戶點擊閱讀的時候,將消息閱讀狀態寫入Messgae表,Status=已讀。
2、找到RecId=自己ID 且 MessageText中(Type=Public 和Group=自己所在組 ) 的消息ID包含在Messgae的MessageID中
將此部分消息提取出來,顯示為用戶公共消息已讀,如果想“刪除”(當然是邏輯上的刪除,並非物理數據庫刪除),設置該Status=刪除。
注:此時可以不驗證Group=自己所在組
第四種 點到全部
點到全部和點到局部采用類似的處理方式。例如管理員向普通用戶組發送,在MessageText表插入消息內容,且設置Type=Global
用戶登錄后分兩種情況:
1、未找到RecId=自己ID 且 MessageText中(Type=Global ) 的消息ID不包含在Messgae的MessageID中
提取出來顯示為用戶系統消息未讀,在用戶點擊閱讀的時候,將消息閱讀狀態寫入Messgae表,Status=已讀。
2、找到RecId=自己ID 且 MessageText中(Type=Global ) 的消息ID包含在Messgae的MessageID中
將此部分消息提取出來,顯示為用戶系統消息已讀,如果想“刪除”(邏輯上的刪除,並非物理數據庫刪除),設置該Status=刪除。
處理流程
我們再來看下整個處理流程,用戶登錄后系統是怎樣提取和顯示信息的。
用戶登錄后,采用Ajax異步加載、統計用戶站內信
- Messgae表中RecId=自己ID 且Status=未讀,顯示為私信未讀
- Messgae表中RecId=自己ID 且Status=已讀 且 Type=Private,顯示為私信已讀
- Messgae表中未找到RecId=自己ID 且 MessageText中(Type=Public 和Group=自己所在組 ) 的消息ID不包含在Messgae的MessageID中,顯示為公共消息未讀
- Messgae表中找到RecId=自己ID 且 MessageText中(Type=Public ) 的消息ID包含在Messgae的MessageID中 ,顯示為公共消息已讀
- Messgae表中未找到RecId=自己ID 且 MessageText中(Type=Global ) 的消息ID不包含在Messgae的MessageID中 ,顯示為系統消息未讀
- Messgae表中找到RecId=自己ID 且 MessageText中(Type=Global ) 的消息ID包含在Messgae的MessageID中 ,顯示為系統消息已讀
或許,大家會想這種方案同樣沒有解決物理刪除的問題或者更加精細化的接收的“面”,在用戶顯示消息時需要很多復雜的判斷影響用戶體驗和性能等等問題。其實我們還可以設置消息的有效期,過期的消息不顯示。或者自己設計一個消息清除機制。當然,這些都需要以后進一步完善和改進,本文中暫時沒有涉及。以上只是本人關於站內信的一些淺薄之見,能力有限必然存在各種問題,懇請園內的各位能提出批評、建議。如果您覺得本文很好可以 點擊推薦,讓更多的人參與討論。 同時向萬倉一黍致以謝意,感謝他分享了自己的寶貴思想。