Feed 流系統的架構設計方案
本文主要針對 Feed 流進行介紹,將從 Feed 流的演變入手,帶你一步步瞭解 Feed 流,而後學習如何從開發角度入手,對其進行建模,抽象出 Feed 流常見的架構,最終搭建高可用、高擴展、高性能的 Feed 流應用。
01 瞭解 Feed 流
在學習如何開發 Feed 流應用前,我們需要先了解什麼是 Feed 流。
1.1 什麼是 Feed 流
Feed 流是一個持續更新並展示給用戶的信息流。它將用戶主動訂閱的若干消息源組合在一起形成內容聚合器,幫助用戶持續地獲取最新的訂閱源內容。所以它通常具有千人千面的個性化特點。舉例來說,我們在各類手機 App 中能看到的猜你喜歡,你的關注和好友動態等功能,都是 Feed 流的一種表現形式。某種意義上來說,你可以一直向下滑動,而後獲取到信息的應用,都是屬於 Feed 流。
1.2 爲什麼會有 Feed 流
瞭解了什麼是 Feed 流後,我們可以從產品角度思考一下,爲什麼會有 Feed 流。
我們可以和傳統的信息獲取渠道,電視,報紙,雜誌進行對比。以前我們獲取的信息,通常是主動前往某個信息聚合的渠道,如:電視的新聞臺,去買一份報紙,訂購一本雜誌。而後,我們從中大量閱讀,然後才能獲取到我們感興趣的信息。上述流程我們可以知道,我們想要獲取豐富的信息可不容易。
所以,這就有了 Feed 流的出現了,它的主要作用是:信息聚合。也就是它可以根據你的行爲去聚合你想要的信息,然後再將它們以輕鬆易得的方式提供給你。這個方式就是信息流的方式,你只需要不斷的滑動,就可以再各種信息中穿梭,而不需要自己去尋找,被動接收信息。當然,僅僅是流的方式還不以讓它成爲現今主流的新聞媒體傳播途徑。因爲傳統的電視節目,當你不感興趣的時候,你也可以換臺進行切換,也是一種簡單易得,可自由選擇的方式。Feeds 最核心的能力在於聚合。他會根據你的行爲聚合出你想要的信息,例如:微博是通過你的關注列表瞭解你可能想要的信息源,而後以時間軸的形式聚合各種信息推給你。後來又出現了抖音的猜你喜歡,它不需要你的手動關注,而是根據你的閱覽時長,點贊等信息生成你的用戶畫像,從而聚合你可能感興趣的信息。朋友圈的 Feed 流則是根據你的好友關係,從而聚合了你可能想要的信息。
正是有了這種豐富多彩的信息聚合能力,用戶在使用 Feed 流獲取信息的時候,就容易獲得他們感興趣的內容。從而有一個很好的使用體驗。
1.3 Feed 流的分類
上面提到了幾種 Feed 流的應用場景,有:微信朋友圈,微博的關注頁,抖音的推薦頁。這幾個例子其實信息聚合的角度都不相同,爲此,我們可以對 Feed 流進行分類,瞭解不同類型的 Feed 流,才知道開發過程中,如何針對不同的應用場景,去設計最合適的架構,實現 Feed 流功能。
首先,我們可以從 Feed 流的信息源聚合依據進行分類,關係有三種:
從上圖我們可以知道,抖音推薦頁可以從你的操作行爲中生成你的用戶畫像,再去匹配聚合信息。而微博則是單向依賴關係,即:我關注了某個大 v,就可以獲取他發佈的信息。這裏的信息聚合依據是單向的關注關係。而微信朋友圈則是雙向關係,需要兩個人互相通過好友,纔會聚合對方的信息到自己的朋友圈中。
三種聚合邏輯,分別適用於信息探測,信息訂閱和熟人社交場景中,各有各的優點。
而除了從信息源聚合依據出發進行分類以外,也可以從 Feed 流本身的展示邏輯出發進行分類,關係有兩類:
注意:微博熱榜很多人也算成了 Feed 流,但是嚴格意義上來說,他是一個信息流。所有人看到的熱榜數據都是一樣的,這缺失了信息聚合的特徵。所以,本質上熱榜的底層模型應該是排行榜,而非 Feed 流。這裏不將它歸爲一類。
兩個分類是從兩個維度對 Feed 流進行的劃分,但是,不管是什麼維度的分類,都是爲了更好的貼近業務特點,進行建模開發。
實際上,上述表格又可以進一步總結爲兩類:
1.4 瞭解 Feed 流的前世今生
通過上面的介紹,想必你對 Feed 流已經有了一定的瞭解。那麼再說一下它的前身。
Feed 流其實不是一開始就是這種形式。它起源於 RSS 系統。RSS 翻譯過來就是簡易信息聚合,它將用戶主動訂閱的若干消息源組合在一起形成內容(aggregator),幫助用戶持續地獲取最新的訂閱源內容。對用戶而言,聚合器是專門用來訂閱網站的軟件,一般稱爲 RSS 閱讀器、Feed 閱讀器等。用戶選擇訂閱多個訂閱源,網站提供 Feed 網址 ,用戶將 Feed 網址登記到聚合器裏,在聚合器裏形成聚合頁,用戶便能持續地獲取最新的訂閱源內容。整個交互流程簡而言之是:用戶主動訂閱感興趣的多個訂閱源,訂閱器幫用戶及時更新訂閱源信息,然後按照 timeline 時間順序展示出來。這樣,用戶可以通過訂閱器獲取即時信息,而不用每天都檢查各個訂閱源是否有更新。
可以看出,上述方式很像是在訂購雜誌,雜誌一旦更新,就會寄到家中。但是那時候的的 Rss 系統,能訂閱的只是新聞網站以及博客。直到後來,Facebook 宣佈了一項新的首頁形式「News Feed」,這一形式打破了傳統 RSS 的訂閱方式。News Feed 可以看做一個新型聚合器:訂閱源由某個新聞網站變成了生產內容的人或者團體,而內容由網站輸出的公告新聞,變成了好友(關注對象)的動態(發佈的內容以及其他的社交行爲)。這樣一來,內容豐富程度直線提高,內容發佈者和訂閱者也由:人和網站變成了人和人,社交距離大大拉近。很快,這種信息獲取模式就普及起來了。從此以後,RSS 被迫淡出歷史舞臺。
1.5 Feed 流模型中的術語
02 瞭解 Feed 流模型的架構
通過上面的介紹,想必你對於將要開發的 Feed 流是什麼已經足夠了解了。那麼,接下來我們從開發的角度切入,再次學習 Feed 流。
我們已經知道了 Feed 流可以分爲兩大類:基於興趣推薦,和基於用戶關係拉取。兩種模式的 Feed 流底層的原理差別很大,所以要分別進行介紹。先介紹第一種:基於用戶關係拉取的 Feed 流。
2.1 依賴用戶關係的時間順序 Feed 流
第一類 Feed 流是依賴用戶關係的,按時間順序進行整合展示的 Feed 流。在開發這個模型前,我們需要先了解這個模型主要面對的挑戰在哪兒。
-
Feed 流模型面臨的挑戰
-
Feed 是一種實時消息,由於消息是實時產生,實時消費,實時推送的,因此滿足實時性是關鍵。(性能要求高)
-
消息來自於很多不同的消息源,消息的產生屬於海量級別。(存儲要求大)
-
性能考慮:從消息產生到消息消費產生巨大的讀寫比。(讀寫失衡模型,時間排序)
-
消息發佈出去後,要求用戶能夠感知,起碼滿足最終一致性,不可以出現消息丟失。(原子性)
-
-
Feed 流模型需要的基本功能
瞭解了 Feed 流的面對的挑戰後,我們先不着急去處理問題,而是進入具體的功能中去分析 Feed 流模型需要開發的功能。包括如下:
1. 用戶發佈消息:用戶可以發佈一條消息,他的訂閱者都能感知到他發佈了消息;(不僅是消息確保推送出去,而且要有紅點提示)
2. 用戶刪除發佈的消息:用戶可以刪除一條已經發布的消息,他的訂閱者都能實時感知到這條消息被刪除了;
3. 用戶查看自己發佈的消息:用戶查看自己已經發布的所有消息;
4. 用戶訂閱消息源:用戶可以訂閱感興趣的人,關注的博主以後發送的消息都可以在用戶的 Feed 流中查看到。需要注意的是,有的場景中要求用戶 Feed 流中能看到博主在被關注之前發的消息,這就要求訂閱的時候,還要主動同步一份博主的所有消息到用戶的 Feed 流中。
5. 用戶取消消息源訂閱:用戶可以取消已經訂閱的人,取關後,Feed 流中關於他的所有消息要除去。
6. 用戶查看訂閱的消息流(Feed 流):用戶可以以 timeline 的形式查看所有訂閱的消息源發佈的消息。消息的刪除和更新,都會實時被用戶感知到。Feed 流的翻頁問題:用戶翻頁 Feed 流的時候,不管 Feed 流更新了多少內容,此時都是沿着最後一次看到的信息往下看。Feed 流前面的信息被刪改不予理會。
7. 額外功能:消息支持配置黑白名單,進行細粒度可見權限控制。
8. 可擴展功能:信息可以支持被評論,評論本身也有增刪改查
- 面臨問題和解決方案
瞭解了上述 Feed 流需要開發的基本功能,我們進一步對功能實現中可能遇到的問題進行分析,並且給出處理方案:
1. 發佈者發佈消息後,訂閱者如何讀取消息?
這裏一般有三種方案:讀擴散,寫擴散和讀寫結合。
* 讀擴散:訂閱者讀取最新收件箱消息的時候,訂閱者主動去查詢關注的人的發件箱,遍歷所有的人,獲取所有的消息,然後更新到自己的收件箱中。
* 寫擴散:發佈者發佈消息後,立刻將自己的消息同步給他所有的粉絲的收件箱中。
* 讀寫結合:由於 Feed 流是讀多寫少的場景,所以一般情況下,我們採用寫擴散,系統的性能會比讀擴散要好。但是,當有大 v 發佈者出現時,他每次發佈消息,可能消息需要同步給 1 億用戶,這樣寫擴散的性能會被嚴重影響到。所以,在大 v 用戶上,採用讀寫結合的方式進行處理。具體來說就是:大 v 用戶發佈消息,消息寫擴散到活躍用戶收件箱。而不活躍用戶在登錄的時候,會去主動拉取大 v 用戶的發件箱,完成自身收件箱的更新。
由於 Feed 流模型是一種讀多寫少的場景,所以一般採用寫擴散更好。
當出現大 v 的時候,寫擴散也太慢了,則採用冷熱分離方案。熱粉絲則寫擴散同步,冷粉絲(殭屍粉)則讀擴散。冷熱粉絲可以記錄登錄次數,時長進行分類。也可以採用 session 池方案,判斷在線的粉絲才進行寫擴散。
2.2 Feed 流是怎麼翻頁的?
Feed 流的分頁入參不會使用 page_size 和 page_num,而是使用 last_id 來記錄上一頁最後一條內容的 id。前端讀取下一頁的時候,必須將 last_id 作爲入參,後臺直接找到 last_id 對應數據,再往後偏移 page_size 條數據,返回給前端,這樣就避免了錯位問題。注意:採用該 last_id 方式要求數據不能被刪除,否則前端持有這個 id,就又可能找不到對應的記錄。爲此,刪除都採用標誌位表示刪除。當拉出的數據存在刪除的時候,進行再次查詢補充。
Feed 流是一個動態列表,每時每刻都可能在更新,所以傳統的使用 page_size 和 page_num 來分頁就不能滿足使用了。因爲但凡兩頁之間出現內容的添加或刪除,都會導致錯位問題。
寫擴散下的翻頁:由於用戶收件箱是提前排序準備好的,所以 last_id 直接往後讀取即可。
讀擴散下的翻頁:由於讀擴散下,用戶的收件箱是實時計算出來的,他翻頁的時候,需要去所有關注人的發件箱中拉取一定量的數據。拉取後,需要記錄當前拉取到了寫信箱的 write_last_id1,多少個關注就要記錄了多少個 write_last_id。而後翻頁的時候,需要用這些 write_last_id 往後拉取新的一定量(比如 page_size 個)的數據。再用這些數據組成的新收件箱列表,篩選 page_size 條返回前端。同時,還需要更新他實際拉取了消息的寫信箱中的 write_last_id,並且存儲。當下一次翻頁的時候,這批 write_last_id 將作爲下次的翻頁時定位的依據。
對比下來看得出:讀擴散的翻頁比寫擴散複雜很多。
2.3 寫擴散模式下
寫擴散模式下,用戶發佈消息可以慢慢擴散出去,但是刪除,修改都要擴散出去,速度過慢會出現時效性問題。而且,如果真的是刪除了數據,可能會影響 Feed 流的分頁功能(第二點已經介紹)。這種情況怎麼處理?
-
採用軟刪除 + 懶刪除機制
軟刪除是指:消息內容不進行實際刪除,而是將消息置爲刪除狀態即可,不擴散出去。如此一來,用戶在自己的讀取收件箱中消息的時候,是先獲取了消息 id 後,再去數據庫查出消息內容,而後判斷狀態進行過濾,把已經刪除的狀態剔除,不返回給前端。此時也需要重新進行撈數據,填充分頁內容。懶刪除是指:如果過濾了某個消息,此時才把消息從用戶收件箱中真正刪除。(redis 的 zset 中的對應 id 進行剔除,完成 Feed 流表的刷新)
-
軟刪除和懶刪除的具體實現如下:採用讀擴散回查方案。
本次需求,我們的寫擴散只寫了一個消息 id 到用戶的收件箱中,所以,用戶查詢收件箱信息的時候,要進行一個回查將信息豐富(該方案相比直接把內容一起寫入收件箱內會更加節約內存,減少冗餘數據,同時消息刪除無需擴散)。
2.4 用戶的收件箱刷新時機問題
用戶收件箱是消息同步庫,緩存的只是消息 id 而已,所以可以全量存儲所有的關注人發佈信息的 id。但是,消息同步庫內的消息實際上變化很大,如果全部採用寫擴散方式,則會導致實時性問題很大。所以對不同的觸發刷新操作,我們需要進行不同處理,各操作如下:
-
關注他人時,用戶的收件箱是否需要觸發刷新:當用戶關注了另一個用戶後,他的收件箱需要獲取到關注用戶的發件箱內所有消息,然後刷新自己的收件箱。(寫擴散)
-
取消關注他人時,用戶的收件箱如何刷新:這裏可以採用過濾的方式:我們從收件箱中獲取到了消息 id,而後需要進行回查,但是回查前,判斷該 id 的所屬發送人是否還在自己關注列表中。不在則進行剔除消息,同時刪除收件箱中的該消息 id。(讀擴散 + 懶刪除)
-
關注人刪除或者修改自己消息時,用戶的收件箱如何刷新:這裏也可以採用回查的方式:由於我們收件箱只存儲 id,消息內容需要回查發件人發件箱的具體消息,所以,回查的時候可以獲取最新消息以此完成刪除、修改的同步。
-
總結:收件箱刷新有兩類,一類是添加,添加都採用寫擴散;一類是刪除和修改,刪除、修改都採用讀擴散。
上述就是我們 Feed 流模型會遇到的問題,已經給出的一個解決方案。當然,不同的業務場景會遇到不同的側重點,上述方案僅僅是一個參考。
03 總體設計
3.1 架構設計
上面我們 Feed 流的底層模型進行了詳細的分析,綜合考慮後,本次開發決定採用以下架構進行開發系統。
上圖可以看出是一個消息發佈的流程交互,通過經過的節點看出我們系統的一個架構。雖然前文討論了很多問題,但其實底層落到 DB 就是幾個表,每個表進行良好的設計後,就可以滿足我們的基礎的性能要求了。而後是我們的系統內部,核心難點是發佈和拉取 Feed 流兩個功能。對這些問題,下面我們也會具體分點介紹設計。
-
數據結構設計
數據結構設計說明:本次系統以面向對象思維進行開發,對 Feed 流中需要的功能進行抽象,抽象了以下數據結構(簡略版本):
-
消息:
屬性:消息標題,消息內容,消息附件,消息類型,消息渠道
方法:豐富消息內容
-
消息發佈處理器:
屬性:發送用戶,發佈配置,消息 id
方法:獲取消息 id,獲取接受者,獲取發佈配置,同步消息,保存消息
-
用戶(消息拉取器):
屬性:用戶 uid,用戶當前操作,用戶當前頁面渠道
方法:獲取關注列表,獲取粉絲列表,查詢發件箱,查詢收件箱(收件箱過濾,包括黑白名單,軟刪除等)
-
發佈配置:
屬性:發佈渠道,發佈方式
方法:獲取發佈方式,獲取發佈渠道
上述抽象類的類圖參考示意圖(非完整版)
3.2 存儲和緩存設計
實現消息推送邏輯,需要將信息進行存儲,下面是存儲表的設計:
1、消息表(消息發佈表):
2、收件箱:採用 redis 的 zset 進行存儲,key 是 “接收者 uid+channelid”,value 爲 “值:發件人 uid + 消息 id,score:發佈時間戳” 。這樣設計,可以將計算下沉,每次收件箱出現消息的刷新的時候,都會自行排序。下面的 redis 的 zset 的圖示:
3、發佈配置表(一般的 feed 流可以不考慮這個,我這是後期打算擴展做成消息推送系統,這裏也可以提供給大家參考。):
4、關注關係表:
3.3 核心業務流程
發佈 Feed 流程
-
當你發佈一條 Feed 消息的時候,流程是這樣的:
-
Feed 消息先進入一個隊列服務。
-
先從關注列表中讀取到自己的粉絲列表,以及判斷自己是否是大 V。
-
將自己的 Feed 消息寫入個人頁 Timeline(發件箱)。
-
如果是大 V,此時拉取活躍用戶;如果是普通用戶,則拉取自己的所有粉絲用戶。然後將自己的 Feed 消息同步寫給自己的粉絲,同步的內容爲 Feed ID。
-
發佈 Feed 的流程到此結束。
讀取 Feed 流流程
當刷新自己的 Feed 流的時候,流程是這樣的:
-
判斷自己是否是活躍用戶,如果不是,去讀取自己關注的大 V 列表。
-
去讀取自己的收件箱,範圍起始位置是上次讀取到的最新 Feed 的 ID,結束位置可以使當前時間,也可以是 MAX。然後通過查詢出來的 FeesId 反查 Feed 內容,並且把已經軟刪除的數據剔除出去。
-
如果有拉取到關注的大 V 列表,則再次併發讀取每一個大 V 的發件箱,如果關注了 10 個大 V,那麼則需要 10 次訪問。
-
合併 2 和 3 步的結果,然後按時間排序,返回給用戶。
至此,使用推拉結合,冷熱分離方式的 Feed 流發佈,讀取 Feed 流的流程都結束了。
核心的發佈 Feed、拉取 Feed 流的總體交互圖如下:
04 總結
相信看了本文以後,對於如何實現一個較爲可靠,性能相對有保證的 Feed 流系統,你已經有了一定的瞭解。那麼,本次 Feed 流的小結到此爲止。
原創作者|彭玉翔
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/CyXO13C3zQLoa-p7N2qPMg