本文作者“商文默”,有修訂和改動(dòng)。
1、寫在前面
即時(shí)通訊網(wǎng)整理的大量IM技術(shù)文章中(見(jiàn)本文末“參考資料”一節(jié)),有關(guān)消息可靠性和一致性問(wèn)題的文章占了很大比重,原因是IM這類系統(tǒng)拋開(kāi)各種眼花繚亂的產(chǎn)品功能和技術(shù)特性,保證消息的可靠性和一致性幾乎是IM產(chǎn)品必需的素質(zhì)。
試想如果一個(gè)IM連發(fā)出的消息都不知道對(duì)方到底能不能收到、發(fā)出的聊天內(nèi)容對(duì)方看到的到底是不是“胡言亂語(yǔ)”(嚴(yán)重亂序問(wèn)題),這樣的APP用戶肯定不會(huì)讓他在手機(jī)上過(guò)夜(肯定第一時(shí)間卸載了),因?yàn)樽罨镜牧奶爝壿嫸紵o(wú)法實(shí)現(xiàn),它已經(jīng)失去了IM軟件本身的意義。
不過(guò),另一個(gè)方面來(lái)講,IM系統(tǒng)是不標(biāo)準(zhǔn)的(雖然曾經(jīng)XMPP這種協(xié)議試圖解決這個(gè)問(wèn)題,但事實(shí)證明那根本不現(xiàn)實(shí)),各家?guī)缀醵际亲砸训乃接袇f(xié)議、不同的實(shí)現(xiàn)邏輯,這也決定了即使同一個(gè)技術(shù)問(wèn)題,對(duì)于IM來(lái)說(shuō)很難有固定的實(shí)現(xiàn)套路和標(biāo)準(zhǔn)的解決方案。
所以,對(duì)于本文來(lái)說(shuō),文中作者雖然提供了有關(guān)IM消息“可靠性”與“一致性”問(wèn)題的解決方案,但方案到底合不合理、適不適合你,這就是仁者見(jiàn)仁、智者見(jiàn)智的事了。用人話說(shuō)就是:本文內(nèi)容僅供參考,具體的解決方案請(qǐng)務(wù)結(jié)合自已的系統(tǒng)構(gòu)架和實(shí)現(xiàn)情況,多閱讀幾篇即時(shí)通訊網(wǎng)上有關(guān)這個(gè)技術(shù)話題的文章,取其精華,找到適合自已的技術(shù)方案和思路才是最明智的。

學(xué)習(xí)交流:
- 即時(shí)通訊/推送技術(shù)開(kāi)發(fā)交流5群:215477170 [推薦]
- 移動(dòng)端IM開(kāi)發(fā)入門文章:《新手入門一篇就夠:從零開(kāi)發(fā)移動(dòng)端IM》
- 開(kāi)源IM框架源碼:https://github.com/JackJiang2011/MobileIMSDK
(本文同步發(fā)布于:http://www.52im.net/thread-3574-1-1.html)
2、本文引言
叢所周之,即時(shí)通訊聊天(IM)系統(tǒng)必需要解決消息可靠性及消息一致性問(wèn)題(PS:如果具體IM系統(tǒng)是什么你都還沒(méi)弄明白,先讀這篇《零基礎(chǔ)IM開(kāi)發(fā)入門(一):什么是IM系統(tǒng)?》)。
這兩個(gè)問(wèn)題,通俗來(lái)說(shuō)就是:
- 1)消息可靠性:簡(jiǎn)單來(lái)說(shuō)就是不丟消息,會(huì)話一方發(fā)送消息,消息成功到達(dá)對(duì)方并正確顯示;
- 2)消息一致性:包括發(fā)送一方消息一致及會(huì)話雙方消息一致,要求消息不重復(fù),不亂序。
本文會(huì)從典型的IM消息發(fā)送邏輯開(kāi)始,簡(jiǎn)單易懂地闡明消息可靠性、一致性問(wèn)題的原理及可參考的技術(shù)解決方法,或許技術(shù)方案并不完美,但希望能為你的IM技術(shù)問(wèn)題解決帶來(lái)啟發(fā)。
3、典型IM消息發(fā)送過(guò)程
IM的消息發(fā)送一般的實(shí)現(xiàn)過(guò)程可以分為兩個(gè)階段:
- 1)發(fā)送方發(fā)送消息、服務(wù)端接收、返回消息 ACK 給發(fā)送方;
- 2)服務(wù)端將消息推送到接收方。
判斷消息發(fā)送是否成功主要依據(jù)第一階段——即服務(wù)器是否接受到消息。
對(duì)于消息發(fā)送者來(lái)說(shuō),消息狀態(tài)可以分為三類:
- 1)正在發(fā)送;
- 2)發(fā)送成功;
- 3)發(fā)送失敗。
具體來(lái)說(shuō),這三類狀態(tài)的具體意義是:
- 1)正在發(fā)送:發(fā)送方觸發(fā)發(fā)送事件開(kāi)始,到收到服務(wù)端返回消息對(duì)應(yīng) ACK 之前;
- 2)發(fā)送成功:發(fā)送方收到消息對(duì)應(yīng) ACK 回復(fù);
- 3)發(fā)送失敗:超過(guò)一定重發(fā)次數(shù),未收到消息對(duì)應(yīng) ACK 回復(fù)。
對(duì)應(yīng)的消息發(fā)送流程如下圖所示:

4、IM消息可靠性
限于篇幅,對(duì)于IM消息可靠性的基本概念和詳細(xì)原理建議閱讀《零基礎(chǔ)IM開(kāi)發(fā)入門(三):什么是IM系統(tǒng)的可靠性?》,本文著重談?wù)劷鉀Q思路。
4.1 重發(fā)機(jī)制
保證消息發(fā)送第一階段(見(jiàn)本文“3、典型IM消息發(fā)送過(guò)程”一節(jié))消息成功發(fā)送的方法是設(shè)立重發(fā)機(jī)制:
- 1)依據(jù)一定時(shí)長(zhǎng)內(nèi)是否收到消息對(duì)應(yīng) ACK,判斷消息是否要重發(fā);
- 2)如果超過(guò)預(yù)設(shè)時(shí)長(zhǎng),就重新發(fā)送;
- 3)當(dāng)重發(fā)次數(shù)超過(guò)預(yù)設(shè)次數(shù),就不再重發(fā),判定該消息發(fā)送失敗,修改消息發(fā)送狀態(tài)。
PS:具體的完整方案級(jí)代碼實(shí)現(xiàn),可以參考MobileIMSDK 中有關(guān)QoS機(jī)制的代碼實(shí)現(xiàn)。
4.2 會(huì)話記錄檢查
消息發(fā)送第二階段(見(jiàn)本文“3、典型IM消息發(fā)送過(guò)程”一節(jié))服務(wù)端推送消息到接收方,如果連接斷開(kāi),會(huì)丟失消息。
所以要保證消息完整,就需要在建立連接后,根據(jù)上一條消息(已經(jīng) ACK)時(shí)間戳,獲取會(huì)話記錄,一次返回一段時(shí)間內(nèi)所有消息(PS:中大型應(yīng)用中,消息的拉取也不是個(gè)簡(jiǎn)單事情,詳情可以閱讀《IM開(kāi)發(fā)干貨分享:如何優(yōu)雅的實(shí)現(xiàn)大量離線消息的可靠投遞》)。
另一種保證方法是加入定時(shí)輪詢,檢查消息完整性,具體的思路如下圖所示。
建立連接流程圖:

4.3 需要考慮的兩個(gè)問(wèn)題
消息重發(fā)、會(huì)話記錄檢查需要考慮兩個(gè)問(wèn)題:
- 1)消息是否會(huì)重復(fù)發(fā)送;
- 2)消息順序是否會(huì)被打亂。
舉兩個(gè)例子。
關(guān)于消息重發(fā)問(wèn)題:
- 1)如果丟消息的點(diǎn)在消息達(dá)到服務(wù)端之前,服務(wù)端并沒(méi)有收到消息,發(fā)送方重新發(fā)送丟失消息,服務(wù)端接收成功,不會(huì)產(chǎn)生兩條相同消息;
- 2)而如果服務(wù)端接收到消息,返回 ACK 丟失,這時(shí)再發(fā)送一次相同消息,就可能造成消息重復(fù)。
關(guān)于消息順序問(wèn)題:
- 1)如果發(fā)送方連發(fā)三條消息,第一、第三條成功被服務(wù)端接收,第二條丟了,那第三條消息是否會(huì)被記錄?
- 2)如果這時(shí)第二條消息達(dá)到服務(wù)端,其順序是在第三條時(shí)間之前還是之后(服務(wù)端一般都會(huì)給記錄打一個(gè)時(shí)間戳)?
5、IM消息一致性
同上節(jié)一樣,對(duì)于IM消息一致性的基本概念和詳細(xì)原理建議閱讀《零基礎(chǔ)IM開(kāi)發(fā)入門(四):什么是IM系統(tǒng)的消息時(shí)序一致性?》。
5.1 使用 uuid 消息去重
對(duì)于消息重發(fā)問(wèn)題,可以給每條消息增加屬性 uuid 作為消息唯一標(biāo)識(shí),重發(fā)消息 uuid 不變,前端根據(jù) uuid 去重。大致思路就是這樣。
PS:對(duì)于IM來(lái)說(shuō),消息ID也是個(gè)很大的技術(shù)話題,有興趣可以讀下面這個(gè)系列:
《IM消息ID技術(shù)專題(一):微信的海量IM聊天消息序列號(hào)生成實(shí)踐(算法原理篇)》
《IM消息ID技術(shù)專題(二):微信的海量IM聊天消息序列號(hào)生成實(shí)踐(容災(zāi)方案篇)》
《IM消息ID技術(shù)專題(三):解密融云IM產(chǎn)品的聊天消息ID生成策略》
《IM消息ID技術(shù)專題(四):深度解密美團(tuán)的分布式ID生成算法》
《IM消息ID技術(shù)專題(五):開(kāi)源分布式ID生成器UidGenerator的技術(shù)實(shí)現(xiàn)》
5.2 使用向量時(shí)鐘進(jìn)行消息排序
對(duì)于消息排序問(wèn)題:因?yàn)樵诹奶熘校⒌捻樞驅(qū)τ诎l(fā)送方的表述有重要的影響,消息不完整或順序顛倒都可能造成語(yǔ)意不連貫,甚至曲解。所以需要保證發(fā)送方發(fā)送消息順序,而會(huì)話雙方消息排序需要考慮實(shí)際情況。
在一般的認(rèn)知里:狀態(tài)是正在發(fā)送的消息,應(yīng)該還沒(méi)有被對(duì)方看到,只有發(fā)送成功的消息,才會(huì)被對(duì)方看到。但在實(shí)現(xiàn)中,消息發(fā)送成功是以服務(wù)器接收消息并返回 ACK 成功為判斷依據(jù),而不是被對(duì)方接收到。
那么就會(huì)出現(xiàn)這樣一個(gè)問(wèn)題:如果一條消息狀態(tài)是正在發(fā)送,此時(shí)收到一條消息,那么收到的消息是在正在發(fā)送的消息之前還是之后?
這是一個(gè)上下文關(guān)系,關(guān)鍵問(wèn)題是:發(fā)送方是以哪條所見(jiàn)消息為依據(jù)發(fā)送消息的。
這里提供一種思路:借鑒分布式系統(tǒng)中的向量時(shí)鐘算法(見(jiàn)《分布式系統(tǒng)中的向量時(shí)鐘算法》)。
先簡(jiǎn)單描述向量時(shí)鐘算法:
向量時(shí)鐘算法用于在分布式系統(tǒng)中生成事件偏序關(guān)系,并糾正因果關(guān)系。一個(gè)系統(tǒng)包含 N 個(gè)節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)產(chǎn)生的消息體中包含該節(jié)點(diǎn)的邏輯時(shí)鐘,整體系統(tǒng)的向量時(shí)鐘由 N 維邏輯時(shí)鐘組成,并在每個(gè)節(jié)點(diǎn)產(chǎn)生的消息體中傳遞。
簡(jiǎn)單來(lái)說(shuō),向量時(shí)鐘算法的實(shí)現(xiàn)原理如下:
- 1)初始狀態(tài),向量值為 0;
- 2)每次節(jié)點(diǎn)處理完節(jié)點(diǎn)事件,該節(jié)點(diǎn)時(shí)鐘+1;
- 3)每次節(jié)點(diǎn)發(fā)送消息,將包含自身時(shí)鐘的系統(tǒng)向量時(shí)鐘一起發(fā)送;
- 4)每次節(jié)點(diǎn)收到消息,更新向量時(shí)鐘,該節(jié)點(diǎn)時(shí)鐘+1,其他節(jié)點(diǎn)對(duì)比每個(gè)節(jié)點(diǎn)本地保留的向量時(shí)鐘值和消息體中向量時(shí)鐘值,取最大值;
- 5)節(jié)點(diǎn)同時(shí)收到多條消息,判斷接收消息的向量時(shí)鐘之間是否存在偏序關(guān)系。
針對(duì)上述的第5)點(diǎn):
- 1)如果存在偏序關(guān)系,則合并向量時(shí)鐘,取偏序較大的向量時(shí)鐘;
- 2)如果不存在偏序關(guān)系,則不能合并。
偏序關(guān)系:如果 A 向量中的每一維都大于等于 B 向量,則 A、B 之間存在偏序關(guān)系,否則不存在偏序關(guān)系。
對(duì)于IM為聊天消息排序來(lái)說(shuō),其實(shí)就是處理聊天消息的上下文語(yǔ)境,決定消息之間的因果關(guān)系。
參考向量時(shí)鐘算法:假設(shè)有 N 個(gè)消息會(huì)話方,系統(tǒng)的向量時(shí)鐘由 N 維時(shí)鐘組成,向量時(shí)鐘在各方發(fā)送的消息體中傳遞,并依據(jù)向量時(shí)鐘排序。
具體實(shí)現(xiàn)思路如下:
- 1)系統(tǒng)向量時(shí)鐘設(shè)為 (0, 0, …, N);
- 2)節(jié)點(diǎn)發(fā)送消息,更新系統(tǒng)向量時(shí)鐘,該節(jié)點(diǎn)時(shí)鐘加一,其他節(jié)點(diǎn)不變;
- 3)節(jié)點(diǎn)接收消息,更新系統(tǒng)向量時(shí)鐘,該節(jié)點(diǎn)時(shí)鐘加一;其他節(jié)點(diǎn)對(duì)比每個(gè)節(jié)點(diǎn)本地保留的向量時(shí)鐘的值和消息中向量時(shí)鐘的值,取最大值;
- 4)依據(jù)消息體內(nèi)系統(tǒng)向量時(shí)鐘的偏序關(guān)系決定消息順序。
針對(duì)上述第4)點(diǎn):
- 1)如果可以確定偏序關(guān)系,則根據(jù)偏序關(guān)系由小到大顯示;
- 2)如果多條消息不能確定偏序關(guān)系,則按照自然順序(接收到的順序)顯示。
向量時(shí)鐘在理論上可以解決大部分消息一致性的問(wèn)題,但在實(shí)現(xiàn)中還需要考慮實(shí)際使用時(shí)的體驗(yàn)。
這其中最需要關(guān)注的問(wèn)題是:是否要強(qiáng)制排序,或者說(shuō),如果實(shí)際顯示順序和向量時(shí)鐘之間的偏序關(guān)系不一致,是否要移動(dòng)消息之間的順序。
舉個(gè)例子:在一個(gè)有多人的會(huì)話中,如果有一方網(wǎng)速特別慢,收不到消息,也發(fā)不出消息。在他看到的最后的消息之后,其他人已經(jīng)開(kāi)始新的話題,這時(shí)他關(guān)于上一個(gè)話題的消息終于發(fā)送成功,并被其他人收到。
此時(shí)就存在這樣一個(gè)問(wèn)題:這條關(guān)于上一個(gè)話題的消息是顯示在最后,還是移到較早時(shí)間?
- 1)如果顯示在最后,但消息內(nèi)容和目前的話題不相關(guān),其他人可能會(huì)感到莫名其妙;
- 2)如果把消息移到較早時(shí)間,那么這條消息可能不會(huì)被其他人看到,或者看到前面多了一條消息,會(huì)有種突兀的感覺(jué)。
IM 的場(chǎng)景很多,也很復(fù)雜,更多的時(shí)候需要從產(chǎn)品角度考慮問(wèn)題。
對(duì)于消息是否需要排序的問(wèn)題,這里只提出一個(gè)比較通用的方案:建議會(huì)話中不強(qiáng)制排序,會(huì)話歷史記錄中按照向量時(shí)鐘的偏序關(guān)系進(jìn)行排序。
6、本文小結(jié)
對(duì)于 IM 系統(tǒng)消息可靠性及一致性問(wèn)題,通過(guò)消息重發(fā)機(jī)制保證消息成功被服務(wù)端接收,通過(guò)會(huì)話記錄檢查保證收取消息完整,從而保證整個(gè)消息發(fā)送過(guò)程的可靠性。使用 uuid 消息去重,參考向量時(shí)鐘算法進(jìn)行消息排序,為保證消息一致性提供一種解決方案。
總之,IM這類系統(tǒng)看似簡(jiǎn)單,實(shí)則水深似海,如果你是IM開(kāi)發(fā)新手,可以從《新手入門一篇就夠:從零開(kāi)發(fā)移動(dòng)端IM》這篇入手系統(tǒng)學(xué)習(xí)。如果你自認(rèn)為已是IM老手,這里整理的 IM中大型架構(gòu)設(shè)計(jì) 方面的文章或許可以參考一下。
7、參考資料
[1] 零基礎(chǔ)IM開(kāi)發(fā)入門(三):什么是IM系統(tǒng)的可靠性?
[2] 零基礎(chǔ)IM開(kāi)發(fā)入門(四):什么是IM系統(tǒng)的消息時(shí)序一致性?
[3] IM消息送達(dá)保證機(jī)制實(shí)現(xiàn)(一):保證在線實(shí)時(shí)消息的可靠投遞
[4] IM消息送達(dá)保證機(jī)制實(shí)現(xiàn)(二):保證離線消息的可靠投遞
[5] 如何保證IM實(shí)時(shí)消息的“時(shí)序性”與“一致性”?
[6] 一個(gè)低成本確保IM消息時(shí)序的方法探討
[8] 完全自已開(kāi)發(fā)的IM該如何設(shè)計(jì)“失敗重試”機(jī)制?
[9] IM開(kāi)發(fā)干貨分享:如何優(yōu)雅的實(shí)現(xiàn)大量離線消息的可靠投遞
[10] 從客戶端的角度來(lái)談?wù)勔苿?dòng)端IM的消息可靠性和送達(dá)機(jī)制
[11] 一套億級(jí)用戶的IM架構(gòu)技術(shù)干貨(下篇):可靠性、有序性、弱網(wǎng)優(yōu)化等
本文已同步發(fā)布于“即時(shí)通訊技術(shù)圈”公眾號(hào)。
▲ 本文在公眾號(hào)上的鏈接是:點(diǎn)此進(jìn)入。同步發(fā)布鏈接是:http://www.52im.net/thread-3574-1-1.html
作者:Jack Jiang (點(diǎn)擊作者姓名進(jìn)入Github)
出處:http://www.52im.net/space-uid-1.html
交流:歡迎加入即時(shí)通訊開(kāi)發(fā)交流群 215891622
討論:http://www.52im.net/
Jack Jiang同時(shí)是【原創(chuàng)Java
Swing外觀工程BeautyEye】和【輕量級(jí)移動(dòng)端即時(shí)通訊框架MobileIMSDK】的作者,可前往下載交流。
本博文
歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明出處(也可前往 我的52im.net 找到我)。