1. 引言
當我那天拿著手機,正在和朋友們的微信群里暢聊著八卦新聞和即將到來的周末計劃時,忽然一條帶著喜意的消息撲面而來,消息正中間寫著八個大字:恭喜發財,大吉大利。
圖片
搶紅包!!相信大部分人對此都不陌生,那微信的這個群聊系統是如何設計的,讓我們可以方便地聊天、分享圖片和表情,還有那個神奇的紅包功能呢?
這個問題一直困擾著,于是我決定深入了解一下,看看微信的群聊系統背后的設計是怎樣的。
微信群聊系統設計
微信作為 10 億用戶級別的全民 App,想必大家都用過,微信建群功能是微信里面核心的一個能力,它可以將數百個好友或陌生人放進一個群空間。
圖片
或許你已經在微信上體驗過很多次群組聊天,但你是否好奇過這個背后的系統是如何設計的呢?
今天我們就來探討一下。
2. 系統需求
2.1 系統特點與功能需求
微信群聊功能是社交應用的核心功能之一,它允許用戶創建自己的社交圈子,與家人、朋友或共同興趣愛好者進行友好地交流。
以下是微信群聊系統的核心功能:
圖片
- 創建群聊:用戶可以創建新的聊天群組,邀請其他好友用戶加入或與陌生人面對面建群。
- 群組管理:群主和管理員能夠管理群成員,設置規則和權限。
- 消息發送和接收:允許群成員發送文本、圖片、音頻、視頻等多種類型的消息,并推送給所有群成員。
- 實時通信:消息應該能夠快速傳遞,確保實時互動。
- 搶紅包:用戶在群聊中發送任意個數和金額的紅包,群成員可以搶到隨機金額的紅包。
2.2 非功能需求:應對高并發、高性能、海量存儲
當我們面對 10 億微信用戶每天都可能使用建群功能的情景時,就需要處理大規模的用戶并發。這就引出了系統的非功能需求,包括:
- 高并發:系統需要支持大量用戶同時創建和使用群組,以確保無延遲的用戶體驗。
- 高性能:快速消息傳遞、即時響應,是數字社交的關鍵。
- 海量存儲:系統必須可擴展,以容納用戶生成的海量消息文本、圖片及音視頻數據。
3. 概要設計
在概要設計中,我們考慮了系統的核心組件和基本業務的概要設計。
3.1 核心組件
微信群聊系統中,會涉及到如下核心組件和協議。
圖片
- 客戶端:接收手機或 PC 端微信群聊的消息,并實時傳輸給后臺服務器
- Websocket傳輸協議:支持客戶端和后臺服務端的實時交互,開銷低,實時性高,常用于微信、QQ 等 IM 系統通信系統
- 長連接集群:與客戶端進行 Websocket 長連接的系統集群,并將消息通過中間件轉發到應用服務器
- 消息處理服務器集群:提供實時消息的處理能力,包括數據存儲、查詢、與數據庫交互等
- 消息推送服務器集群:這是信息的中轉站,負責將消息傳遞給正確的群組成員
- 數據庫服務器集群:用于存儲用戶文本數據、圖片的縮略圖、音視頻元數據等
- 分布式文件存儲集群:存儲用戶圖片、音視頻等文件數據
3.2 業務概要設計
群聊創建
- 唯一ID分配:當用戶請求創建一個新群組時,系統生成一個唯一的群組 ID,通常可以使用分布式 ID 生成器如雪花算法(Snowflake)或直接使用數據庫自增 ID。這里我們為了實現簡便,采用 MySQL 的自增 ID。
- 群組信息存儲:將群組 ID 和相關信息(例如群名、創建者 ID 等)存儲在群組數據庫中。
- 成員關聯:將群主添加為群組的創始成員,同時創建者也會成為管理員。
- 消息歷史記錄:為了確保新成員能夠訪問以前的消息,將此新群組的群組 ID 與用戶消息關聯存儲。
除了拉好友建群,微信還實現了面對面建群的能力。
接下來,我們深入探討了三到四個核心功能的詳細設計,包括面對面建群、消息發送與接收及搶紅包功能。
4. 面對面建群
用戶發起面對面建群,并輸入一個 4 位數的隨機碼,周圍的用戶輸入該隨機碼后可加入群聊,面對面建群功能通常涉及數據表設計和核心業務交互流程如下。
4.1 數據庫表設計
- User 表:存儲用戶信息,包括用戶 ID、昵稱、頭像等。
- Group 表:存儲群組信息,包括群 ID、群名稱、創建者 ID、群成員個數等。
- GroupMember 表:關聯用戶和群組,包括用戶 ID 和群 ID。
- RandomCode 表:存儲面對面建群的隨機碼和關聯的群 ID。
4.2 核心業務交互流程
圖片
用戶 A 在手機端應用中發起面對面建群,并輸入一個隨機碼,校驗通過后,等待周圍(50 米之內)的用戶加入。此時,系統將用戶信息以 HashMap 的方式存入緩存中,并設置過期時間為 3min。
{隨機碼,用戶列表[用戶A(ID、名稱、頭像)]}
用戶 B 在另一個手機端發起面對面建群,輸入指定的隨機碼,如果該用戶周圍有這樣的隨機碼,則進入同一個群聊等待頁面,并可以看到其它群員的頭像和昵稱信息。
此時,系統除了根據隨機碼獲取所有用戶信息,也會實時更新緩存里的用戶信息。
圖片
當第一個用戶點擊進入該群時,就可以加入群聊,系統將生成的隨機碼保存在 RandomCode 表中,并關聯到新創建的群 ID,更新群成員的個數。
然后,系統將用戶信息和新生成的群聊信息存儲在 Group、GroupMember 表中
成員加入,刷新群員信息
之后 B、C 用戶帶著隨機碼加入群聊時,手機客戶端向服務器后端發送請求,驗證隨機碼是否有效。服務器后端驗證隨機碼,檢查隨機碼是否存在于緩存中,以及是否在有效期內。
然后,判斷當前群成員是否滿員(目前普通用戶創建的群聊人數最多為 500 人),如果驗證通過,服務器后端將用戶 B、C 添加到群成員表 GroupMember 中,并返回成功響應。
移動客戶端應用收到成功響應后,更新用戶 B、C 的群聊列表,展示他們已加入的新群聊。
其它技術組件
這樣,用戶 A 通過創建隨機碼和周圍的用戶掃描二維碼的方式成功建立了一個面對面建群。這個功能涉及了多個技術組件,包括分布式緩存、數據庫、二維碼生成和驗證等。
同時,在面對面建群的過程中相當重要的能力是標識用戶的區域,比如 50 米以內。這個可以用到 redis 的 GeoHash 算法,來獲取一個范圍內的所有用戶信息。
由于篇幅有限,這里不展開贅述,想了解更多和二維碼生成及位置算法的細節,可以看我之前的文章:聽說你會架構設計?來,弄一個公交&地鐵乘車系統。
5. 消息發送與接收
當某個成員在微信群里發言,系統需要處理消息的分發、通知其他成員、以及確保消息的顯示。以下是這一功能的詳細交互步驟,以及數據庫存儲方案。
5.1 交互流程
消息發送和接收時序圖如下:
圖片
- 用戶A在群中發送一條帶有圖片、視頻或音頻的消息。
- 移動客戶端應用將消息內容和媒體文件上傳到服務器后端。
- 服務器后端接收到消息和媒體文件后,將消息內容存儲到 Message 表中,同時將媒體文件存儲到分布式文件存儲集群中。在 Message 表里,不僅記錄了媒體文件的 MediaID,以便關聯消息和媒體;還記錄了縮略圖、視頻封面圖等等。
- 服務器后端會向所有群成員廣播這條消息。移動客戶端應用接收到消息后,會根據消息類型(文本、圖片、視頻、音頻)加載對應的展示方式。
- 當用戶點擊查看圖片、視頻或音頻縮略圖時,客戶端應用會根據 MediaID 到對象存儲集群中獲取對應的媒體文件路徑,并將其展示給用戶。
這個流程確保了消息和媒體文件的有效存儲和展示。用戶可以上傳和查看各種類型的媒體數據,而服務器后端通過關聯 Message 和對象存儲服務器中的信息,實現了有效的消息存儲和展示。
5.2 消息存儲和展示
在微信群中保存和展示用戶的圖片、視頻或音頻數據,通常需要進行數據存儲和展示方面的設計。除了上面面對面建群功能中提到的用戶表和群組表以外,還需要以下表結構:
- Message表: 用于存儲消息,每個消息都有一個唯一的 MessageID,消息類型(文本、圖片、視頻、音頻),消息內容(文字、圖片縮略圖、視頻封面圖等),發送者 UserID、接收群 GroupID、發送時間等字段。
- Media表: 存儲用戶上傳的圖片、視頻、音頻等媒體數據。每個媒體文件都有一個唯一的 MediaID,文件路徑、上傳者 UserID、上傳時間等字段。
- MessageState表: 用于存儲用戶消息狀態,包括 MessageID、用戶 ID、是否已讀等。在消息推送時,通過這張表計算未讀數,統一推送給用戶,并在離線用戶的手機上展示一個小數字代表消息未讀數。
我們知道,MySQL 每次查詢 select count 類型的語句時,都會觸發全表掃描,所以每次加載消息未讀數都很慢。
為了查詢性能考慮,我們可以將用戶的消息數量存入 Redis,并實時記錄一個未讀數值。并且,當未讀數大于 99 時,就將未讀數值置為 100 且不再增加。
當推送用戶消息時,只要未讀數為 100,就將推送消息數設置為 99+,以此來提升存儲的性能和交互的效率。
6. 搶紅包
搶紅包功能允許用戶在群聊中發送任意個數和金額的紅包,群成員可以搶到隨機金額的紅包,但要保證每個用戶的紅包金額不小于 0.01 元。
圖片
搶紅包的詳細交互流程如下:
- 用戶接收到搶紅包通知,點擊通知打開群聊頁面
- 用戶點擊搶紅包,后臺服務驗證用戶資格,確保用戶尚未領取過此紅包
- 若用戶資格驗證通過,后臺服務分配紅包金額并存儲領取記錄
- 用戶在微信群中看到領取金額,紅包狀態更新為“已領取”
- 異步調用支付接口,將紅包金額更新到錢包里
搶紅包功能需要關注搶紅包的數據庫設計,搶紅包實時性和紅包分配算法。
6.1 數據庫設計
紅包表 redpack 的字段如下:
- id: 主鍵,紅包ID
- totalAmount: 總金額
- surplusAmount: 剩余金額
- total: 紅包總數
- surplusTotal: 剩余紅包總數
- userId: 發紅包的用戶ID
該表用來記錄用戶發了多少紅包,以及需要維護的剩余金額。
紅包記錄表 redpack_record 如下:
- id: 主鍵,記錄ID
- redpackId: 紅包ID,外鍵
- userId: 用戶ID
- amount: 搶到的金額
記錄表用來存放用戶具體搶到的紅包信息,也是紅包表的副表。
6.2 實時性
1、發紅包
- 用戶設置紅包的總金額和個數后,在紅包表中增加一條數據,開始發紅包
- 為了保證實時性和搶紅包的效率,在 Redis 中增加一條記錄,存儲紅包 ID 和總人數 n
- 搶紅包消息推送給所有群成員
2、搶紅包
從 2015 年起,微信紅包的搶紅包和拆紅包就分離了,用戶點擊搶紅包后需要進行兩次操作。這也是為什么明明有時候搶到了紅包,點開后卻發現該紅包已經被領取完了。
搶紅包的交互步驟如下:
- 搶紅包:搶操作在 Redis 緩存層完成,通過原子遞減的操作來更新紅包個數,到 0 后就說明搶光了。
- 拆紅包:拆紅包時,首先會實時計算金額,一般是通過二倍均值法實現(即 0.01 到剩余平均值的 2 倍之間)。
- 紅包記錄:用戶獲取紅包金額后,通過數據庫的事務操作累加已經領取的個數和金額,并更新紅包表和記錄表。
- 轉賬:為了提升效率,最終的轉賬為異步操作,這也是為什么在春節期間,紅包領取后不能立即在余額中看到的原因。
6.3 紅包分配算法
紅包金額分配時,由于是隨機分配,所以有兩種實現方案:實時拆分和預先生成。
1、實時拆分
實時拆分,指的是在搶紅包時實時計算每個紅包的金額,以實現紅包的拆分過程。
這個需要我們設計一個好的拆分算法,讓紅包拆分時一直保證后續待拆分紅包的金額不能為空。
實時拆分時,不容易做到拆分的紅包金額服從正態分布規律。
2、預先生成
預先生成,指的是在紅包開搶之前已經完成了紅包的金額拆分,搶紅包時只是依次取出拆分好的紅包金額。
這種方式對拆分算法要求較低,可以拆分出隨機性很好的紅包金額,但通常需要結合隊列使用,而且需要多設計一個表來存儲紅包的拆分金額。
3、二倍均值法
綜合上述優缺點考慮,以及微信群聊中的人數不多(目前最高 500 人),所以我們采用實時拆分的方式,用二倍均值法來生成隨機紅包,只滿足隨機即可,不需要正態分布。
故可能出現很大的紅包差額,但這更刺激不是嗎