微信朋友圈信息流的系统设计
引言
信息推流(以下簡稱“Feed流”)這種功能在我們手機APP中幾乎無處不在(尤其是社交/社群產品中),最常用的就是微信朋友圈、新浪微博等。
對Feed流的定義,可以簡單理解為只要大拇指不停地往下劃手機屏幕,就有一條條的信息不斷涌現出來。就像給牲畜喂飼料一樣,只要它吃光了就要不斷再往里加,故此得名Feed(飼養)。
大多數帶有Feed流功能的產品都包含兩種Feed流:
- 基于算法:即動態算法推薦,比如今日頭條、抖音短視頻。
- 基于關注:即社交/好友關系,比如微信、知乎。
這兩種Feed流,它們背后用到的技術差別會比較大。不同于“推薦”頁卡那種千人千面算法推薦的方式,通?!瓣P注”頁卡所展示的內容先后順序都有固定的規則,最常見的規則是基于時間線來排序,也就是展示“我關注的人所發的帖子、動態、心情,根據發布時間從晚到早依次排列”。
本文將重點討論的是“關注”功能對應的技術實現:先總結常用的基于時間線Feed流的后臺技術實現方案,再結合具體的業務場景,根據實際需求在基本設計思路上做一些靈活的運用。
讀擴散
讀擴散也稱為“拉模式”,這應該是最符合我們認知直覺的一種技術實現方式。原理如下圖:
如上圖所示: 每一個內容發布者都有一個自己的發件箱(“我發布的內容”),每當我們發出一個新帖子,都存入自己的發件箱中。當我們的粉絲來閱讀時,系統首先需要拿到粉絲關注的所有人,然后遍歷所有發布者的發件箱,取出他們所發布的帖子,然后依據發布時間排序,展示給閱讀者。
這種設計: 閱讀者讀一次Feed流,后臺會擴散為N次讀操作(N等于關注的人數)以及一次聚合操作,因此稱為讀擴散。每次讀Feed流相當于去關注者的收件箱主動拉取帖子,因此也得名——拉模式。
這種模式:
- 好處是:底層存儲簡單,沒有空間浪費;
- 壞處是:每次讀操作會非常重,操作非常多。
設想一下: 如果我關注的人數非常多,遍歷一遍我所關注的所有人,并且再聚合一下,這個系統開銷會非常大,時延上可能達到無法忍受的地步。
因此: 讀擴散主要適用系統中閱讀者關注的人沒那么多,并且刷Feed流并不頻繁的場景。
拉模式還有一個比較大的缺點: 就是分頁不方便,我們刷微博或朋友圈,肯定是隨著大拇指在屏幕不斷劃動,內容一頁一頁的從后臺拉取。如果不做其他優化,只采用實時聚合的方式,下滑到比較靠后的頁碼時會非常麻煩。
寫擴散
大多數Feed流產品的讀寫比大概在100:1,也就是說大部分情況都是刷Feed流看別人發的朋友圈和微博,只有很少情況是自己親自發一條朋友圈或微博給別人看。因此讀擴散那種很重的讀邏輯并不適合大多數場景。
我們寧愿讓發帖的過程復雜一些,也不愿影響用戶讀Feed流的體驗,因此稍微改造一下前面方案就有了寫擴散。寫擴散也稱為“推模式”,這種模式會對拉模式的一些缺點做改進。原理如下圖:
如上圖所示: 系統中每個用戶除了有發件箱,也會有自己的收件箱。當發布者發表一篇帖子的時候,除了往自己發件箱記錄一下之外,還會遍歷發布者的所有粉絲,往這些粉絲的收件箱也投放一份相同內容。這樣閱讀者來讀Feed流時,直接從自己的收件箱讀取即可。
這種設計: 每次發表帖子,都會擴散為M次寫操作(M等于自己的粉絲數),因此成為寫擴散。每篇帖子都會主動推送到所有粉絲的收件箱,因此也得名推模式。
這種模式可想而知: 發一篇帖子,背后會涉及到很多次的寫操作。通常為了發帖人的用戶體驗,當發布的帖子寫到自己發件箱時,就可以返回發布成功。后臺另外起一個異步任務,不慌不忙地往粉絲收件箱投遞帖子即可。
寫擴散的好處在于通過數據冗余(一篇帖子會被存儲M份副本),提升了閱讀者的用戶體驗。通常適當的數據冗余不是什么問題,但是到了微博明星這里,完全行不通。比如目前微博粉絲量Top2的謝娜與何炅,兩個人微博粉絲過億。
設想一下: 如果單純采用推模式,那每次謝娜何炅發一條微博,微博后臺都要地震一次。一篇微博導致后臺上億次寫操作,這顯然是不可行的。
另外: 由于寫擴散是異步操作,寫的太慢會導致帖子發出去半天,有些粉絲依然沒能看見,這種體驗也不太好。
通常寫擴散適用于好友量不大的情況,比如微信朋友圈正是寫擴散模式。每一名微信用戶的好友上限為5000人,也就是說你發一條朋友圈最多也就擴散到5000次寫操作,如果異步任務性能好一些,完全沒有問題。
讀寫混合
在設計后臺存儲的時候,我們如果能夠區分一下場景,在不同場景下選擇最適合的方案,并且動態調整策略,就實現了讀寫混合。以微博為例子,原理圖如下:
當何炅這種粉絲量超大的人發帖時,將帖子寫入何炅的發件箱,另外提取出來何炅粉絲當中比較活躍的那一批(這已經可以篩掉大部分了),將何炅的帖子寫入他們的收件箱。當一個粉絲量很小的路人甲發帖時,采用寫擴散方式,遍歷他的所有粉絲并將帖子寫入粉絲收件箱。
對于那些活躍用戶登錄刷Feed流時: 他直接從自己的收件箱讀取帖子即可,保證了活躍用戶的體驗。
當一個非活躍的用戶突然登錄刷Feed流時:
- 一方面需要讀他的收件箱;
- 另一面需要遍歷他所關注的大V用戶的發件箱提取帖子,并且做一下聚合展示。
在展示完后: 系統還需要有個任務來判斷是否有必要將該用戶升級為活躍用戶。
因為有讀擴散的場景存在,因此即使是混合模式,每個閱讀者所能關注的人數也要設置上限,例如新浪微博限制每個賬號最多可以關注2000人。
如果不設上限: 設想一下有一位用戶把微博所有賬號全部關注了,那他打開關注列表會讀取到微博全站所有帖子,一旦出現讀擴散,系統必然崩潰(即使是寫擴散,他的收件箱也無法容納這么多的微博)。
讀寫混合模式下,系統需要做兩個判斷:
- 1)哪些用戶屬于大V,我們可以將粉絲量作為一個判斷指標;
- 2)哪些用戶屬于活躍粉絲,這個判斷標準可以是最近一次登錄時間等。
這兩處判斷標準就需要在系統發展過程中動態地識別和調整,沒有固定公式了。
可以看出: 讀寫結合模式綜合了兩種模式的優點,屬于最佳方案。
然而他的缺點是:系統機制非常復雜,給程序員帶來無數煩惱。通常在項目初期,只有一兩個開發人員,用戶規模也很小的時候,一步到位地采用這種混合模式還是要慎重,容易出bug。當項目規模逐漸發展到新浪微博的水平,有一個大團隊專門來做Feed流時,讀寫混合模式才是必須的。
分頁問題
上面幾節已經敘述了基于時間線的幾種Feed流常見設計方案,但實操起來會比理論要麻煩許多。
接下來專門討論一個Feed流技術方案中的痛點——Feed流的分頁。
不管是讀擴散還是寫擴散,Feed流本質上是一個動態列表,列表內容會隨著時間不斷變化。傳統的前端分頁參數使用page_size和page_num,分表表示每頁幾條,以及當前是第幾頁。
對于一個動態列表會有如下問題:
如上圖所示: 在T1時刻讀取了第一頁,T2時刻有人新發表了“內容11”,在T3時刻如果來拉取第二頁,會導致錯位出現,“內容6”在第一頁和第二頁都被返回了。事實上,但凡兩頁之間出現內容的添加或刪除,都會導致錯位問題。
為了解決這一問題: 通常Feed流的分頁入參不會使用page_size和page_num,而是使用last_id來記錄上一頁最后一條內容的id。前端讀取下一頁的時候,必須將last_id作為入參,后臺直接找到last_id對應數據,再往后偏移page_size條數據,返回給前端,這樣就避免了錯位問題。
采用last_id的方案有一個重要條件:就是last_id本身這條數據不可以被硬刪除。
設想一下:
- 1)上圖中T1時刻返回5條數據,last_id為內容6;
- 2)T2時刻內容6被發布者刪除;
- 3)那么T3時刻再來請求第二頁,我們根本找不到last_id對應的數據了,也就無法確認分頁偏移量。
通常碰到刪除的場景: 我們采用軟刪除方式,只是在內容上置一個標志位,表示內容已刪除。
由于已經刪除的內容不應該再返回給前端,因此軟刪除模式下,找到last_id并往后偏移page_size條,如果其中有被刪除的數據會導致獲得足夠的數據條數給前端。
這里解決方案有2個:
- 找不夠繼續再往下找;
- 與前端協商,允許返回條數少于page_size條,page_size只是個建議值。甚至大家約定好了以后,可以不要page_size參數。
總結
幾乎所有基于時間線和關注關系的Feed流都逃不開三種基本設計模式:
- 讀擴散;
- 寫擴散;
- 讀寫混合。
具體到實際業務中,可能會有更復雜的場景,比如 微博朋友圈場景中會有廣告接入、特別關注、熱點話題等可能影響到Feed流排序的因素。這些場景就只能根據業務需求,做相對應的變通了。
總結
以上是生活随笔為你收集整理的微信朋友圈信息流的系统设计的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Nat. Commun. | 无需参数的
- 下一篇: 【星海出品】数据可视化之Matplotl