- 單聊
- 一. 功能點拆分
- 二. 數據結構
- 三. 架構層級拆分
- 四. 推拉模式選擇
- 五. 消息流轉
- 小結
![](https://news.xinpengboligang.com/upload/keji/00fcad4c66a7ab993fb9631c46bf3e2c.jpeg)
單聊
在眾多的軟件中,聊天功能是不可或缺的一個功能模塊,或是用戶和用戶,或是用戶和客服,都需要一個能夠即時溝通的功能。
那麼一個IM(InstantMessaging)的1對1聊天系統架構和存儲應該如何設計呢。
下面來一步步的分析規劃。
一. 功能點拆分
首先來看一個IM軟件模塊包括哪些基本功能
- 會話列表(需要按照最後一條消息時間的倒序,將會話進行排列)
- 聊天內容頁(單聊雙方的消息按時間順序依次排列)
- 未讀消息計數(發送了但是沒有讀取的對話,需要在頭像旁顯示未讀數字)
- 用戶頭像,昵稱(對話的用戶資料)
根據上述功能點拆分後,可以確定下來需要哪些數據存儲
- 會話列表
- 聊天的消息記錄
- 離線消息列表
- 未讀消息數據數量
- 用戶資料
二. 數據結構
實際進行下面幾種數據結構存儲時,可使用適合自己的場景的組件,例如公司自研的,或熟悉並滿足場景要求的。
以下我拿redis或mysql來舉例子,提供一個思路,實際生產環境還需要具體設計和選型
1. 會話列表
首先,需要為每一個會話創建一個會話Id進行標識。
再來看,會話列表的特性是新來消息的會話需要排在列表的上面,那麼就可以使用一個有序集合SortedSet來存儲。
結構如下:
key: prefix_xxx:{uid} value: {會話Id} score: {msgId}
key使用當前用戶的uid來標識,集合中的每個item則是會話的Id,item的score為會話的最後一條消息的Id,這樣根據score自動形成一個有序集合後,就能夠滿足我們的應用場景了。
2. 單聊消息列表
場景:聊天的消息列表,是一個按照時間順序來排列的消息記錄,並且需要可以根據offset來進行數據拉取。
同樣可以使用redis的有序集合SortedSet來存儲會話的消息列表,通過scan拉取消息
key: prefix_session_list:{sessionId} value: {msgId} score: {msgId}
也可以創建一個Mysql數據表來持久化存儲消息記錄
create table t_msg_record_list (
`id` bigint not null primary key,
`sessionId` bigint not null comment '會話Id',
`msgId` bigint not null comment '消息Id',
`isRead` tinyint not null default 0 commment '已讀狀態',
`recordStatus` smallint not null default 0 commment '消息狀態',
`createTime` datetime not null,
key `sessionId` (`sessionId`)
)engine=innodb;
根據會話Id分頁查詢時,就可以這樣查詢出所有msgId,再根據msgId去拉取msg的詳情,組合成列表返回給客戶端
SELECT msgId FROM t_msg_record_list WHERE sessionId = 1 AND recordStatus = 0 AND msgId > 1 ORDER BY id desc LIMIT 10;
3. 離線消息
離線消息可以分為「索引」和「消息id列表」兩部分
離線消息索引需要記錄的是,哪些用戶給當前用戶發送了離線消息,所以我們可以使用redis的集合Set來記錄這些信息
key: prefix_xxx:{uid} value: {senderUid}
通過scan離線消息索引拿到了sendUid,再去拿這個會話的具體的離線消息id列表
然後,消息id列表使用redis的一個list鏈表來存儲
key:prefix_offline_msg:{uid}:{senderUid} value:{msgId}
拿到所有msgId以後,去獲取msg的實體詳情填充即可
4. 未讀計數
未讀計數= 收到消息總數 - 已讀數量
所以我們要存儲兩個已知數據便於計算出未讀數量,即消息總數量和已讀數量
由於對話存在雙方發消息,所以分別維護對話雙方的兩個數據項,方便計算各自的未讀數
接受消息總數量
key: prefix_session_count:{會話Id}:{uid} value: 總數量
已讀數量
key: prefix_session_read_count:{會話Id}:{uid} value: 已讀數量
5. 用戶資料
使用mysql按需設計即可,變更保存後將數據同步到redis中使用
三. 架構層級拆分
![](https://news.xinpengboligang.com/upload/keji/97bcce708c86755fdc8ec7ad06dbaaeb.jpeg)
如圖所示,我們可以將架構大致分為五層,具體說明如下
1. 客戶端層
我們IM服務的client肯定是有多個,web/app等,需要封裝多種SDK隱藏底層細節,便於接入方接入。
2. 連接層
即時通訊需要客戶端和服務端之間建立一個長鏈接,一方面維護用戶的在線狀態,另一方面便於復用連接進行消息的收發。
而維護連接這個動作,它的獨立性很強,不需要與業務邏輯耦合,所以我們把鏈接層單獨拆分出來一個。
這樣在業務邏輯迭代上線時,業務層進行滾動上線也不會導致用戶的鏈接斷開。
連接協議
至於連接協議的選擇,有如下幾種方式
- 基於tcp鏈接,自定義傳輸協議(開發成本高,需要有一定條件)
- websocket
- http chunk (不建議使用,http工作在7層上,且隻能服務端單向的向客戶端傳輸數據,心跳連接不好維護)
這裡推薦優先使用四層的協議來進行長鏈接的維護。
因為長鏈接集群的前方要做負載均衡,使用七層的協議,客戶端要先和負載均衡機器建立鏈接,然後負載均衡機器再和業務層集群交互。
這樣在連接數很大的時候,負載均衡的機器容易成為瓶頸。四層的負載均衡可以直接通過修改目標機器ip prot的方式來進行轉發,不需要client和負載均衡機器建鏈接
3. 業務層
業務層可以分為「長鏈接業務層 」和「短鏈接業務層 」
具體兩者的功能拆分,可根據業務實際情況設計
- 長鏈接業務層: 負責會話相關的業務邏輯,比如收發消息/拉取會話列表/未讀計數push等業務
- 短鏈接業務層: 負責一些臨時接口請求,比如用戶資料拉取/資料變更等類似業務
兩種業務層都通過調用服務層來進行數據讀取和寫入等擦歐總
4. 服務層
這層屬於微服務,來為上層業務層提供基礎服務能力,例如敏感消息過濾/會話列表數據讀寫/消息的落地和發送等功能。
5. 數據層
為上層的服務層來提供數據的實際落地寫入,可以使用mysql,redis或其他sql/nosql數據庫。
四. 推拉模式選擇
那麼在消息的發送上,我們應該選用推模式,還是拉模式,抑或是推拉結合呢?
1. 純推模式
首先,我們假設使用純推模式 ,來看會存在什麼樣的問題
場景1: 新設備登陸初始化
用戶新登陸一臺設備的時候,如果消息記錄全都是空的,體驗會很不好。
那麼就需要服務端推送全量 的消息記錄到客戶端,歷史消息量大的時候,非常浪費服務端資源和帶寬。
場景2: 設備間切換
![](https://news.xinpengboligang.com/upload/keji/a8d0b8a79b45453bbd6b4667816ea80e.jpeg)
tips:設備A和B都非第一次登陸
如圖所示,流程如下
- 用戶1在設備A上登陸,收到了用戶2的消息1和2,push到了設備A上。
- 用戶1退出了設備A,用戶2又給他發送了消息3和4
- 用戶1登陸了設備B,服務端push消息3和4到了設備B
但是此時,設備B缺少了消息1和2,用戶再登陸回設備A的話又缺少了消息3和4,這也就產生了「消息空洞 」
2. 純拉模式
然後,我們假設使用純拉模式 ,來看會存在哪些問題
場景1: 收新消息
純拉模式下,客戶端需要和服務端進行一個長輪詢,來定時檢查是否存在新消息,並進行消息拉取。
這樣輪詢的時間間隔需要很難確定合適,間隔大了消息不實時,間隔小了無疑對服務器會產生很大的壓力,無法支撐大量的在線用戶進行聊天。
總結
由於推拉模式分別適用於業務中的不同場景需要,所以我們要使用推拉結合的方式來做。
拉模式適合的場景如下:
- 設備初始化時:先拉取會話列表,在根據會話的列表來為每個會話拉取一定的消息記錄。可以通過控制拉取的數據量,減輕服務端壓力。
- 歷史聊天記錄:按需拉取一定條數的記錄,用戶向上翻取記錄再拉取固定條數的記錄,直到翻到沒有記錄(就是翻頁)。
推模式適合的場景如下:
- 用戶實時接收消息
- 用戶在線,有未讀消息做通知欄push時
五. 消息流轉
上面確定好推拉模式後,我們來看發消息和收消息都有哪些業務邏輯執行。
發消息
![](https://news.xinpengboligang.com/upload/keji/e46c21e53c30260cc04a587aff3caa54.jpeg)
如上圖所示,大致可分為三步
1. 消息過濾
首先用戶的消息通過客戶端的SDK發送出來,通過長鏈接到達了「邏輯層」,邏輯層接收到該請求後,可以根據定義的攔截過濾規則調用「服務層」的服務接口,來對消息進行處理;
2. 消息補充
處理通過後,來對消息的發送方資料進行填充,簡單來說就是senderId標識,接收方接收消息時能夠填充到對應的會話中。
3. 派發任務
消息實體處理完成後,將該消息push到「服務層」的「異步任務隊列」服務中。
異步隊列任務 主要需要做以下四個方面的操作
- 更新存儲端的「聊天記錄」
- 更新會話的「消息總數量」,用來計算未讀計數
- 根據接收方的在線狀態來判斷,是直接進行push,還是存入到離線列表中,等待用戶上線後再進行消息拉取
- 更新「會話列表」的score值
具體異步隊列還可以細化拆分,例如
實時任務隊列
延時任務隊列
失敗重試隊列 分別啟動不同的線程池來消費任務,按需分配線程數處理
收消息
收消息主要有以下幾個場景需要處理
- 客戶端需要將消息append到聊天列表中,並在會話列表中將該會話增加未讀消息標識。
- 如果接收方打開了開聊天窗口,客戶端會發送一個消息的ACK給服務端,來標記該消息已讀。
- 服務端收到已讀ACK後需要更新「已讀計數」相關數據項
- 如果是拉取離線消息,服務端還需要更新「離線消息」相關數據項
基於 Spring Boot MyBatis Plus Vue & Element 實現的後臺管理系統 用戶小程序,支持 RBAC 動態權限、多租戶、數據權限、工作流、三方登錄、支付、短信、商城等功能
項目地址:
https://github.com/YunaiV/ruoyi-vue-pro視頻教程:
https://doc.iocoder.cn/video/
小結
本文從五個方面來對單聊的IM架構進行了設計分析
- 業務功能拆分
- 數據結構設計
- 系統結構設計
- 推拉模式選擇
- 消息流轉分析 講了基礎的結構有哪些,數據結構有哪些要求,以及消息流傳的過程是什麼樣的。
對im單聊場景的開發框架有了大體的一個認識,但是實際落地的時候還有很多細節需要去實現。