IM開發(fā)技術(shù)分享:淺談IM系統(tǒng)中離線消息、歷史消息的最佳實踐

本文由融云技術(shù)團隊原創(chuàng)分享,原題“IM 消息數(shù)據(jù)存儲結(jié)構(gòu)設(shè)計”,內(nèi)容有修訂。
1、引言
在如今的移動互聯(lián)網(wǎng)時代,IM類產(chǎn)品已是我們生活中不可或缺的組成部分。像微信、釘釘、QQ等是典型的以 IM 為核心功能的社交產(chǎn)品。另外也有一些應(yīng)用雖然IM功能不是核心,但IM能力也是其整個應(yīng)用極其重要的組成部分,比如在線游戲、電商直播等應(yīng)用。
在IM技術(shù)應(yīng)用場景越來越廣泛的前提下,對即時通訊IM技術(shù)的學(xué)習(xí)和掌握就顯的越來越有必要。
在IM龐大的技術(shù)體系中,消息系統(tǒng)無疑是最核心的,而消息系統(tǒng)中,最關(guān)鍵的部分是消息的分發(fā)和存儲,而離線消息和歷史消息又是這個關(guān)鍵環(huán)節(jié)中不可回避的技術(shù)要點。
本文將基于IM消息系統(tǒng)的技術(shù)實踐,分享關(guān)于離線消息和歷史消息的正確理解,以及具體的技術(shù)配合和實踐,希望能為你的離線消息和歷史消息技術(shù)設(shè)計帶來最佳實踐靈感。

學(xué)習(xí)交流:
- 移動端IM開發(fā)入門文章:《新手入門一篇就夠:從零開發(fā)移動端IM》
- 開源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK?
(本文同步發(fā)布于:http://www.52im.net/thread-3887-1-1.html)
2、相關(guān)文章
技術(shù)相關(guān)文章:
《什么是IM系統(tǒng)的可靠性?》
《閑魚IM的在線、離線聊天數(shù)據(jù)同步機制優(yōu)化實踐》
《閑魚億級IM消息系統(tǒng)的可靠投遞優(yōu)化實踐》
《一套億級用戶的IM架構(gòu)技術(shù)干貨(下篇):可靠性、有序性、弱網(wǎng)優(yōu)化等》
《IM消息送達保證機制實現(xiàn)(二):保證離線消息的可靠投遞》
《我是如何解決大量離線消息導(dǎo)致客戶端卡頓的》
融云技術(shù)團隊分享的其它文章:
《融云安卓端IM產(chǎn)品的網(wǎng)絡(luò)鏈路?;罴夹g(shù)實踐》
《全面揭秘億級IM消息的可靠投遞機制》
《解密融云IM產(chǎn)品的聊天消息ID生成策略》
《萬人群聊消息投遞方案的思考和實踐》
《基于WebRTC的實時音視頻首幀顯示時間優(yōu)化實踐》
《融云IM技術(shù)分享:萬人群聊消息投遞方案的思考和實踐》
3、IM消息投遞的一般做法
在通常的IM消息系統(tǒng)中,對于實時消息、離線消息、歷史消息大概都是下面這樣的技術(shù)思路。
對于在線用戶:消息會直接實時發(fā)送到在線的接收方,消息發(fā)送完成后,服務(wù)器端并不會對消息進行落地存儲。
而對于離線的用戶:服務(wù)器端會將消息存入到離線庫,當(dāng)用戶登錄后,從離線庫中將離線消息拉走,然后服務(wù)器端將離線消息刪除。
這樣實現(xiàn)的缺點就是消息不持久化,導(dǎo)致消息無法支持消息漫游,降低了消息的可靠性。
(PS:實際上,這其實也不能算是缺點,因為一些場景下存儲歷史消息并不是必須的,所謂的消息漫游能力也不是必備的,比如微信。)
而在我們設(shè)計的消息系統(tǒng)中,服務(wù)器只要接收到了發(fā)送方發(fā)上來的消息,在轉(zhuǎn)發(fā)給接收方的同時也會在離線數(shù)據(jù)庫及歷史消息庫中進行消息的落地存儲,而歷史消息的落地也就能支持消息漫游等相關(guān)功能了。
4、什么是離線消息和歷史消息?
關(guān)于離線消息和歷史消息,在技術(shù)上,我們是這樣定義。
1)離線消息:
離線消息就是用戶(即接收方)在離線過程中收到的消息,這些消息大多是用戶比較關(guān)心的消息,具有一定的時效性。
以我們的系統(tǒng)經(jīng)驗來說,我們的離線消息默認(rèn)只保存最近七天的消息。
用戶(即接收方)在下次登錄后會全量獲取這些離線消息,然后在客戶端根據(jù)聊天會話進行離線消息的UI展示(比如顯示一個未讀消息氣泡等)。
(PS:用戶離線的可能性在技術(shù)上其實是由很多種情況組成的,比如對方不在線、對方網(wǎng)絡(luò)斷掉了、對方手機崩潰了、服務(wù)器發(fā)送時出錯了等等,嚴(yán)格來講——只要無法實時發(fā)送成的消息,都算“離線消息”。)
2)歷史消息:
歷史消息存儲了用戶所有的聊天消息,這些消息包括發(fā)出的消息以及接收到的消息。
在客戶端獲取歷史消息時,通常是按照會話進行分頁獲取的。
以我們的系統(tǒng)經(jīng)驗來說,歷史消息的存儲時間我們設(shè)計默認(rèn)為半年,當(dāng)然這個時間可以按實際的產(chǎn)品運營規(guī)則來定,沒有硬性規(guī)定。
5、IM消息的發(fā)送及存儲流程
以下是我們系統(tǒng)整體的消息發(fā)送及存儲流程:

?如上圖所示:當(dāng)用戶發(fā)送聊天消息到服務(wù)器端后,首先會進入到消息系統(tǒng)中,消息系統(tǒng)會對消息進行分發(fā)以及存儲。
這個過程中:對于在線的接收方,會選擇直接推送消息。但是遇到接收方不在線或者是消息推送失敗的情況下,也會有另外的消息獲取方式,比如接收方會主動向服務(wù)器拉取未收到的消息。但是接收方何時來服務(wù)器拉取消息以及從哪里拉取是未知的,所以消息存入到離線庫的意義也就在這里。
消息系統(tǒng)存儲離線的過程中,為了不影響整個系統(tǒng)的更為平穩(wěn),我們使用了MQ消息隊列進行IO解偶,所以聊天消息實際上是異步存入到離線庫中的(通過MQ進行慢IO解偶,這其實也是慣常做法)。
在分發(fā)完消息后:消息服務(wù)會同步一份消息數(shù)據(jù)到歷史消息服務(wù)中,歷史消息服務(wù)同樣會對消息進行落地存儲。
對于新的客戶端設(shè)備:會有同步消息的需求(所謂的消息漫游能力),而這也正是歷史消息的主要作用。在歷史消息庫中,客戶端是可以拉取任意會話的全量歷史消息的。
6、IM離線消息、歷史消息在存儲邏輯上的區(qū)別
6.1 概述
通過上面的圖中能清晰的看到:
1)離線消息我們存儲介質(zhì)選用的是?Redis;
2)歷史消息我們選用的是?HBase。
對于為什么選用不同的存儲介質(zhì),其實我們考慮的是離線消息和歷史消息不同的業(yè)務(wù)場景和讀寫模式。
下面我們重點介紹一下離線消息和歷史消息存儲的區(qū)別。
6.2 離線消息存儲模式——“擴散寫”
離線消息的存儲模式我們用的是擴散寫。

如上圖所示:每個用戶都有自己單獨的收件箱和發(fā)件箱:
1)收件箱存放的是需要向這個接收端同步的所有消息;
2)發(fā)件箱里存放的是發(fā)送端發(fā)出的所有消息。
以單聊為例:聊天中的兩人會話中,消息會產(chǎn)生兩次寫,即發(fā)送者的發(fā)件箱和接收端的收件箱。
而在群的場景下:寫入會被更加的放大(擴散),如果群里有 N 個人,那一條群消息就會被擴散寫 N 次。
小結(jié)一下:
1)擴散寫的優(yōu)點是:接收端的邏輯會非常清晰簡單,只需要從收件箱里讀取一次即可,大大降低了同步消息所需的讀的壓力;
2)擴散寫的缺點是:寫入會被成指數(shù)地放大,特別是針對群這種場景。
6.3 歷史消息存儲模式——“擴散讀”
歷史消息的存儲模式我們用的是擴散讀。
因為歷史消息中,每個會話都保存了整個會話的全量消息。在擴散讀這種模式下,每個會話的消息只保存一次。
對比擴散寫模式,擴散讀的優(yōu)點和缺點如下:
1)優(yōu)點是:寫入次數(shù)大大降低,特別是針對群消息,只需要存一次即可;
2)缺點是:接收端接收消息非常的復(fù)雜和低效,因為這種模式客戶端想拉取到所有消息就只能每個會話同步一次,讀就會被放大,而且可能會產(chǎn)生很多次無效的讀,因為有些會話可能根本沒有新消息。

6.4 小結(jié)
在 IM 這種應(yīng)用場景下,通常會用到擴散寫這種消息同步模型,一條消息產(chǎn)生一條,但是可能會被讀多次,是典型的讀多寫少的場景。
一個優(yōu)化好的IM系統(tǒng),必須從設(shè)計上平衡讀寫壓力,避免讀或者寫任意一個維度達到天花板。
當(dāng)然擴散寫這種模式也有其弊端,比如萬人群,會導(dǎo)致一條消息,寫入了一萬次。
綜合來講:我們需要根據(jù)自己的業(yè)務(wù)場景做相應(yīng)設(shè)計選擇,以我們的IM系統(tǒng)為例,就是是根據(jù)了離線和歷史消息的不同場景選擇了寫擴散和讀擴散的組合模式。適合的才是最好的,沒有必要死搬硬套理論。
7、IM客戶端的拉取消息邏輯
7.1 離線消息拉取邏輯
對于IM客戶端而言,離線消息的獲取針對的是自己的整個離線消息,包括所有的會話(直白了說,就是上線時拉取此次離線過程中的所有未收取的離線消息)。
離線消息的獲取是自上而下的方式(按時間序),我們的經(jīng)驗是一次獲取 200 條(PS:如果離線消息過多,會分頁多次拉取,拉取1“次”可以理解為拉取1“頁”)。
在客戶端拉取離線消息的信令中,需要帶上當(dāng)前客戶端緩存的消息的最大時間戳。
通過上節(jié)的圖我們應(yīng)該知道,離線消息我們存儲的是一個線性結(jié)構(gòu)(指的是按時間順序),Server 會根據(jù)這個時間戳向下查找離線消息。當(dāng)重裝或者新安裝 App 時,客戶端的“當(dāng)前客戶端緩存的消息的最大時間戳”可以傳 0 上來。
Server 也會緩存客戶端拉取到的最后一條消息的時間戳,然后根據(jù)業(yè)務(wù)場景,客戶端類型等因素來決定從哪里開始拉取,如果沒有拉取完 Server 會在拉取消息的應(yīng)答中帶相應(yīng)的標(biāo)記位,告訴客戶端繼續(xù)拉取,客戶端循環(huán)拉取,直到所有離線消息拉完。
7.2 歷史消息拉取邏輯
歷史消息的獲取通常針對的是單一會話。
在拉取過程中,需要向服務(wù)端提交兩個參數(shù):
1)對方的 ID(如果是單聊的話就是對方的 UserID,如果是群則是群組ID);
2)當(dāng)前會話的最前面消息的時間戳(即當(dāng)前會話最老一條消息的時間戳)。
Server據(jù)這兩個參數(shù),可以定位到這個客戶端的此會話,然后一次獲取 20 條歷史消息。
消息的拉取時序上采用的是自下而上的方式(也就是時間序逆序),即從最后面往前翻。只要有消息,客戶端可以一直向前翻,手動觸發(fā)獲取會話的歷史消息。
上面的拉取邏輯,在IM界面功能上通常對應(yīng)的是下拉或點擊“加載更多”,比如這樣:

8、本文小結(jié)
本文主要分享了IM中有關(guān)離線消息和歷史消息的正確,主要包括離線消息和歷史消息的區(qū)別,以及二者在存儲、分發(fā)、拉取邏輯方面的最佳踐等。如對文中內(nèi)容有異議,歡迎留言討論。
9、參考資料
[1]?一套海量在線用戶的移動端IM架構(gòu)設(shè)計實踐分享(含詳細圖文)
[2]?一套原創(chuàng)分布式即時通訊(IM)系統(tǒng)理論架構(gòu)方案
[3]?從零到卓越:京東客服即時通訊系統(tǒng)的技術(shù)架構(gòu)演進歷程
[4]?一套億級用戶的IM架構(gòu)技術(shù)干貨(上篇):整體架構(gòu)、服務(wù)拆分等
[5]?閑魚億級IM消息系統(tǒng)的架構(gòu)演進之路
[6]?閑魚億級IM消息系統(tǒng)的可靠投遞優(yōu)化實踐
[7]?閑魚億級IM消息系統(tǒng)的及時性優(yōu)化實踐
[8]?基于實踐:一套百萬消息量小規(guī)模IM系統(tǒng)技術(shù)要點總結(jié)
[9]?IM消息送達保證機制實現(xiàn)(一):保證在線實時消息的可靠投遞
[10]?理解IM消息“可靠性”和“一致性”問題,以及解決方案探討
[11]?零基礎(chǔ)IM開發(fā)入門(一):什么是IM系統(tǒng)?
(本文同步發(fā)布于:http://www.52im.net/thread-3887-1-1.html)