羣聊這麼複雜,該怎麼玩轉?

原文鏈接


【需求緣起】

之前的文章更多的聊了單對單的消息投遞

微信爲什麼不丟消息?

http如何像tcp一樣實時的收消息?


羣聊是多人社交的基本訴求,不管是QQ羣,還是微信羣,一個羣友在羣內發了一條消息:

1)在線的羣友能第一時間收到消息

2)離線的羣友能在登陸後收到消息

由於“消息風暴擴散係數”的存在(概念詳見《QQ狀態同步究竟是推還是拉?》),羣消息的複雜度要遠高於單對單消息。羣消息的實時性,可達性,離線消息是今天將要討論的核心話題。

 

【常見的羣消息流程】

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

羣成員表用來描述一個羣裏有多少成員

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,D5個成員,成員x發了一個消息

2)成員AB在線,期望實時收到消息

3)成員CD離線,期望未來拉取到離線消息

 

系統架構簡介:

1)客戶端:x,A,B,C,D5個客戶端用戶

2)服務端

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

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

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

 


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

步驟1:羣消息發送者xserver發出羣消息

步驟2serverdb中查詢羣中有多少用戶(x,A,B,C,D)

步驟3servercache中查詢這些用戶的在線狀態

步驟4:對於羣中在線的用戶AB,羣消息server進行實時推送

步驟5:對於羣中離線的用戶CD,羣消息server進行離線存儲

 


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

步驟1:離線消息拉取者Cserver拉取羣離線消息

步驟2serverdb中拉取離線消息並返回羣用戶C

步驟3serverdb中刪除羣用戶C的羣離線消息

 

存在的問題

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

 

【羣消息優化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,才能保證羣消息一定到達。

 

【羣消息優化2:應用層ACK


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

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

步驟6在線的用戶AB收到羣消息後,需要增加一個應用層ACK,來標識消息到達

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

 


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

步驟1:先拉取msg_id

步驟3:再拉取msg_detail

步驟5:最後應用層ACK

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

 

存在的問題

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

回答:會,可以在客戶端去重,對於重複的msg_id,對用戶不展現,從而不影響用戶體驗

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

 

【羣消息優化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在線的用戶AB在應用層ACK後,將last_ack_msg_id更新即可(優化前需要將msg_id從離線消息表刪除)

 


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

步驟1:拉取離線消息

步驟3ACK離線消息

步驟4:更新last_ack_msg_id

 

存在的問題

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

 

【羣消息優化4:批量ACK

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

批量ACK的方式又有兩種:

1每收到N條羣消息ACK一次,這樣請求量就降低爲原來的1/N

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

 

新的問題

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

解決方案

msg_id去重,不對用戶展現,保證良好的用戶體驗

 

還可能存在的問題

羣離線消息過多:拉取過慢

解決方案分頁拉取(按需拉取),分頁拉取的細節在“微信爲啥不丟離線消息”一章中有詳細敘述,此處不再展開(詳見《微信爲啥不丟“離線消息”?》)。

 

【總結】

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

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

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

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

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

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

==【完】==

回【http】http如何像tcp一樣實時的收消息?
回【可靠】微信爲什麼不丟消息?
回【狀態】QQ狀態同步究竟是推還是拉?

回【離線】微信爲啥不丟“離線消息”?


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章