IM訊息送達保證機制實現(二):保證離線訊息的可靠投遞
1、前言
本文的上篇《IM訊息送達保證機制實現(一):保證線上實時訊息的可靠投遞》中,我們討論了線上實時訊息的投遞可以通過應用層的確認、傳送方的超時重傳、接收方的去重等手段來保證業務層面訊息的不丟不重。 但實時線上投遞針對的是訊息收發雙方都線上的情況(如當傳送方使用者A傳送訊息給接收方使用者B時,使用者B是線上的),那如果訊息的接收方使用者B不線上,系統是如何保證訊息的可達性的呢?這就是本文要討論的問題。
2、IM開發乾貨系列文章
3、訊息接收方不線上時的典型訊息傳送流程
如上圖所述,通常此類情況下訊息的傳送流程如下:
- Step 1:使用者A傳送一條訊息給使用者B;
- Step 2:伺服器檢視使用者B的狀態,發現B的狀態為“offline”(即B當前不線上);
- Step 3:伺服器將此條訊息以離線訊息的形式持久化儲存到DB中(當然,具體的持久化方案可由您IM的具體技術實現為準);
- Step 4:伺服器返回使用者A“傳送成功”ACK確認包(注:對於訊息傳送方而言,訊息一旦落地儲存至DB就認為是傳送成功了)。
關於 “Step 4” 的補充說明:請一定要理解“Step 4”,因為現在無論是傳統的PC端IM(類似QQ這樣的——可以在UI上看到好友的線上、離線狀態)還是目前主流的移動端IM(強調的是使用者全時線上——即你看不到好友到底線上還是離線,反正給你的假像就是這個好友“應該”是線上的),訊息傳送出去後,無論是對方實時線上收到還是對方不線上而被服務端離線儲存了,對於傳送方而言只要訊息沒有因為網路等原因莫名消失,就應該認為是“被收到了”。
4、典型離線訊息表的設計以及拉取離線訊息的過程
① 儲存離線消看書的表主要欄位大致如下:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 |
|
② 離線訊息拉取模式:接收方B要拉取傳送方A給ta傳送的離線訊息,只需在receiver_uid(即接收方B的使用者ID), sender_uid(即傳送方A的使用者ID)上查詢,然後把離線訊息刪除,再把訊息返回B即可。③ 離線訊息的拉取,如果用SQL語句來描述的話,它可以是:
1 2 3 |
|
④ 離線拉取的整體流程如下圖所示:
- Stelp 1:使用者B開始拉取使用者A傳送給ta的離線訊息;
- Stelp 2:伺服器從DB(或對應的持久化容器)中拉取離線訊息;
- Stelp 3:伺服器從DB(或對應的持久化容器)中把離線訊息刪除;
- Stelp 4:伺服器返回給使用者B想要的離線訊息。
5、上述流程存在的問題以及優化方案
如果使用者B有很多好友,登陸時客戶端需要對所有好友進行離線訊息拉取,客戶端與伺服器互動次數就會比較多。① 拉取好友離線訊息的客戶端虛擬碼:
1 2 3 4 5 |
|
② 優化方案1: 先拉取各個好友的離線訊息數量,真正使用者B進去看離線訊息時,才往伺服器傳送拉取請求(手機端為了節省流量,經常會使用這個按需拉取的優化)。③ 優化方案2: 如下圖所示,一次性拉取所有好友傳送給使用者B的離線訊息,到客戶端本地再根據sender_uid進行計算,這樣的話,離校訊息表的訪問模式就變為->只需要按照receiver_uid來查詢了。登入時與伺服器的互動次數降低為了1次。
④ 方案小結:通常情況下,主流的的移動端IM(比如微信、手Q等)通常都是以“優化方案2”為主,因為行動網路的不可靠性加上電量、流量等資源的昂貴性,能儘量一次性幹完的事,就儘可能一次搞定,從而提供整個APP的使用者體驗(對於移動端應用而言,省電、省流量同樣是使用者體驗的一部分)。這方面的文章,可以進一步參閱《談談移動端 IM 開發中登入請求的優化》、《移動端IM實踐:iOS版微信介面卡頓監測方案》、《移動端IM實踐:Android版微信如何大幅提升互動效能(二)》。
6、訊息接收方一次拉取大量離線訊息導致速度慢、卡頓的解決方法
使用者B一次性拉取所有好友發給ta的離線訊息,訊息量很大時,一個請求包很大、速度慢,容易卡頓怎麼辦?
正如上圖所示,我們可以分頁拉取:根據業務需求,先拉取最新(或者最舊)的一頁訊息,再按需一頁頁拉取,這樣便能很好地解決使用者體驗問題。
7、優化離線訊息的拉取過程,保證離線訊息不會丟失
如何保證可達性,上述步驟第三步執行完畢之後,第四個步驟離線訊息返回給客戶端過程中,伺服器掛點,路由器丟訊息,或者客戶端crash了,那離線訊息豈不是丟了麼(資料庫已刪除,使用者還沒收到)? 確實,如果按照上述的1、2、3、4步流程,的確是的,那如何保證離線訊息的絕對可靠性、可達性?
如同線上訊息的應用層ACK機制一樣,離線訊息拉時,不能夠直接刪除資料庫中的離線訊息,而必須等應用層的離線訊息ACK(說明使用者B真的收到離線訊息了),才能刪除資料庫中的離線訊息。這個應用層的ACK可以通過實時訊息通道告之服務端,也可以通過服務端提供的REST介面,以更通用、簡單的方式通知服務端。
8、進一步優化,解決重複拉取離線訊息的問題
如果使用者B拉取了一頁離線訊息,卻在ACK之前crash了,下次登入時會拉取到重複的離線訊息麼?確實,拉取了離線訊息卻沒有ACK,伺服器不會刪除之前的離線訊息,故下次登入時系統層面還會拉取到。但在業務層面,可以根據msg_id去重。SMC理論:系統層面無法做到訊息不丟不重,業務層面可以做到,對使用者無感知。優化後的拉取過程,如下圖所示:
9、進一步優化,降低離線拉取ACK帶來的額外與伺服器的互動次數
假設有N頁離線訊息,現在每個離線訊息需要一個ACK,那麼豈不是客戶端與伺服器的互動次數又加倍了?有沒有優化空間?
如上圖所示,不用每一頁訊息都ACK,在拉取第二頁訊息時相當於第一頁訊息的ACK,此時伺服器再刪除第一頁的離線訊息即可,最後一頁訊息再ACK一次(實際上:最後一頁拉取的肯定是空返回,這樣可以極大地簡化這個分頁過程,否則客戶端得知道當前離線訊息的總頁數,而由於訊息讀取延遲的存在,這個總頁數理論上並非絕對不變,從而加大了資料讀取不一致的可能性)。這樣的效果是,不管拉取多少頁離線訊息,只會多一個ACK請求,與伺服器多一次互動。
10、本文小結
正如本文中所列舉的問題所描述的那樣,保證“離線訊息”的可達性比大家想象的要複雜一些,常見優化總結如下:
- 1)對於同一個使用者B,一次性拉取所有使用者發給ta的離線訊息,再在客戶端本地進行傳送方分析,相比按照發送方一個個進行訊息拉取,能大大減少伺服器互動次數;
- 2)分頁拉取,先拉取計數再按需拉取,是無線端的常見優化;
- 3)應用層的ACK,應用層的去重,才能保證離線訊息的不丟不重;
- 4)下一頁的拉取,同時作為上一頁的ACK,能夠極大減少與伺服器的互動次數。
網易雲信,你身邊的即時通訊和音視訊技術專家,瞭解我們,請戳網易雲信官網
想要閱讀更多行業洞察和技術乾貨,請關注網易雲信部落格
本文轉載自52im,作者:JackJiang