本文作者“商文默”,有修訂和改動。
1、寫在前面
即時通訊網整理的大量IM技術文章中(見本文末“參考資料”一節),有關消息可靠性和一致性問題的文章占了很大比重,原因是IM這類系統拋開各種眼花繚亂的產品功能和技術特性,保證消息的可靠性和一致性幾乎是IM產品必需的素質。
試想如果一個IM連發出的消息都不知道對方到底能不能收到、發出的聊天內容對方看到的到底是不是“胡言亂語”(嚴重亂序問題),這樣的APP用戶肯定不會讓他在手機上過夜(肯定第一時間卸載了),因為最基本的聊天邏輯都無法實現,它已經失去了IM軟件本身的意義。
不過,另一個方面來講,IM系統是不標準的(雖然曾經XMPP這種協議試圖解決這個問題,但事實證明那根本不現實),各家幾乎都是自已的私有協議、不同的實現邏輯,這也決定了即使同一個技術問題,對于IM來說很難有固定的實現套路和標準的解決方案。
所以,對于本文來說,文中作者雖然提供了有關IM消息“可靠性”與“一致性”問題的解決方案,但方案到底合不合理、適不適合你,這就是仁者見仁、智者見智的事了。用人話說就是:本文內容僅供參考,具體的解決方案請務結合自已的系統構架和實現情況,多閱讀幾篇即時通訊網上有關這個技術話題的文章,取其精華,找到適合自已的技術方案和思路才是最明智的。
(本文同步發布于:http://www.52im.net/thread-3574-1-1.html)
2、本文引言
叢所周之,即時通訊聊天(IM)系統必需要解決消息可靠性及消息一致性問題(PS:如果具體IM系統是什么你都還沒弄明白,先讀這篇《零基礎IM開發入門(一):什么是IM系統?》)。
這兩個問題,通俗來說就是:
- 1)消息可靠性:簡單來說就是不丟消息,會話一方發送消息,消息成功到達對方并正確顯示;
- 2)消息一致性:包括發送一方消息一致及會話雙方消息一致,要求消息不重復,不亂序。
本文會從典型的IM消息發送邏輯開始,簡單易懂地闡明消息可靠性、一致性問題的原理及可參考的技術解決方法,或許技術方案并不完美,但希望能為你的IM技術問題解決帶來啟發。
3、典型IM消息發送過程
IM的消息發送一般的實現過程可以分為兩個階段:
- 1)發送方發送消息、服務端接收、返回消息 ACK 給發送方;
- 2)服務端將消息推送到接收方。
判斷消息發送是否成功主要依據第一階段——即服務器是否接受到消息。
對于消息發送者來說,消息狀態可以分為三類:
具體來說,這三類狀態的具體意義是:
- 1)正在發送:發送方觸發發送事件開始,到收到服務端返回消息對應 ACK 之前;
- 2)發送成功:發送方收到消息對應 ACK 回復;
- 3)發送失?。撼^一定重發次數,未收到消息對應 ACK 回復。
對應的消息發送流程如下圖所示:
4、IM消息可靠性
限于篇幅,對于IM消息可靠性的基本概念和詳細原理建議閱讀《零基礎IM開發入門(三):什么是IM系統的可靠性?》,本文著重談談解決思路。
4.1 重發機制
保證消息發送第一階段(見本文“3、典型IM消息發送過程”一節)消息成功發送的方法是設立重發機制:
- 1)依據一定時長內是否收到消息對應 ACK,判斷消息是否要重發;
- 2)如果超過預設時長,就重新發送;
- 3)當重發次數超過預設次數,就不再重發,判定該消息發送失敗,修改消息發送狀態。
PS:具體的完整方案級代碼實現,可以參考MobileIMSDK 中有關QoS機制的代碼實現。
4.2 會話記錄檢查
消息發送第二階段(見本文“3、典型IM消息發送過程”一節)服務端推送消息到接收方,如果連接斷開,會丟失消息。
所以要保證消息完整,就需要在建立連接后,根據上一條消息(已經 ACK)時間戳,獲取會話記錄,一次返回一段時間內所有消息(PS:中大型應用中,消息的拉取也不是個簡單事情,詳情可以閱讀《IM開發干貨分享:如何優雅的實現大量離線消息的可靠投遞》)。
另一種保證方法是加入定時輪詢,檢查消息完整性,具體的思路如下圖所示。
建立連接流程圖:
4.3 需要考慮的兩個問題
消息重發、會話記錄檢查需要考慮兩個問題:
- 1)消息是否會重復發送;
- 2)消息順序是否會被打亂。
舉兩個例子。
關于消息重發問題:
- 1)如果丟消息的點在消息達到服務端之前,服務端并沒有收到消息,發送方重新發送丟失消息,服務端接收成功,不會產生兩條相同消息;
- 2)而如果服務端接收到消息,返回 ACK 丟失,這時再發送一次相同消息,就可能造成消息重復。
關于消息順序問題:
- 1)如果發送方連發三條消息,第一、第三條成功被服務端接收,第二條丟了,那第三條消息是否會被記錄?
- 2)如果這時第二條消息達到服務端,其順序是在第三條時間之前還是之后(服務端一般都會給記錄打一個時間戳)?
5、IM消息一致性
同上節一樣,對于IM消息一致性的基本概念和詳細原理建議閱讀《零基礎IM開發入門(四):什么是IM系統的消息時序一致性?》。
5.1 使用 uuid 消息去重
對于消息重發問題,可以給每條消息增加屬性 uuid 作為消息唯一標識,重發消息 uuid 不變,前端根據 uuid 去重。大致思路就是這樣。
PS:對于IM來說,消息ID也是個很大的技術話題,有興趣可以讀下面這個系列:
《IM消息ID技術專題(一):微信的海量IM聊天消息序列號生成實踐(算法原理篇)》
《IM消息ID技術專題(二):微信的海量IM聊天消息序列號生成實踐(容災方案篇)》
《IM消息ID技術專題(三):解密融云IM產品的聊天消息ID生成策略》
《IM消息ID技術專題(四):深度解密美團的分布式ID生成算法》
《IM消息ID技術專題(五):開源分布式ID生成器UidGenerator的技術實現》
《IM消息ID技術專題(六):深度解密滴滴的高性能ID生成器(Tinyid)》
5.2 使用向量時鐘進行消息排序
對于消息排序問題:因為在聊天中,消息的順序對于發送方的表述有重要的影響,消息不完整或順序顛倒都可能造成語意不連貫,甚至曲解。所以需要保證發送方發送消息順序,而會話雙方消息排序需要考慮實際情況。
在一般的認知里:狀態是正在發送的消息,應該還沒有被對方看到,只有發送成功的消息,才會被對方看到。但在實現中,消息發送成功是以服務器接收消息并返回 ACK 成功為判斷依據,而不是被對方接收到。
那么就會出現這樣一個問題:如果一條消息狀態是正在發送,此時收到一條消息,那么收到的消息是在正在發送的消息之前還是之后?
這是一個上下文關系,關鍵問題是:發送方是以哪條所見消息為依據發送消息的。
這里提供一種思路:借鑒分布式系統中的向量時鐘算法(見《分布式系統中的向量時鐘算法》)。
先簡單描述向量時鐘算法:
向量時鐘算法用于在分布式系統中生成事件偏序關系,并糾正因果關系。一個系統包含 N 個節點,每個節點產生的消息體中包含該節點的邏輯時鐘,整體系統的向量時鐘由 N 維邏輯時鐘組成,并在每個節點產生的消息體中傳遞。
簡單來說,向量時鐘算法的實現原理如下:
- 1)初始狀態,向量值為 0;
- 2)每次節點處理完節點事件,該節點時鐘+1;
- 3)每次節點發送消息,將包含自身時鐘的系統向量時鐘一起發送;
- 4)每次節點收到消息,更新向量時鐘,該節點時鐘+1,其他節點對比每個節點本地保留的向量時鐘值和消息體中向量時鐘值,取最大值;
- 5)節點同時收到多條消息,判斷接收消息的向量時鐘之間是否存在偏序關系。
針對上述的第5)點:
- 1)如果存在偏序關系,則合并向量時鐘,取偏序較大的向量時鐘;
- 2)如果不存在偏序關系,則不能合并。
偏序關系:如果 A 向量中的每一維都大于等于 B 向量,則 A、B 之間存在偏序關系,否則不存在偏序關系。
對于IM為聊天消息排序來說,其實就是處理聊天消息的上下文語境,決定消息之間的因果關系。
參考向量時鐘算法:假設有 N 個消息會話方,系統的向量時鐘由 N 維時鐘組成,向量時鐘在各方發送的消息體中傳遞,并依據向量時鐘排序。
具體實現思路如下:
- 1)系統向量時鐘設為 (0, 0, …, N);
- 2)節點發送消息,更新系統向量時鐘,該節點時鐘加一,其他節點不變;
- 3)節點接收消息,更新系統向量時鐘,該節點時鐘加一;其他節點對比每個節點本地保留的向量時鐘的值和消息中向量時鐘的值,取最大值;
- 4)依據消息體內系統向量時鐘的偏序關系決定消息順序。
針對上述第4)點:
- 1)如果可以確定偏序關系,則根據偏序關系由小到大顯示;
- 2)如果多條消息不能確定偏序關系,則按照自然順序(接收到的順序)顯示。
向量時鐘在理論上可以解決大部分消息一致性的問題,但在實現中還需要考慮實際使用時的體驗。
這其中最需要關注的問題是:是否要強制排序,或者說,如果實際顯示順序和向量時鐘之間的偏序關系不一致,是否要移動消息之間的順序。
舉個例子:在一個有多人的會話中,如果有一方網速特別慢,收不到消息,也發不出消息。在他看到的最后的消息之后,其他人已經開始新的話題,這時他關于上一個話題的消息終于發送成功,并被其他人收到。
此時就存在這樣一個問題:這條關于上一個話題的消息是顯示在最后,還是移到較早時間?
- 1)如果顯示在最后,但消息內容和目前的話題不相關,其他人可能會感到莫名其妙;
- 2)如果把消息移到較早時間,那么這條消息可能不會被其他人看到,或者看到前面多了一條消息,會有種突兀的感覺。
IM 的場景很多,也很復雜,更多的時候需要從產品角度考慮問題。
對于消息是否需要排序的問題,這里只提出一個比較通用的方案:建議會話中不強制排序,會話歷史記錄中按照向量時鐘的偏序關系進行排序。
6、本文小結
對于 IM 系統消息可靠性及一致性問題,通過消息重發機制保證消息成功被服務端接收,通過會話記錄檢查保證收取消息完整,從而保證整個消息發送過程的可靠性。使用 uuid 消息去重,參考向量時鐘算法進行消息排序,為保證消息一致性提供一種解決方案。
總之,IM這類系統看似簡單,實則水深似海,如果你是IM開發新手,可以從《新手入門一篇就夠:從零開發移動端IM》這篇入手系統學習。如果你自認為已是IM老手,這里整理的 IM中大型架構設計 方面的文章或許可以參考一下。
7、參考資料
[1] 零基礎IM開發入門(三):什么是IM系統的可靠性?
[2] 零基礎IM開發入門(四):什么是IM系統的消息時序一致性?
[3] IM消息送達保證機制實現(一):保證在線實時消息的可靠投遞
[4] IM消息送達保證機制實現(二):保證離線消息的可靠投遞
[5] 如何保證IM實時消息的“時序性”與“一致性”?
[6] 一個低成本確保IM消息時序的方法探討
[7] IM群聊消息如此復雜,如何保證不丟不重?
[8] 完全自已開發的IM該如何設計“失敗重試”機制?
[9] IM開發干貨分享:如何優雅的實現大量離線消息的可靠投遞
[10] 從客戶端的角度來談談移動端IM的消息可靠性和送達機制
[11] 一套億級用戶的IM架構技術干貨(下篇):可靠性、有序性、弱網優化等
[12] 從新手到專家:如何設計一套億級消息量的分布式IM系統
本文已同步發布于“即時通訊技術圈”公眾號。

▲ 本文在公眾號上的鏈接是:點此進入。同步發布鏈接是:http://www.52im.net/thread-3574-1-1.html