高併發系統設計 -1-: 如何實現抖音關注 - 粉絲列表

音作爲國民短視頻社交應用,有數據顯示總用戶數量已超過 8 億,每日活躍用戶達 7 億,人均單日使用時長超過 2 小時。在這樣龐大的用戶基數下,一個小小的功能,背後可能需要複雜的設計。以用戶服務爲例,單單存儲 8 億用戶,已經遠遠超出單庫單表 MySQL 的極限。本文中我們的問題是如何在這樣的用戶基數上,實現關注列表、粉絲列表功能。

需求分析

事實上,關注列表、粉絲列表只是需求的一部分。抖音上人與人的關係是一種弱關係,可以單方向進行關注,這一點與微博、twitter 類似。作爲對比,微信上人與人的關係是強關係,必須是對等的。

如果打開抖音,我們可以看到三個標籤:朋友、關注、粉絲。

這三個標籤分別對應:

  1. 查詢自己的朋友列表,並支持搜索 (互相關注)

  2. 查詢自己的關注列表,並支持搜索 (關注的用戶)

  3. 查詢自己的粉絲列表,並支持搜索 (關注你的用戶)

除了這三類讀操作之外,還有一些寫操作:

  1. 關注

  2. 取消關注

  3. 拉黑

  4. 取消拉黑等等

數據分佈

憑經驗來看,每個用戶關注的用戶數是有限的,大概率不會超過 5000; 一個普通用戶的粉絲數也是有限的,能超過 5000 至少說明精心運營過。

但超級大 v 的粉絲數可以遠遠超出這個量級,比如在抖音上搜索 “劉德華”、“鄧紫棋”、“王一博 “等,粉絲數都是千萬級別的。精心運營過的劉德華粉絲數達到了驚人的 7600w,這個量級可以給他定製化一張 MySQL 表了,表名就叫 liudehua_followers 😂

當然,定製化一張 MySQL 表只是一個玩笑。但一個事實是,劉德華的粉絲數是我的 100w 倍;我們沒有渠道得知抖音用戶的粉絲數分佈,馬太效應肯定是超級明顯。

存儲架構

關注關係本質上是一種關係,但業務上體現爲兩種:粉絲列表和關注列表。朋友是粉絲列表和關注列表的交集。如果設計一種存儲架構,需要滿足一些條件:

  1. 持久化存儲: 關注關係存起來以後,不能丟

  2. 數據一致性: 粉絲列表和關注列表的數據必須一致

  3. 高性能: 查詢和更新都必須快,比如內網服務接口響應 100ms 以內

  4. 架構簡單、資源佔用可接受

先不考慮數據量,如果用一張 MySQL 表存儲關注列表,那麼結構大概是這樣的:

1. uid bigint
2. follower_uid bigint
3. status tinyint
4. create_time tiimestamp
5. modify_time tiimestamp

在 uid 和 follower_uid 都加上索引,用 Go 寫個服務封裝成接口,就可以用了。

但是,這是 8 億用戶,分庫分表肯定是少不了的。一旦做了分庫分表,關注列表和粉絲列表都必須分開存儲了,因爲:

  1. 如果用 uid 做分片,通過 follower_uid 取關注列表時,數據分散在不同的分片上,讀寫的效率都會很低,還不支持分頁

  2. 如果用 follower_uid 做分片,通過 uid 讀取粉絲列表時,會遇到同樣的問題

所以,粉絲列表仍然使用上面的表結構,uid 是用戶 ID,follower_uid 是粉絲 ID,對 uid 進行分片;

關注列表則採用稍微不同的表結構,uid 是用戶 ID,following_uid 是被關注用戶的 ID,對 uid 進行分片:

1. uid bigint
2. following_uid bigint
3. status tinyint
4. create_time tiimestamp
5. modify_time tiimestamp

按照功能拆表、對錶進行分片,這兩個操作完事以後,我們滿足了持久化和高性能兩個指標,但如何保證兩張表數據的一致性呢?

我們簡單聊一下 CAP 理論:

在一個大型分佈式系統中,這三點不可能同時完成。我們看 CAP 理論如何應用到粉絲場景。

在粉絲場景中,我們需要糾結的是 C 和 A 選哪個。

如果選 C,那麼就需要依賴分佈式鎖,保證兩張表都寫入以後,才返回結果給客戶端;這裏的缺點很明顯: 一是引入外部依賴 (分佈式鎖),鎖掛了怎麼兜底;而且性能差;

如果選 A,就是最終一致性方案。當關注行爲被觸發時,優先將數據寫入 “關注表”(沒有馬太效應、性能可控),通過消費 Binlog 更新數據到 “粉絲表”;

權衡到業務場景的要求、技術實現的成本、服務的穩定性,選 A 更優。

業務支持

場景 1: 朋友列表

  1. 通過 uid 獲取 “關注列表”,list<following_uid>

  2. 通過 folloing_uid + uid,反查 “粉絲列表”

場景 2: 通過名字搜索

  1. 先找到粉絲或關注的 uid list

  2. 用 list + 名字搜索 user 表 (或存放用戶信息的 ElasticSearch)

這兩個場景下,如果是普通用戶,基本上沒啥問題。

如果我是劉德華,場景 1 倒是沒啥問題,場景 2 如果搜索的是粉絲,那麼性能上也會有問題,怎麼解決呢?解決辦法就是不讓大 V 搜索。

胖客戶端的一點聯想

客戶端和服務端的分界線從來都不是那麼清晰,隨着歷史的演進,

大學那會兒,教科書上會說瀏覽器是瘦客戶端,桌面應用是胖客戶端。

上面提到的存儲設計,或者上層的接口設計,都是服務端做的事情。抖音需要服務這麼大的用戶羣體,本身已經需要很多機器。每個看起來微不足道的請求,QPS 一旦上來,需要的機器數量都不會少。那麼有沒有一種方法,可以減少機器佔用呢?

當然有,由於目前手機的配置普遍都比較高,很多原本在服務端的信息,都可以緩存到手機內置存儲裏。只需要一定的更新機制,保證服務端和客戶端的數據一致即可。不然手機端的 app 怎麼都這麼大呢?

因此,粉絲列表、關注列表這類數據對都可以緩存到 App 端。粉絲列表的變更從服務端定期拉取更新;關注列表的變更由 App 端觸發,只需要保證服務端接口調用成功後,更新本地緩存即可。

用本地緩存的數據支持複雜的查詢,大大壓縮了數據量,解決大表 JOIN 的問題,模糊搜索玩出花都可以!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/cIgfuumVCL4rxtNYGcQH4A