群聊比單聊,為什麼複雜這麼多?

收藏待读

群聊比單聊,為什麼複雜這麼多?

群聊是多人社交的基本訴求,一個群友在群內發了一條消息,期望做到:

(1) 在線的群友 第一時間收到 消息;

(2) 離線的群友 能在 登陸後收到 消息;

群消息的實時性、可達性、離線消息 的複雜度,要遠高於單對單消息。

常見的群消息流程如何?

群業務的核心數據結構有兩個。

群成員表

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-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份,這極大的增加了數據庫的存儲壓力。

如何優化,減少消息冗餘量?

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

群消息表

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

畫外音: 用來存儲一個群中所有的消息內容

群離線消息表 ,需要進行 優化

t_offine_msgs(user_id, group_id, msg_id)

畫外音: 優化後只存儲msg_id

群聊比單聊,為什麼複雜這麼多?這樣優化後,群在線消息發送就做了一些修改:

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

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

群聊比單聊,為什麼複雜這麼多?拉取離線消息時也做了響應的修改:

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

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

步驟5:刪除離線msg_id;

優化後的流程,能保證消息的可達性么? 例如:

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

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

畫外音:單對單消息的可靠投遞一樣,是通過加入應用層的ACK實現的,群消息呢?

群消息,如何通過 應用層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;

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

似乎會,但可以在客戶端去重,對於重複的msg_id,對用戶不展現,從而不影響用戶體驗。

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

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

群成員表 ,增加一個屬性:

t_group_users(group_id, user_id, last_ack_msg_id )

畫外音:用來描述一個群里有多少成員,以及每個成員最後一條ack的群消息的msg_id(或者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;

加入ACK機制,保證群消息的可靠投遞只會,假設1個群有500個用戶,「每條」群消息都會變為500個應用層ACK,似乎會對服務器造成巨大的衝擊。 有沒有辦法減少ACK請求量呢?

批量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)離線消息過多,可以分頁拉取(按需拉取)優化;

思路比結論重要 ,希望大家有收穫。

群聊比單聊,為什麼複雜這麼多?

架構師之路-分享 可落地 的技術文章

推薦閱讀:

世界上最漂亮的排序算法

TopK與快速排序深度解析

拜託,面試別再問我基數排序了!

拜託,面試別再問我計數排序了!

拜託,面試別再問我桶排序了!

你丟過群消息么?

原文 :

相關閱讀

免责声明:本文内容来源于mp.weixin.qq.com,已注明原文出处和链接,文章观点不代表立场,如若侵犯到您的权益,或涉不实谣言,敬请向我们提出检举。