1. 程式人生 > >IM群聊訊息如此複雜,如何保證不丟不重?

IM群聊訊息如此複雜,如何保證不丟不重?

1、前言

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

  • 線上的群友能第一時間收到訊息;
  • 離線的群友能在登陸後收到訊息。

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

2、IM開發乾貨系列文章

3、常見的群訊息流程

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

1

2

3

4

群成員表:用來描述一個群裡有多少成員

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裡。

180836alzdf9d8z1cz66lj.jpg (629Ã260)

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

  • 步驟1:群訊息傳送者x向server發出群訊息;
  • 步驟2:server去db中查詢群中有多少使用者(x,A,B,C,D);
  • 步驟3:server去cache中查詢這些使用者的線上狀態;
  • 步驟4:對於群中線上的使用者A與B,群訊息server進行實時推送;
  • 步驟5:對於群中離線的使用者C與D,群訊息server進行離線儲存。

215937qwg4vyymosalkx5v.jpg (603Ã165)

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

  • 步驟1:離線訊息拉取者C向server拉取群離線訊息;
  • 步驟2:server從db中拉取離線訊息並返回群使用者C;
  • 步驟3:server從db中刪除群使用者C的群離線訊息。

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

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

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

1

2

3

4

群訊息表:用來儲存一個群中所有的訊息內容

t_group_msgs(group_id, sender_id, time,msg_id, msg_detail)

群離線訊息表:優化後只儲存msg_id

t_offine_msgs(user_id, group_id, msg_id)

220325gawzcbbhcwppbccc.jpg (581Ã287)

這樣優化後,群線上訊息傳送就做了一些修改:  

  • 步驟3:每次傳送線上群訊息之前,要先儲存群訊息的內容;
  • 步驟6:每次儲存離線訊息時,只儲存msg_id,而不用為每個使用者儲存msg_detail。

220348zyfaa0cbw5ab07y4.jpg (599Ã185)

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

  • 步驟1:先拉取所有的離線訊息msg_id;
  • 步驟3:再根據msg_id拉取msg_detail;
  • 步驟5:刪除離線msg_id。

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

  • 1)線上訊息的投遞可能出現訊息丟失,例如伺服器重啟,路由器丟包,客戶端crash;
  • 2)離線訊息的拉取也可能出現訊息丟失,原因同上。

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

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

220545o9qxotxqfo91579q.jpg (586Ã286)

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

  • 步驟3:在訊息msg_detail儲存到群訊息表後,不管使用者是否線上,都先將msg_id儲存到離線訊息表裡;
  • 步驟6:線上的使用者A和B收到群訊息後,需要增加一個應用層ACK,來標識訊息到達;
  • 步驟7:線上的使用者A和B在應用層ACK後,將他們的離線訊息msg_id刪除掉。

220559llglgamik2cnaenk.jpg (602Ã187)

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

  • 步驟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。

1

2

3

4

5

群成員表:用來描述一個群裡有多少成員,以及每個成員最後一條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)

群離線訊息表:不再需要了

220712w77j04g5zv0j34e3.jpg (627Ã269)

離線訊息表優化後,群線上訊息的投遞流程:  

  • 步驟3:在訊息msg_detail儲存到群訊息表後,不再需要操作離線訊息表(優化前需要將msg_id插入離線訊息表);
  • 步驟7:線上的使用者A和B在應用層ACK後,將last_ack_msg_id更新即可(優化前需要將msg_id從離線訊息表刪除)。

220736f0o052l4oh6lhw2s.jpg (641Ã184)

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

  • 步驟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)離線訊息過多,可以分頁拉取(按需拉取)優化。

網易雲信,你身邊的即時通訊和音視訊技術專家,瞭解我們,請戳網易雲信官網

想要閱讀更多行業洞察和技術乾貨,請關注網易雲信部落格

本文轉載自52im,作者:JackJiang