微信:我絕不丟離線消息!
《微信:我們絕不丟消息!》提到,單人實時聊天消息的可靠投遞,是通過應用層的超時、重傳、確認、去重來保證的。
那如果沒有打開手機,沒有登錄微信,好友發給我的微信消息,有沒有可能丟失呢?今天和大家聊聊離線消息的話題。
沒有做過IM業務的架構師可能會說,離線消息存儲數據庫不就行了嗎?可事實上,遠比你想的復雜。
接收方不在線,消息發送流程是怎么樣的?
如上圖所述,A給B發了一條消息,而B不在線,離線消息存儲的流程如下:
1. A發送消息給B,通過server中轉;
2. server查看用戶B的狀態為offline;
3. server將消息存儲到DB中;
4. server返回用戶A發送成功,并帶上特殊標識,避免A重發。
離線消息表如何設計?
很容易想到,消息業務有這樣的一些關鍵屬性:
t_offline_msg(
receiver_uid, // 離線消息接收方
msg_id, // 消息ID
time, // 消息發送時間
sender_uid, // 消息發送方
msg_type, // 消息類型
msg_content, // 消息內容
…
);
B登陸之后,如何拉取A發給他的離線消息呢?
(receiver_uid(B), sender_uid(A))
在上述索引查詢,然后把離線消息刪除,再把消息返回B即可。
整體流程如上圖所述:
- B拉取A發送給ta的離線消息;
- server從DB中拉取離線消息;
- server從DB中把離線消息刪除;
- server返回給用戶B想要的離線消息;
想到這一步,也不難。
那么問題來了,B登錄微信的時候,不止要拉取A發給他的離線消息,還需要拉取所有其他好友發給他的離線消息,這該如何實現呢?
如果用戶B有很多好友,登錄后客戶端需要對所有好友進行離線消息拉取。
客戶端偽代碼:
get B's friend-list; // 拉取B的好友列表
for(all uid in B's friend-list){ // 遍歷所有好友uid
get_offline_msg(B,uid); // 拉取離線消息
}
如果有10000個好友,難道要拉取10000次?
畫外音:我的微信好友已滿員,大家猜微信好友上限是多少?
有沒有減少拉取次數的優化方法呢?
按需拉?。合壤「鱾€好友的離線消息數量,真正查看離線消息時,才往服務器發送拉取請求。
除了減少流量的“按需拉取”優化,還有減少拉取次數的優化方案么?
一次性拉?。嚎梢砸淮涡酝ㄟ^receiver_uid即接收方ID,拉取所有好友發送給B的離線消息,把登錄時與服務器的交互次數降低為了1次。到客戶端本地再根據sender_uid進行計算。
問題又來了,用戶B一次性拉取所有好友發給ta的離線消息,消息量很大時,一個請求包很大,速度慢怎么辦?
分頁拉取:根據業務需求,先拉取最新的一頁消息,再按需一頁頁拉取。
新的問題又來了,離線消息會不會丟失,用戶會不會收不到呢?
例如,上述步驟第三步執行完畢之后(刪除了離線消息),第四個步驟離線消息返回給客戶端過程中,服務器掛掉,路由器丟消息,或者客戶端crash了,那離線消息豈不是丟了么。
畫外音:數據庫已刪除,用戶卻還沒看到。
如何保證離線消息的可達性?
加入ACK機制:如同在線消息的應用層ACK機制一樣,離線消息拉時,不能夠直接刪除數據庫中的離線消息,而必須等應用層的離線消息ACK,等客戶端真的收到離線消息,才能刪除數據庫中的離線消息。
新的問題又來了,如果用戶B拉取了一頁離線消息,卻在ACK之前crash了,下次登錄時會拉取到重復的離線消息么?
拉取了離線消息卻沒有ACK,服務器不會刪除之前的離線消息,故下次登錄時系統層面還會拉取到。和在線消息投遞一樣,接收方通過msgid去重,系統層面會收到重復消息,但在業務層面,用戶卻無感知。
另一個問題,假設有N頁離線消息,現在每個離線消息需要一個ACK,那么豈不是客戶端與服務器的交互次數又加倍了?有沒有優化空間?
其實,不用每一頁消息都ACK,在拉取第二頁消息時相當于第一頁消息的ACK,此時服務器再刪除第一頁的離線消息即可,最后一頁消息再ACK一次。這樣的效果是,不管拉取多少頁離線消息,只會多一個ACK請求,與服務器多一次交互。
稍作總結
離線消息的可靠投遞,關鍵技術有:
- 對于同一個用戶B,一次性拉取所有用戶發給ta的離線消息,能大大減少服務器交互次數;
- 按需拉取,是無線端的常見優化;
- 分頁拉取,是一個請求次數與包大小的折衷;
- 應用層的ACK,應用層的去重,才能保證離線消息的不丟不重;
- 下一頁的拉取,同時作為上一頁的ACK,能夠極大減少與服務器的交互次數;
知其然,知其所以然。
思路比結論更重要。