羣聊是多人社交的基本訴求,一個羣友在羣內發了一條消息,指望作到:數據庫
(1) 在線的羣友能第一時間收到消息;服務器
(2) 離線的羣友能在登錄後收到消息;markdown
羣消息的實時性、可達性、離線消息的複雜度,要遠高於單對單消息。數據結構
常見的羣消息流程如何?架構
羣業務的核心數據結構有兩個。優化
羣成員表:spa
t_group_users(group_id, user_id)3d
畫外音:用來描述一個羣裏有多少成員。code
羣離線消息表:orm
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) 離線消息過多,能夠分頁拉取(按需拉取)優化;
思路比結論重要,但願你們有收穫。
架構師之路 - 分享可落地的技術文章
你丟過羣消息麼?