IM消息送達保證機制實現(二):保證離線消息的可靠投遞

一、前言

本文的上篇《IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞》中,咱們討論了在線實時消息的投遞能夠經過應用層的確認、發送方的超時重傳、接收方的去重等手段來保證業務層面消息的不丟不重。html

但實時在線投遞針對的是消息收發雙方都在線的狀況(如當發送方用戶A發送消息給接收方用戶B時,用戶B是在線的),那若是消息的接收方用戶B不在線,系統是如何保證消息的可達性的呢?這就是本文要討論的問題。(本文同步發佈於:http://www.52im.net/thread-59...數據庫

二、學習交流

三、IM消息送達保證系列文章

本文是討論IM消息送達保證系列文章中的第2篇,總目錄以下:網絡

IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞
IM消息送達保證機制實現(二):保證離線消息的可靠投遞》(本文)性能

另外,若是您正在查閱移動端IM開發資料,推薦閱讀《新手入門一篇就夠:從零開發移動端IM》。學習

四、消息接收方不在線時的典型消息發送流程

如上圖所述,一般此類狀況下消息的發送流程以下:優化

Step 1:用戶A發送一條消息給用戶B;ui

Step 2:服務器查看用戶B的狀態,發現B的狀態爲「offline」(即B當前不在線);.net

Step 3:服務器將此條消息以離線消息的形式持久化存儲到DB中(固然,具體的持久化方案可由您IM的具體技術實現爲準);

Step 4:服務器返回用戶A「發送成功」ACK確認包(注:對於消息發送方而言,消息一旦落地存儲至DB就認爲是發送成功了)。

關於 「Step 4」 的補充說明:

請必定要理解「Step 4」,由於如今不管是傳統的PC端IM(相似QQ這樣的——能夠在UI上看到好友的在線、離線狀態)仍是目前主流的移動端IM(強調的是用戶全時在線——即你看不到好友到底在線仍是離線,反正給你的假像就是這個好友「應該」是在線的),消息發送出去後,不管是對方實時在線收到仍是對方不在線而被服務端離線存儲了,對於發送方而言只要消息沒有由於網絡等緣由莫名消失,就應該認爲是「被收到了」。

從技術的角度講,消息接收方收到的消息應答ACK包的真正發起者,實際上有兩種可能性:一種是由接收方發出、而另外一種是由服務端代爲發送(這在MobileIMSDK開源工程裏被稱做「僞應答」)。

五、典型離線消息表的設計以及拉取離線消息的過程

① 存儲離線消看書的表主要字段大體以下:

-- 消息接收者ID

receiver_uidvarchar(50),

-- 消息的惟一指紋碼(即消息ID),用於去重等場景

msg_idvarchar(70),

-- 消息發出時的時間戳(若是是個跨國IM,則此時間戳多是GMT-0標準時間)

send_timetime,

-- 消息發送者ID

sender_uidvarchar(50),

-- 消息類型(標識此條消息是:文本、圖片仍是語音留言等)

msg_typeint,

-- 消息內容(若是是圖片或語音留言等類型,由此字段存放的多是對應文件的存儲地址或CDN的訪問URL)

msg_contentvarchar(1024),

…

② 離線消息拉取模式:

接收方B要拉取發送方A給ta發送的離線消息,只需在receiver_uid(即接收方B的用戶ID), sender_uid(即發送方A的用戶ID)上查詢,而後把離線消息刪除,再把消息返回B便可。

③ 離線消息的拉取,若是用SQL語句來描述的話,它能夠是:

SELECT msg_id, send_time, msg_type, msg_content FROM offline_msgs WHERE receiver_uid = ? and sender_uid = ?

④ 離線拉取的總體流程以下圖所示:

Step 1:用戶B開始拉取用戶A發送給ta的離線消息;

Step 2:服務器從DB(或對應的持久化容器)中拉取離線消息;

Step 3:服務器從DB(或對應的持久化容器)中把離線消息刪除;

Step 4:服務器返回給用戶B想要的離線消息。

六、上述流程存在的問題以及優化方案

若是用戶B有不少好友,登錄時客戶端須要對全部好友進行離線消息拉取,客戶端與服務器交互次數就會比較多。

① 拉取好友離線消息的客戶端僞代碼:

// 登錄時全部好友都要拉取

for(all uid in B’s friend-list){

      // 與服務器交互

     get_offline_msg(B,uid);

}

② 優化方案1:

先拉取各個好友的離線消息數量,真正用戶B進去看離線消息時,才往服務器發送拉取請求(手機端爲了節省流量,常常會使用這個按需拉取的優化)。

③ 優化方案2:

以下圖所示,一次性拉取全部好友發送給用戶B的離線消息,到客戶端本地再根據sender_uid進行計算,這樣的話,離校消息表的訪問模式就變爲->只須要按照receiver_uid來查詢了。登陸時與服務器的交互次數下降爲了1次。

④ 方案小結:

一般狀況下,主流的的移動端IM(好比微信、手Q等)一般都是以「優化方案2」爲主,由於移動網絡的不可靠性加上電量、流量等資源的昂貴性,能儘可能一次性幹完的事,就儘量一次搞定,從而提供整個APP的用戶體驗(對於移動端應用而言,省電、省流量一樣是用戶體驗的一部分)。這方面的文章,能夠進一步參閱《談談移動端 IM 開發中登陸請求的優化》、《移動端IM實踐:iOS版微信界面卡頓監測方案》、《移動端IM實踐:Android版微信如何大幅提高交互性能(二)》。

七、消息接收方一次拉取大量離線消息致使速度慢、卡頓的解決方法

用戶B一次性拉取全部好友發給ta的離線消息,消息量很大時,一個請求包很大、速度慢,容易卡頓怎麼辦?

正如上圖所示,咱們能夠分頁拉取:根據業務需求,先拉取最新(或者最舊)的一頁消息,再按需一頁頁拉取,這樣便能很好地解決用戶體驗問題。

八、優化離線消息的拉取過程,保證離線消息不會丟失

如何保證可達性,上述步驟第三步執行完畢以後,第四個步驟離線消息返回給客戶端過程當中,服務器掛點,路由器丟消息,或者客戶端crash了,那離線消息豈不是丟了麼(數據庫已刪除,用戶還沒收到)?

確實,若是按照上述的一、二、三、4步流程,的確是的,那如何保證離線消息的絕對可靠性、可達性?

如同在線消息的應用層ACK機制同樣,離線消息拉時,不可以直接刪除數據庫中的離線消息,而必須等應用層的離線消息ACK(說明用戶B真的收到離線消息了),才能刪除數據庫中的離線消息。這個應用層的ACK能夠經過實時消息通道告之服務端,也能夠經過服務端提供的REST接口,以更通用、簡單的方式通知服務端。

九、進一步優化,解決重複拉取離線消息的問題

若是用戶B拉取了一頁離線消息,卻在ACK以前crash了,下次登陸時會拉取到重複的離線消息麼?

確實,拉取了離線消息卻沒有ACK,服務器不會刪除以前的離線消息,故下次登陸時系統層面還會拉取到。但在業務層面,能夠根據msg_id去重。SMC理論:系統層面沒法作到消息不丟不重,業務層面能夠作到,對用戶無感知。

優化後的拉取過程,以下圖所示:

十、進一步優化,下降離線拉取ACK帶來的額外與服務器的交互次數

假設有N頁離線消息,如今每一個離線消息須要一個ACK,那麼豈不是客戶端與服務器的交互次數又加倍了?有沒有優化空間?

如上圖所示,不用每一頁消息都ACK,在拉取第二頁消息時至關於第一頁消息的ACK,此時服務器再刪除第一頁的離線消息便可,最後一頁消息再ACK一次(實際上:最後一頁拉取的確定是空返回,這樣能夠極大地簡化這個分頁過程,不然客戶端得知道當前離線消息的總頁數,而因爲消息讀取延遲的存在,這個總頁數理論上並不是絕對不變,從而加大了數據讀取不一致的可能性)。這樣的效果是,無論拉取多少頁離線消息,只會多一個ACK請求,與服務器多一次交互。

十一、本文小結

正如本文中所列舉的問題所描述的那樣,保證「離線消息」的可達性比你們想象的要複雜一些,常見優化總結以下:

1)對於同一個用戶B,一次性拉取全部用戶發給ta的離線消息,再在客戶端本地進行發送方分析,相比按照發送方一個個進行消息拉取,能大大減小服務器交互次數;

2)分頁拉取,先拉取計數再按需拉取,是無線端的常見優化;

3)應用層的ACK,應用層的去重,才能保證離線消息的不丟不重;

4)下一頁的拉取,同時做爲上一頁的ACK,可以極大減小與服務器的交互次數。

(本文同步發佈於:http://www.52im.net/thread-59...,本文內容參考了:微信爲啥不丟「離線消息」)

相關文章
相關標籤/搜索