一、前言
IM的羣聊消息,究竟存1份(即擴散讀方式)仍是存多份(即擴散寫方式)?
上一篇文章《IM羣聊消息的已讀回執功能該怎麼實現?》是說,「很容易想到,是存一份」,被網友們罵了,你們爭論的很激烈(見下圖)。
<ignore_js_op>
網友罵的對,任何技術方案,都不是天才般靈感乍現想到的,必定是一個演進迭代,逐步優化的過程。今天就聊一聊,IM羣聊消息,爲啥只須要存一份。
不過,從公開的技術資料來看,微信的羣聊消息應該使用的是存多份(即擴散寫方式),詳細的方案能夠在微信團隊分享的這篇文章裏找到答案:《微信後臺團隊:微信後臺異步消息隊列的優化升級實踐分享》。php
學習交流:html
- 即時通信開發交流3羣:185926912[推薦]算法
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》數據庫
(本文同步發佈於:http://www.52im.net/thread-1616-1-1.html)微信
二、本文做者
<ignore_js_op>
沈劍:58技術委員會主席,58高級架構師,58到家技術總監。C2C技術部負責人,58技術學院優秀講師。
沈劍的另外幾篇有關IM的文章也值得你去閱讀:
架構
三、IM開發乾貨系列文章
本文是系列文章中的第15篇,總目錄以下:
負載均衡
- 《IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞》
- 《IM消息送達保證機制實現(二):保證離線消息的可靠投遞》
- 《如何保證IM實時消息的「時序性」與「一致性」?》
- 《IM單聊和羣聊中的在線狀態同步應該用「推」仍是「拉」?》
- 《IM羣聊消息如此複雜,如何保證不丟不重?》
- 《一種Android端IM智能心跳算法的設計與實現探討(含樣例代碼)》
- 《移動端IM登陸時拉取數據如何做到省流量?》
- 《通俗易懂:基於集羣的移動端IM接入層負載均衡方案分享》
- 《淺談移動端IM的多點登錄和消息漫遊原理》
- 《IM開發基礎知識補課(一):正確理解前置HTTP SSO單點登錄接口的原理》
- 《IM開發基礎知識補課(二):如何設計大量圖片文件的服務端存儲架構?》
- 《IM開發基礎知識補課(三):快速理解服務端數據庫讀寫分離原理及實踐建議》
- 《IM開發基礎知識補課(四):正確理解HTTP短鏈接中的Cookie、Session和Token》
- 《IM羣聊消息的已讀回執功能該怎麼實現?》
- 《IM羣聊消息到底是存1份(即擴散讀)仍是存多份(即擴散寫)?》(本文)
另外,若是您是IM開發初學者,強烈建議首先閱讀《新手入門一篇就夠:從零開發移動端IM》。異步
四、更多關於IM羣聊的文章
IM系統中的羣聊功能,是個很大話題,下面幾篇在關羣聊的文章您也能夠讀一讀:
學習
- 《如何保證IM實時消息的「時序性」與「一致性」?》
- 《IM單聊和羣聊中的在線狀態同步應該用「推」仍是「拉」?》
- 《IM羣聊消息如此複雜,如何保證不丟不重?》
- 《微信後臺團隊:微信後臺異步消息隊列的優化升級實踐分享》
- 《移動端IM中大規模羣消息的推送如何保證效率、實時性?》
- 《現代IM系統中聊天消息的同步和存儲方案探討》
- 《關於IM即時通信羣聊消息的亂序問題討論》
- 《IM羣聊消息的已讀回執功能該怎麼實現?》
>> 更多同類文章 ……
另外,《一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)》一文中也包含了羣聊的完整設計,若是您設計IM不知從何下手,能夠詳細地參考此文。優化
五、最基本的方案:「在線的羣友不存儲消息,離線的羣友才存儲」
羣信息,用戶信息,羣成員關係都是基礎數據:
group_info(gid, group_info);
user_info(uid, user_info);
group_members(gid, uid);
假設一個羣(gid)裏有4個成員,其中三個在線(A, uid1, uid2),一個不在線(uid3)。
A發送了一條消息,很容易想到,對於不一樣的羣友消息存多份,每一個羣友一個隊列來存儲。但因爲在線的用戶會實時的收到消息,因此暫定只爲離線的用戶存儲。
用戶收到的羣消息,也是基礎數據:
user_msgs(uid,msgid,gid,sender_uid,time,content);
<ignore_js_op>
很容易想到,整個羣消息的發送流程如上圖1-4:
- 1)發送消息;
- 2)查詢狀態;
- 3)不在線的存儲離線;
- 4)在線的實時推送。
「在線的羣友不存儲,離線的羣友才存儲」會帶來的問題是,若是第四步發生異常,羣友會丟失消息。
六、優化的方案:「無論羣員是否在線,都要先存儲消息」
消息的可達性是聊天系統中最重要的要素(沒有之一),故這個方案是不行的,須要優化爲「不論是否在線,都要先存儲」。
<ignore_js_op>
發送羣消息的流程優化爲,如上圖1-4:
- 1)發送消息;
- 2)全部人都存一份;
- 3)查詢狀態;
- 4)在線的實時推送。
先將消息落地,可以保證消息可達性,那什麼時候才能刪除已經落地的羣消息呢?咱們繼續往下看。
<ignore_js_op>
對於在線的羣友:收到羣消息後,給個ack確認才能刪除。
畫外音:邏輯刪除,仍是物理刪除,根據業務是否有消息漫遊決定。
<ignore_js_op>
對於離線的羣友:在下次登錄後,拉取完離線消息再給ack確認才能刪除。
總之:爲了保證消息的可達性,不論是在線消息仍是離線消息,必須接收方給ack確認,才能刪除消息。
七、「無論羣員是否在線,都冗餘一份羣消息」帶來的問題
「不論是否在線,都冗餘一份羣消息」帶來的問題是:同一條消息存儲了不少次,對磁盤和帶寬形成了很大的浪費。
很容易想到的優化是:羣消息實體存儲一份,用戶只冗餘消息ID。
<ignore_js_op>
故基礎數據能夠由:
user_msgs(uid,msgid,gid,sender_uid,time,content);
優化爲:
group_msgs(msgid,gid,sender_uid,time,content);
user_msgs(uid, msgid, gid);
這個優化,對於消息投遞,以及消息刪除的核心流程沒有影響,幾個實踐爲:
- 在線用戶投遞消息實體,ack消息ID;
- 離線用戶先拉取消息ID,再拉取消息實體,再ack消息ID。
如此這般,假如在某個羣友A期間,羣裏陸續發送了N條消息,則user_msgs(uid, msgid, gid)裏,會有 uidA -> mid1,mid2, mid3, … midN 等N條離線記錄,拉取離線消息時,能夠把這N條消息一次性拉取出來,而後再刪除:
delete from user_msgs where msgid in($mid1,$mid2…, $midN) and gid=$gid
八、終級方案:利用羣消息的「偏序」特性優雅地實現「只存1份」
然而,羣消息具有「偏序」特性,上面的一次性刪除徹底能夠優化爲:
delete from user_msgs
where msgid >= $mid1 and gid=$gid
這就意味着,每一個用戶只須要記錄「最近一次收到的消息ID」,而不用記錄「全部未收到的消息ID集合」,每當收在線消息ack,以及拉離線消息ack時,只須要更新這個「最近一次收到的消息ID」便可。
因而乎,基礎數據能夠由:
group_members(gid, uid);
group_msgs(msgid,gid,sender_uid,time,content);
user_msgs(uid, msgid, gid);
優化爲:
group_members(gid, uid, last_ack_msgid);
group_msgs(msgid,gid,sender_uid,time,content);
user_msgs(uid, msgid, gid); // 再也不須要
<ignore_js_op>
即:羣消息只存儲一份,羣友無需冗餘任何消息實體,或者消息ID了。
<ignore_js_op>
對於在線的羣友:收到羣消息後,修改這個last_ack_msgid。
<ignore_js_op>
對於離線的羣友:拉取羣消息後,也修改這個last_ack_msgid。
畫外音:這裏的討論,僅限於接收方收到了哪些消息,和發送方的已讀回執沒有關係。(這裏指的是做者的上篇文章《IM羣聊消息的已讀回執功能該怎麼實現?》)
九、本文小結
任何架構方案都不是靈光一現,而是逐步迭代優化產生的:
- 方案1:羣聊消息存多份,只存在線,消息容易丟;
- 方案2:羣聊消息存多份,全部羣友都存儲,消息冗餘多;
- 方案3:羣聊消息存多份,只存ID,未利用偏序;
- 終極方案:羣聊消息存一份,只存last_ack_msgid。
架構不(只)是設計出來的,更是演進出來的。
(本文同步發佈於:http://www.52im.net/thread-1616-1-1.html)