開啟聊天軟體,跟重要的人說個早安,掃一眼關閉了提醒的群訊息。相信這是很多人開始一天的“固定動作”。
隨著移動網際網路和通訊技術的高速發展,線上交流已成為人們工作、生活的最重要方式。沒帶手機出門就像失去了全世界,電量低於 50% 就開始焦慮,因為好友列表裡儲存著我們跟這個世界的聯結。而通過會話列表開啟一個會話,是我們每天最頻繁的動作之一。
IM 會話列表裡會話的準確性、實時性會直接影響使用者的使用體驗與感受。本文分享融雲 IM 即時通訊的會話服務資料讀寫設計思路。(關注 融雲全球網際網路通訊雲,瞭解更多IM & RTC技術、場景話題)
海量訊息會話的技術挑戰
以單聊為例,使用者 A 給使用者 B 傳送訊息,會產生兩條會話記錄:一條傳送者的會話,一條接受者的會話。服務端會把這兩條會話記錄儲存到資料庫中,便於後續服務重啟、更新、查詢使用。
為了減少服務端與客戶端的互動,通常情況下,我們會在會話記錄中儲存最後一條訊息。之後每次 A 給 B 發訊息,或 B 給 A 發訊息都會相應地更新這兩條會話的訊息記錄以及會話的最後時間等。
假如某一時刻有 10 萬個單聊使用者傳送訊息,便會產生 20 萬條會話的新增(第一次聊天)或更新(後續聊天)操作。
在高頻次讀寫的場景下,要提供準確快速的查詢,以及可靠的儲存就很考驗服務在高併發場景下的處理能力了。
高併發下會話的查詢操作
為了提供快速的查詢能力,我們一般會把被頻繁訪問的熱點資料儲存在快取中,比如 Redis 等,以方便系統快速做出響應,而不是每次查詢到資料庫造成資料庫壓力。
為了減少網路互動,降低伺服器壓力和提升使用者體驗,我們可以將熱點資料放到服務記憶體中。當下次使用者查詢時,先去記憶體中查詢是否存在。若存在則直接返回給使用者;若不存在則去資料庫中查詢出來再放到記憶體中快取起來,便於下次查詢直接從記憶體中獲取然後返回給使用者。
(圖 1 會話查詢流程)
在分散式系統架構中,為了提高記憶體中快取資料的命中率,我們一般會採用一致性 Hash 的方式,將一個使用者的所有會話的操作都落到同一個服務例項上。這樣做,不僅利於效能提升,還有助於降低處理分散式讀寫帶來的資料一致性問題的難度。
(圖 2 一致性 Hash 演算法計算服務落點)
高併發下會話的插入更新操作
解決完查詢操作,我們再來看看如何優化寫操作,包含插入、更新以及刪除等。
當有大量資料要新增到資料庫中時,勢必提高資料庫查詢的延時。該如何處理呢?
首先,記憶體大小是有限的,把所有的會話都放在記憶體中是不現實的。我們會把新新增的會話放到 LRU 快取中供查詢使用,然後把要新增的會話寫入到佇列中,通過非同步方式新增到資料庫。此時你可能會想:這也並沒有減少插入資料庫的次數呀,只是放到後面非同步處理了而已。是的,到這一步確實達不到減少運算元據庫次數的效果,且往下看。
(圖 3 非同步化資料落地處理)
假如要新增會話 A,首先會更新到 LRU 中,然後把會話加入到佇列中等待新增到資料庫中。此時佇列中若沒有積壓,則會直接更新到資料庫中;若有積壓,更新到此條會話時先對比 LRU 中的會話,把最新的會話更新到資料庫中,並記錄最新的會話時間。
(圖 4 會話資料存取策略)
如圖 4,從佇列中取出會話 1 進行儲存,此刻佇列中的會話 1 的訊息時間是 1,LRU 中的會話 1 的訊息時間是 4。相比較 LRU 中的會話是最新的,則把 LRU 中的會話入庫,並記錄這條會話的更新時間。當佇列中更新到 time 為 3 這條舊的會話 1 時,由於這條會話的時間比記錄的時間小,則丟棄不入庫。這樣,就可以有效減少高併發積壓的情況下,相同會話頻繁更新導致頻繁入庫的情況,從而達到減小資料庫壓力的目的。
總而言之,融雲的會話服務資料讀寫的設計思路如下:
充分利用記憶體對熱點資料進行快取,減少對後端資料儲存服務的讀取壓力;
通過服務落點計算,提升快取命中率;
通過合併業務資料, 儘量減少無效業務操作,減少對儲存服務的寫入操作;
通過非同步解耦業務與資料寫入流程。
隨著通訊能力成為越來越多場景的基礎需求,高併發場景越來越多,我們也將不斷對服務架構進行迭代,提高併發支援能力,為廣大開發者提供穩定、可靠、低延時的通訊能力支援。