IM序列5:IM群聊消息如此複雜,如何保證不丟不重?

1、前言

群聊已經成為主流IM軟體的基本功能,不管是QQ群、還是微信群,一個群友在群內發了一條消息,那麼對於IM伺服器來說需要保證:

在線的群友能第一時間收到消息;

離線的群友能在登陸後收到消息。

由於「消息風暴擴散係數」的存在(概念詳見《IM單聊和群聊中的在線狀態同步應該用「推」還是「拉」?》),群消息的複雜度要遠高於一對一的單聊消息。群消息的實時性、可達性、離線消息是今天將要討論的核心話題。

2、IM開發乾貨系列文章

《IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞》

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

《如何保證IM實時消息的「時序性」與「一致性」?》

《IM單聊和群聊中的在線狀態同步應該用「推」還是「拉」?》

《IM群聊消息如此複雜,如何保證不丟不重?》(本文

《一種Android端IM智能心跳演算法的設計與實現探討(含樣例代碼)》

《移動端IM登錄時拉取數據如何作到省流量?》

《通俗易懂:基於集群的移動端IM接入層負載均衡方案分享》

《淺談移動端IM的多點登陸和消息漫遊原理》

本系列由公號「編碼前線」整理。

3、常見的群消息流程

開始講群消息投遞流程之前,先介紹兩個群業務的核心數據結構:

  • 群成員表:用來描述一個群里有多少成員
  • t_group_users(group_id, user_id)
  • 群離線消息表:用來描述一個群成員的離線消息
  • t_offine_msgs(user_id, group_id, sender_id,time, msg_id, msg_detail)

業務場景舉例:

1)一個群中有x,A,B,C,D共5個成員,成員x發了一個消息;

2)成員A與B在線,期望實時收到消息;

3)成員C與D離線,期望未來拉取到離線消息。

系統架構簡介:

1)客戶端:x,A,B,C,D共5個客戶端用戶;

2)服務端:

2.1)所有模塊與服務抽象為server;

2.2)所有用戶在線狀態抽象存儲在高可用cache里;

2.3)所有數據信息,例如群成員、群離線消息抽象存儲在db里。

典型群消息投遞流程,如上圖步驟1-4所述:

步驟1:群消息發送者x向server發出群消息;

步驟2:server去db中查詢群中有多少用戶(x,A,B,C,D);

步驟3:server去cache中查詢這些用戶的在線狀態;

步驟4:對於群中在線的用戶A與B,群消息server進行實時推送;

步驟5:對於群中離線的用戶C與D,群消息server進行離線存儲。

典型的群離線消息拉取流程,如上圖步驟1-3所述:

步驟1:離線消息拉取者C向server拉取群離線消息;

步驟2:server從db中拉取離線消息並返回群用戶C;

步驟3:server從db中刪除群用戶C的群離線消息。

存在的問題:

上述流程是最容易想,也最容易理解的,存在的問題也最顯而易見:對於同一份群消息的內容,多個離線用戶存儲了很多份。假設群中有200個用戶離線,離線消息則冗餘了200份,這極大的增加了資料庫的存儲壓力。

4、群消息優化1:減少存儲量

為了減少離線消息的冗餘度,增加一個群消息表,用來存儲所有群消息的內容,離線消息表只存儲用戶的群離線消息msg_id,就能大大的降低資料庫的冗餘存儲量,思路如下。

  • 群消息表:用來存儲一個群中所有的消息內容
  • t_group_msgs(group_id, sender_id, time,msg_id, msg_detail)
  • 群離線消息表:優化後只存儲msg_id
  • t_offine_msgs(user_id, group_id, msg_id)

這樣優化後,群在線消息發送就做了一些修改:

步驟3:每次發送在線群消息之前,要先存儲群消息的內容;

步驟6:每次存儲離線消息時,只存儲msg_id,而不用為每個用戶存儲msg_detail。

拉取離線消息時也做了響應的修改:

步驟1:先拉取所有的離線消息msg_id;

步驟3:再根據msg_id拉取msg_detail;

步驟5:刪除離線msg_id。

存在的問題(如同單對單消息的發送一樣):

1)在線消息的投遞可能出現消息丟失,例如伺服器重啟,路由器丟包,客戶端crash;

2)離線消息的拉取也可能出現消息丟失,原因同上。

需要和單對單消息的可靠投遞一樣,加入應用層的ACK,才能保證群消息一定到達。

5、群消息優化2:應用層ACK

應用層ACK優化後,群在線消息發送又發生了一些變化:

步驟3:在消息msg_detail存儲到群消息表後,不管用戶是否在線,都先將msg_id存儲到離線消息表裡;

步驟6:在線的用戶A和B收到群消息後,需要增加一個應用層ACK,來標識消息到達;

步驟7:在線的用戶A和B在應用層ACK後,將他們的離線消息msg_id刪除掉。

對應到群離線消息的拉取也一樣:

步驟1:先拉取msg_id;

步驟3:再拉取msg_detail;

步驟5:最後應用層ACK;

步驟6:server收到應用層ACK才能刪除離線消息表裡的msg_id。

存在的問題:

1)如果拉取了消息,卻沒來得及應用層ACK,會收到重複的消息么?

答案是肯定的,不過可以在客戶端去重,對於重複的msg_id,對用戶不展現,從而不影響用戶體驗

2)對於離線的每一條消息,雖然只存儲了msg_id,但是每個用戶的每一條離線消息都將在資料庫中保存一條記錄,有沒有辦法減少離線消息的記錄數呢?

6、群消息優化3:離線消息表

其實,對於一個群用戶,在ta登出後的離線期間內,肯定是所有的群消息都沒有收到的,完全不用對所有的每一條離線消息存儲一個離線msg_id,而只需要存儲最近一條拉取到的離線消息的time(或者msg_id),下次登錄時拉取在那之後的所有群消息即可,而完全沒有必要存儲每個人未拉取到的離線消息msg_id。

  • 群成員表:用來描述一個群里有多少成員,以及每個成員最後一條ack的群消息的msg_id(或者time)
  • t_group_users(group_id, user_id, last_ack_msg_id(last_ack_msg_time))
  • 群消息表:用來存儲一個群中所有的消息內容,不變
  • t_group_msgs(group_id, sender_id, time,msg_id, msg_detail)
  • 群離線消息表:不再需要了

離線消息表優化後,群在線消息的投遞流程:

步驟3:在消息msg_detail存儲到群消息表後,不再需要操作離線消息表(優化前需要將msg_id插入離線消息表);

步驟7:在線的用戶A和B在應用層ACK後,將last_ack_msg_id更新即可(優化前需要將msg_id從離線消息表刪除)。

群離線消息的拉取流程也類似:

步驟1:拉取離線消息;

步驟3:ACK離線消息;

步驟4:更新last_ack_msg_id。

存在的問題:

由於「消息風暴擴散係數」的存在,假設1個群有500個用戶,「每條」群消息都會變為500個應用層ACK,將對伺服器造成巨大的衝擊,有沒有辦法減少ACK請求量呢?

7、群消息優化4:批量ACK

由於「消息風暴擴散係數」的存在,如果每條群消息都ACK,會給伺服器造成巨大的衝擊,為了減少ACK請求量,很容易想到的方法是批量ACK。

批量ACK的方式又有兩種:

1)每收到N條群消息ACK一次,這樣請求量就降低為原來的1/N了;

2)每隔時間間隔T進行一次群消息ACK,也能達到類似的效果。

新的問題:批量ACK有可能導致:還沒有來得及ACK群消息,用戶就退出了,這樣下次登錄會拉取到重複的離線消息。

解決方案:msg_id去重,不對用戶展現,保證良好的用戶體驗。

還可能存在的問題:群離線消息過多:拉取過慢。

解決方案:分頁拉取(按需拉取),分頁拉取的細節在《IM消息送達保證機制實現(下篇):保證離線消息的可靠投遞》一章中有詳細敘述,此處不再展開。

8、本文小結

群消息還是非常有意思的,可達性、實時性、離線消息、消息風暴擴散等等等等,做個總結:

1)不管是群在線消息,還是群離線消息,應用層的ACK是可達性的保障;

2)群消息只存一份,不用為每個用戶存儲離線群msg_id,只需存儲一個最近ack的群消息id/time;

3)為了減少消息風暴,可以批量ACK;

4)如果收到重複消息,需要msg_id去重,讓用戶無感知;

5)離線消息過多,可以分頁拉取(按需拉取)優化。


推薦閱讀:

TAG:科技 | 通信 | 即時通訊IM |