新聞中心
【需求緣起】

讓客戶(hù)滿(mǎn)意是我們工作的目標(biāo),不斷超越客戶(hù)的期望值來(lái)自于我們對(duì)這個(gè)行業(yè)的熱愛(ài)。我們立志把好的技術(shù)通過(guò)有效、簡(jiǎn)單的方式提供給客戶(hù),將通過(guò)不懈努力成為客戶(hù)在信息化領(lǐng)域值得信任、有價(jià)值的長(zhǎng)期合作伙伴,公司提供的服務(wù)項(xiàng)目有:域名注冊(cè)、網(wǎng)站空間、營(yíng)銷(xiāo)軟件、網(wǎng)站建設(shè)、伊美網(wǎng)站維護(hù)、網(wǎng)站推廣。
之前的文章更多的聊了單對(duì)單的消息投遞:
《微信為什么不丟消息?》
《http如何像tcp一樣實(shí)時(shí)的收消息?》
群聊是多人社交的基本訴求,不管是QQ群,還是微信群,一個(gè)群友在群內(nèi)發(fā)了一條消息:
(1)在線的群友能***時(shí)間收到消息
(2)離線的群友能在登陸后收到消息
由于“消息風(fēng)暴擴(kuò)散系數(shù)”的存在(概念詳見(jiàn)《QQ狀態(tài)同步究竟是推還是拉?》),群消息的復(fù)雜度要遠(yuǎn)高于單對(duì)單消息。群消息的實(shí)時(shí)性,可達(dá)性,離線消息是今天將要討論的核心話(huà)題。
【常見(jiàn)的群消息流程】
開(kāi)始講群消息投遞流程之前,先介紹兩個(gè)群業(yè)務(wù)的核心數(shù)據(jù)結(jié)構(gòu):
群成員表:用來(lái)描述一個(gè)群里有多少成員
t_group_users(group_id, user_id)
群離線消息表:用來(lái)描述一個(gè)群成員的離線消息
t_offine_msgs(user_id, group_id, sender_id,time, msg_id, msg_detail)
業(yè)務(wù)場(chǎng)景舉例:
(1)一個(gè)群中有x,A,B,C,D共5個(gè)成員,成員x發(fā)了一個(gè)消息
(2)成員A與B在線,期望實(shí)時(shí)收到消息
(3)成員C與D離線,期望未來(lái)拉取到離線消息
系統(tǒng)架構(gòu)簡(jiǎn)介:
(1)客戶(hù)端:x,A,B,C,D共5個(gè)客戶(hù)端用戶(hù)
(2)服務(wù)端
(2.1)所有模塊與服務(wù)抽象為server
(2.2)所有用戶(hù)在線狀態(tài)抽象存儲(chǔ)在高可用cache里
(2.3)所有數(shù)據(jù)信息,例如群成員、群離線消息抽象存儲(chǔ)在db里
典型群消息投遞流程,如圖步驟1-4所述:
步驟1:群消息發(fā)送者x向server發(fā)出群消息
步驟2:server去db中查詢(xún)?nèi)褐杏卸嗌儆脩?hù)(x,A,B,C,D)
步驟3:server去cache中查詢(xún)這些用戶(hù)的在線狀態(tài)
步驟4:對(duì)于群中在線的用戶(hù)A與B,群消息server進(jìn)行實(shí)時(shí)推送
步驟5:對(duì)于群中離線的用戶(hù)C與D,群消息server進(jìn)行離線存儲(chǔ)
典型的群離線消息拉取流程,如圖步驟1-3所述:
步驟1:離線消息拉取者C向server拉取群離線消息
步驟2:server從db中拉取離線消息并返回群用戶(hù)C
步驟3:server從db中刪除群用戶(hù)C的群離線消息
存在的問(wèn)題
上述流程是最容易想,也最容易理解的,存在的問(wèn)題也最顯而易見(jiàn):對(duì)于同一份群消息的內(nèi)容,多個(gè)離線用戶(hù)存儲(chǔ)了很多份。假設(shè)群中有200個(gè)用戶(hù)離線,離線消息則冗余了200份,這極大的增加了數(shù)據(jù)庫(kù)的存儲(chǔ)壓力。
【群消息優(yōu)化1:減少存儲(chǔ)量】
為了減少離線消息的冗余度,增加一個(gè)群消息表,用來(lái)存儲(chǔ)所有群消息的內(nèi)容,離線消息表只存儲(chǔ)用戶(hù)的群離線消息msg_id,就能大大的降低數(shù)據(jù)庫(kù)的冗余存儲(chǔ)量
群消息表:用來(lái)存儲(chǔ)一個(gè)群中所有的消息內(nèi)容
t_group_msgs(group_id, sender_id, time,msg_id, msg_detail)
群離線消息表:優(yōu)化后只存儲(chǔ)msg_id
t_offine_msgs(user_id, group_id, msg_id)
這樣優(yōu)化后,群在線消息發(fā)送就做了一些修改:
步驟3:每次發(fā)送在線群消息之前,要先存儲(chǔ)群消息的內(nèi)容
步驟6:每次存儲(chǔ)離線消息時(shí),只存儲(chǔ)msg_id,而不用為每個(gè)用戶(hù)存儲(chǔ)msg_detail
拉取離線消息時(shí)也做了響應(yīng)的修改:
步驟1:先拉取所有的離線消息msg_id
步驟3:再根據(jù)msg_id拉取msg_detail
步驟5:刪除離線msg_id
存在的問(wèn)題
如同單對(duì)單消息的發(fā)送一樣:
(1)在線消息的投遞可能出現(xiàn)消息丟失,例如服務(wù)器重啟,路由器丟包,客戶(hù)端crash
(2)離線消息的拉取也可能出現(xiàn)消息丟失,原因同上
需要和單對(duì)單消息的可靠投遞一樣,加入應(yīng)用層的ACK,才能保證群消息一定到達(dá)。
【群消息優(yōu)化2:應(yīng)用層ACK】
應(yīng)用層ACK優(yōu)化后,群在線消息發(fā)送又發(fā)生了一些變化:
步驟3:在消息msg_detail存儲(chǔ)到群消息表后,不管用戶(hù)是否在線,都先將msg_id存儲(chǔ)到離線消息表里
步驟6:在線的用戶(hù)A和B收到群消息后,需要增加一個(gè)應(yīng)用層ACK,來(lái)標(biāo)識(shí)消息到達(dá)
步驟7:在線的用戶(hù)A和B在應(yīng)用層ACK后,將他們的離線消息msg_id刪除掉
對(duì)應(yīng)到群離線消息的拉取也一樣:
步驟1:先拉取msg_id
步驟3:再拉取msg_detail
步驟5:***應(yīng)用層ACK
步驟6:server收到應(yīng)用層ACK才能刪除離線消息表里的msg_id
存在的問(wèn)題
(1)如果拉取了消息,卻沒(méi)來(lái)得及應(yīng)用層ACK,會(huì)收到重復(fù)的消息么?
回答:會(huì),可以在客戶(hù)端去重,對(duì)于重復(fù)的msg_id,對(duì)用戶(hù)不展現(xiàn),從而不影響用戶(hù)體驗(yàn)
(2)對(duì)于離線的每一條消息,雖然只存儲(chǔ)了msg_id,但是每個(gè)用戶(hù)的每一條離線消息都將在數(shù)據(jù)庫(kù)中保存一條記錄,有沒(méi)有辦法減少離線消息的記錄數(shù)呢?
【群消息優(yōu)化3:離線消息表】
離線消息表的優(yōu)化
其實(shí),對(duì)于一個(gè)群用戶(hù),在ta登出后的離線期間內(nèi),肯定是所有的群消息都沒(méi)有收到的,完全不用對(duì)所有的每一條離線消息存儲(chǔ)一個(gè)離線msg_id,而只需要存儲(chǔ)最近一條拉取到的離線消息的time(或者msg_id),下次登錄時(shí)拉取在那之后的所有群消息即可,而完全沒(méi)有必要存儲(chǔ)每個(gè)人未拉取到的離線消息msg_id
群成員表:用來(lái)描述一個(gè)群里有多少成員,以及每個(gè)成員***一條ack的群消息的msg_id(或者time)
t_group_users(group_id, user_id, last_ack_msg_id(last_ack_msg_time))
群消息表:用來(lái)存儲(chǔ)一個(gè)群中所有的消息內(nèi)容,不變
t_group_msgs(group_id, sender_id, time,msg_id, msg_detail)
群離線消息表:不再需要了
離線消息表優(yōu)化后,群在線消息的投遞流程:
步驟3:在消息msg_detail存儲(chǔ)到群消息表后,不再需要操作離線消息表(優(yōu)化前需要將msg_id插入離線消息表)
步驟7:在線的用戶(hù)A和B在應(yīng)用層ACK后,將last_ack_msg_id更新即可(優(yōu)化前需要將msg_id從離線消息表刪除)
群離線消息的拉取流程也類(lèi)似:
步驟1:拉取離線消息
步驟3:ACK離線消息
步驟4:更新last_ack_msg_id
存在的問(wèn)題
由于“消息風(fēng)暴擴(kuò)散系數(shù)”的存在,假設(shè)1個(gè)群有500個(gè)用戶(hù),“每條”群消息都會(huì)變?yōu)?00個(gè)應(yīng)用層ACK,將對(duì)服務(wù)器造成巨大的沖擊,有沒(méi)有辦法減少ACK請(qǐng)求量呢?
【群消息優(yōu)化4:批量ACK】
由于“消息風(fēng)暴擴(kuò)散系數(shù)”的存在,如果每條群消息都ACK,會(huì)給服務(wù)器造成巨大的沖擊,為了減少ACK請(qǐng)求量,很容易想到的方法是批量ACK。
批量ACK的方式又有兩種:
(1)每收到N條群消息ACK一次,這樣請(qǐng)求量就降低為原來(lái)的1/N了
(2)每隔時(shí)間間隔T進(jìn)行一次群消息ACK,也能達(dá)到類(lèi)似的效果
新的問(wèn)題
批量ACK有可能導(dǎo)致:還沒(méi)有來(lái)得及ACK群消息,用戶(hù)就退出了,這樣下次登錄會(huì)拉取到重復(fù)的離線消息
解決方案
msg_id去重,不對(duì)用戶(hù)展現(xiàn),保證良好的用戶(hù)體驗(yàn)
還可能存在的問(wèn)題
群離線消息過(guò)多:拉取過(guò)慢
解決方案:分頁(yè)拉取(按需拉取),分頁(yè)拉取的細(xì)節(jié)在“微信為啥不丟離線消息”一章中有詳細(xì)敘述,此處不再展開(kāi)(詳見(jiàn)《微信為啥不丟“離線消息”?》)。
【總結(jié)】
群消息還是非常有意思的,可達(dá)性、實(shí)時(shí)性、離線消息、消息風(fēng)暴擴(kuò)散等等等等,做個(gè)總結(jié):
(1)不管是群在線消息,還是群離線消息,應(yīng)用層的ACK是可達(dá)性的保障
(2)群消息只存一份,不用為每個(gè)用戶(hù)存儲(chǔ)離線群msg_id,只需存儲(chǔ)一個(gè)最近ack的群消息id/time
(3)為了減少消息風(fēng)暴,可以批量ACK
(4)如果收到重復(fù)消息,需要msg_id去重,讓用戶(hù)無(wú)感知
(5)離線消息過(guò)多,可以分頁(yè)拉取(按需拉取)優(yōu)化
文章轉(zhuǎn)載自微信公眾號(hào)“架構(gòu)師之路”
網(wǎng)頁(yè)題目:群消息這么復(fù)雜,怎么能做到不丟不重?
地址分享:http://fisionsoft.com.cn/article/ccdjcch.html


咨詢(xún)
建站咨詢(xún)
