微信:群消息,我們絕對不可能丟!
前幾天聊了1v1在線消息,離線消息的可達性:
《微信:我們絕不丟消息!》
有水友問我說,微信的群消息,是怎么做到不丟的?
做過幾十年IM架構(gòu),今天和大家聊聊消息的可靠投遞。
群聊的核心需求,群友在群內(nèi)發(fā)消息,期望:
- 在線的群友能第一時間收到消息;
- 離線的群友能在登錄后收到消息;
群消息的實時性、可達性、離線消息的復(fù)雜度,要遠高于1v1消息。
群業(yè)務(wù)的核心數(shù)據(jù)結(jié)構(gòu)如何?
群成員表:記錄群里的群成員。
t_group_users(group_id, user_id)
群離線消息表:記錄群里的離線消息。
t_offine_msgs(user_id, group_id, sender_id, time, msg_id, msg_detail)
常見的群消息流程如何?
- 假設(shè)一個群中有x,A,B,C,D共5個成員,x發(fā)了一個群消息;
- A與B在線,期望實時收到消息;
- C與D離線,期望未來拉取到離線消息;
首先,是群在線消息。 圖片 典型群消息投遞流程,如1-5所述:
- x向server發(fā)出群消息;
- server去db中查詢?nèi)河脩?x,A,B,C,D);
- server去cache中查詢?nèi)河脩粼诰€狀態(tài);
- 在線的用戶A與B,進行實時推送;
- 離線的用戶C與D,進行離線存儲;
然后,是群離線消息。 圖片 典型的群離線消息拉取流程,如1-3所述:
- C重新登陸的時候,向server拉取群離線消息;
- server從db中拉取離線消息并返回群用戶C;
- server從db中刪除群用戶C的群離線消息;
那么,問題來了!對于同一份群消息的內(nèi)容,多個離線用戶似乎要存儲很多份。假設(shè)群中有200個用戶離線,離線消息則冗余了200份,這極大地增加了數(shù)據(jù)庫的存儲壓力。
如何優(yōu)化,減少消息冗余量?
為了減少離線消息的冗余度,增加一個群消息表,用來存儲所有群消息的內(nèi)容,離線消息表只存儲用戶的群離線消息msg_id,就能大大的降低數(shù)據(jù)庫的冗余存儲量。
群消息表:存儲群中所有的消息。
t_group_msgs(group_id, sender_id, time, msg_id, msg_detail)
群離線消息表,需要進行優(yōu)化,只存儲msg_id:
t_offine_msgs(user_id, group_id, msg_id)
這樣優(yōu)化后,群在線消息發(fā)送需要做對應(yīng)的調(diào)整:
- 3.每次發(fā)送在線群消息之前,要先存儲群消息的內(nèi)容;
- 6.每次存儲離線消息時,只存儲msg_id,而不用為每個用戶存儲msg_detail;
拉取離線消息,也需要做對應(yīng)的修改:
- 1.先拉取所有的離線消息msg_id;
- 3.再根據(jù)msg_id拉取msg_detail;
- 5.刪除離線msg_id;
優(yōu)化后的流程,能保證消息的可達性么?
- 在線消息的投遞可能出現(xiàn)消息丟失,例如服務(wù)器重啟,路由器丟包,客戶端crash;
- 離線消息的拉取也可能出現(xiàn)消息丟失,原因同上;
很容易想到,必須和1v1消息投遞一樣,需要加入應(yīng)用層的ACK。
群消息,如何通過應(yīng)用層ACK,保證消息的可靠投遞?
應(yīng)用層ACK優(yōu)化后,群在線消息發(fā)送又做了調(diào)整:
- 3.在消息msg_detail存儲到群消息表后,不管用戶是否在線,都先將msg_id存儲到離線消息表里;
- 6.在線的用戶A和B收到群消息后,需要增加一個應(yīng)用層ACK,來標(biāo)識消息到達;
- 7.在線的用戶A和B在應(yīng)用層ACK后,將他們的離線消息msg_id刪除掉;
對應(yīng)到群離線消息的拉取也一樣:
- 1.先拉取msg_id;
- 2.再拉取msg_detail;
- 5.最后應(yīng)用層ACK;
- 6.server收到應(yīng)用層ACK才能刪除離線消息表里的msg_id;
如果拉取了消息,卻沒來得及應(yīng)用層ACK,會收到重復(fù)的消息么?
會,但可以在客戶端去重,對于重復(fù)的msg_id,對用戶不展現(xiàn),從而不影響用戶體驗。
對于離線的每一條消息,雖然只存儲了msg_id,但是每個用戶的每一條離線消息都將在數(shù)據(jù)庫中保存一條記錄,有沒有辦法減少離線消息的記錄數(shù)呢?
對于一個群用戶,在ta登出后的離線期間內(nèi),肯定是所有的群消息都沒有收到的,不用對所有的每一條離線消息存儲一個離線msg_id,而只需要存儲最近一條拉取到的離線消息的time(或者msg_id),下次登錄時拉取在那之后的所有群消息即可,而沒有必要存儲每個人未拉取到的所有離線消息msg_id。
群成員表,增加一個屬性,記錄每個群成員最近一條ACK的群消息msg_id(或者time)。 t_group_users(group_id, user_id, last_ack_msg_id)
群消息表,不變,依然存儲群中所有的消息: t_group_msgs(group_id, sender_id, time, msg_id, msg_detail)
群離線消息表:不再需要。
離線消息表優(yōu)化后,群在線消息的投遞流程:
- 3.在消息msg_detail存儲到群消息表后,不再需要操作離線消息表(優(yōu)化前需要將msg_id插入離線消息表);
- 7.在線的用戶A和B在應(yīng)用層ACK后,將last_ack_msg_id更新即可(優(yōu)化前需要將msg_id從離線消息表刪除);
群離線消息的拉取流程也類似:
- 1.拉取離線消息;
- 3.ACK離線消息;
- 4.更新last_ack_msg_id;
加入ACK機制,假設(shè)1個群有500個用戶,“每條”群消息都會變?yōu)?00個應(yīng)用層ACK,似乎會對服務(wù)器造成巨大的沖擊。有沒有辦法減少ACK請求量呢?
批量ACK,是一種常見的,降低請求量的方式。
批量ACK,批量ACK的方式又有兩種方式:
- 每收到N條群消息ACK一次,這樣請求量就降低為原來的1/N了;
- 每隔時間間隔T進行一次群消息ACK,也能達到類似的效果;
批量ACK有可能導(dǎo)致新的問題:如果還沒有來得及ACK群消息,用戶就退出了,這樣下次登錄似乎會拉取到重復(fù)的離線消息,怎么辦?
仍然在客戶端去重,對于重復(fù)的msg_id,對用戶不展現(xiàn),不影響用戶體驗。
群離線消息過多,拉取過慢,怎么辦?
分頁拉?。ò葱枥。?,都是常見的優(yōu)化方案。
總結(jié)
群消息還是非常有意思的,做個簡單總結(jié):
- 不管是群在線消息,還是群離線消息,應(yīng)用層的ACK是可達性的保障;
- 群消息可以只存一份,不用為每個用戶存儲離線群msg_id,只需存儲一個最近ack的群消息id/time;
- 為了減少消息風(fēng)暴,可以批量ACK;
- 如果收到重復(fù)消息,需要msg_id去重,讓用戶無感知;
- 離線消息過多,可以分頁拉?。ò葱枥。﹥?yōu)化;
知其然,知其所以然。 思路比結(jié)論更重要。