一、什么是消息一致性
消息一致性指的是消息的時序一致性,即消息收發的一致性。如果不能保證時序一致性,就會造成聊天語義不連貫,引起誤會。
對於點對點的聊天場景,時序一致性保證接收方的接收順序和發送方的發出順序一致;對於群聊場景,時序一致性保證所有接收人看到的消息展現順序一致。
二、消息一致性的難點
1.多發送方、多接收方、服務端多線程並發處理情況下,無法保證時序一致性。
2.分布式環境下,多個機器的本地時鍾不一致,沒有“全局時鍾”,不能用“本地時間”保證時序的一致性。
三、消息的一致性的實現
▶1. “全局序號生成器”作為“時序基准”
為什么不以客戶端(發送方)的本地序號/本地時鍾作為“時序基准”?
如果以發送方的本地時鍾作為時序基准,發送方發送消息時,將消息本身和本地的時間戳或一個本地維護的序號發送到IM服務端,IM服務端再將這個消息和時間戳或序號發送給消息接收方,消息接收方根據這個時間戳或序號進行排序。
若發送方隨時調整時鍾,會導致時間戳回退;若發送方重裝應用,會導致序號清零,從而回退。
而針對多發送方場景,如群聊和多點登錄,存在同一時鍾的某個時間點、多條消息發送給同一接收對象的可能。比如一個群聊內,用戶A先發言、用戶B后發言,但如果用戶A的時鍾比用戶B的時鍾慢,已發送方的本地時鍾作為“時序基准”就會出問題;再比如微信在手機、電腦上同時登錄,兩台設備可以給某一接收方發送消息;若設備的本地時鍾不一致,接收方可能會出現消息不連貫的問題。
為什么不以服務器的本地時鍾作為“時序基准”?
如果以IM服務器的本地時鍾作為時序基准,發送方將消息發送到IM服務端,IM服務端依據自身服務器的時鍾生成一個時間戳,然后將消息和時間戳一起發送給消息接收方,消息接收方根據這個時間戳進行排序。
一旦IM服務集群化部署,就會出現多台服務器本地時鍾不一致的問題。雖然多台服務器可以通過NTP時間同步服務,但依然存在一定的時間誤差。
將“全局序號生成器”作為“時序基准”,可以解決每條消息沒有標准“生產日期”的問題,按着實現方式可以分為兩類,一是支持單調自增序號的生成,如Redis的原子自增命令incr、MySQL的自增ID,二是分布式時間相關的ID生成,如snowflake算法、時間相關的分布式序號生成服務等。
對於群聊和多點登錄的場景,只需要保證一個群的消息有序即可,無需全局的跨多個群的絕對時序性。每個群聊有其獨立的“ID生成器”,可以通過哈希規則路由到對應的主庫實例上,降低多個群聊共用一個“ID生成器”的壓力。
▶2. 消息整流
當 IM 服務端接收到消息后,若 IM 服務器是集群化部署,可能因為服務器性能的差異,導致后收到的消息先發出去;又或者多線程處理消息的流程不能保證先到達的消息先發送出去,從而使接收方收到的消息順序有誤,因此需要消息整流。消息整流又分為服務端包內整流和客戶端(接收方)整流。
-
服務端包內整流
比如實現離線推送,當用戶上線,網關機會通知業務層用戶已上線,業務層就會把該用戶的多條離線消息 pub 給這個網關機的 Topic,網關機再把收到的多條消息通過長連接推送給用戶。
1)生產者為每個消息包生成一個packageID,為包內的每條消息加個有序自增的seqID(注:這里的seqID和推送時的SeqID不是一個概念,這是的seqID只是當前這個包的序號,推送時不用看這個);
2)消費者根據每條消息的packageID 和 seqID 進行整流和排序;
3)執行模塊只有在一定超時時間內完整有序地收到所有消息才執行最終操作,否則根據業務需要觸發重試或直接放棄操作。
-
消息接收端整流
1)發送方發送消息時,連同消息和序號一起發送給接收方;
2)接收方接收到消息后,先去查找上一條消息的序號,然后比對收到的序號和上一條消息的序號;
3)如果收到的消息序號大於上一條消息序號,直接追加;反之則去查找小於該序號的最大消息序號,並追加到其后。
四、參考
消息時序一致性還可以參考58沈劍大佬的文章:消息“時序”與“一致性”為何這么難?,又會有不一樣的收獲。