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