<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          如何設(shè)計(jì) Feed 流系統(tǒng)?

          共 5856字,需瀏覽 12分鐘

           ·

          2022-11-14 23:47

          說(shuō)到 Feed 流,大家肯定都不陌生,我們每天刷的朋友圈,還有各種新聞?lì)?APP,都是采用的這種方式。

          那這樣的系統(tǒng)應(yīng)該如何設(shè)計(jì)呢?在開(kāi)發(fā)過(guò)程中有哪些點(diǎn)需要注意呢?最近正好看到一篇相關(guān)的文章,分享給大家。

          轉(zhuǎn)載信息如下:

          作者:Finley
          鏈接:https://www.cnblogs.com/Finley/p/16857008.html

          Feed 流產(chǎn)品在我們手機(jī) APP 中幾乎無(wú)處不在,比如微信朋友圈、新浪微博、今日頭條等。只要大拇指不停地往下劃手機(jī)屏幕,就有一條條的信息不斷涌現(xiàn)出來(lái)。就像給寵物喂食一樣,只要它吃光了就要不斷再往里加,故此得名 Feed(飼養(yǎng))。

          Feed 流產(chǎn)品一般有兩種形態(tài),一種是基于算法推薦,另一種是基于關(guān)注關(guān)系并按時(shí)間排列?!戈P(guān)注頁(yè)」這種按時(shí)間排序的 Feed 流也被為 Timeline?!戈P(guān)注頁(yè)」自然的也被稱(chēng)為「關(guān)注 Timeline」, 存放自己發(fā)送過(guò)的 Feed 的頁(yè)面被稱(chēng)為「?jìng)€(gè)人 Timeline」 比如微博的個(gè)人頁(yè)。

          就是這么個(gè) Feed 流系統(tǒng)讓「淘金網(wǎng)」的工程師分成兩派吵作一團(tuán),一直吵到了小明辦公室。

          推與拉之爭(zhēng)

          拉模型

          一部分工程師認(rèn)為應(yīng)該在查詢(xún)時(shí)首先查詢(xún)用戶(hù)關(guān)注的所有創(chuàng)作者 uid,然后查詢(xún)他們發(fā)布的所有文章,最后按照發(fā)布時(shí)間降序排列。

          使用拉模型方案用戶(hù)每打開(kāi)一次「關(guān)注頁(yè)」系統(tǒng)就需要讀取 N 個(gè)人的文章(N 為用戶(hù)關(guān)注的作者數(shù)), 因此拉模型也被稱(chēng)為讀擴(kuò)散。

          拉模型不需要存儲(chǔ)額外的數(shù)據(jù),而且實(shí)現(xiàn)比較簡(jiǎn)單:發(fā)布文章時(shí)只需要寫(xiě)入一條 articles 記錄,用戶(hù)關(guān)注(或取消關(guān)注)也只需要增刪一條 followings 記錄。特別是當(dāng)粉絲數(shù)特別多的頭部作者發(fā)布內(nèi)容時(shí)不需要進(jìn)行特殊處理,等到讀者進(jìn)入關(guān)注頁(yè)時(shí)再計(jì)算就行了。

          拉模型的問(wèn)題同樣也非常明顯,每次閱讀「關(guān)注頁(yè)」都需要進(jìn)行大量讀取和一次重新排序操作,若用戶(hù)關(guān)注的人數(shù)比較多一次拉取的耗時(shí)會(huì)長(zhǎng)到難以接受的地步。

          推模型

          另一部分工程師認(rèn)為在創(chuàng)作者發(fā)布文章時(shí)就應(yīng)該將新文章寫(xiě)入到粉絲的關(guān)注 Timeline,用戶(hù)每次閱讀只需要到自己的關(guān)注 Timeline 拉取就可以了:

          使用推模型方案創(chuàng)作者每次發(fā)布新文章系統(tǒng)就需要寫(xiě)入 M 條數(shù)據(jù)(M 為創(chuàng)作者的粉絲數(shù)),因此推模型也被稱(chēng)為寫(xiě)擴(kuò)散。推模型的好處在于拉取操作簡(jiǎn)單高效,但是缺點(diǎn)一樣非常突出。

          首先,在每篇文章要寫(xiě)入 M 條數(shù)據(jù),在如此恐怖的放大倍率下關(guān)注 Timeline 的總體數(shù)據(jù)量將達(dá)到一個(gè)驚人數(shù)字。而粉絲數(shù)有幾十萬(wàn)甚至上百萬(wàn)的頭部創(chuàng)作者每次發(fā)布文章時(shí)巨大的寫(xiě)入量都會(huì)導(dǎo)致服務(wù)器地震。

          通常為了發(fā)布者的體驗(yàn)文章成功寫(xiě)入就向前端返回成功,然后通過(guò)消息隊(duì)列異步地向粉絲的關(guān)注 Timeline 推送文章。

          其次,推模型的邏輯要復(fù)雜很多,不僅發(fā)布新文章時(shí)需要實(shí)現(xiàn)相關(guān)邏輯,新增關(guān)注或者取消關(guān)注時(shí)也要各自實(shí)現(xiàn)相應(yīng)的邏輯,聽(tīng)上去就要加很多班。

          在線推,離線拉

          在做出最終決定之前我們先來(lái)對(duì)比一下推拉模型:


          優(yōu)點(diǎn)缺點(diǎn)
          讀取操作快邏輯復(fù)雜,消耗大量存儲(chǔ)空間,粉絲數(shù)多的時(shí)候會(huì)是災(zāi)難
          邏輯簡(jiǎn)單,節(jié)約存儲(chǔ)空間讀取效率低下,關(guān)注人數(shù)多的時(shí)候會(huì)出現(xiàn)災(zāi)難

          雖然乍看上去拉模型優(yōu)點(diǎn)多多,但是 Feed 流是一個(gè)極度讀寫(xiě)不平衡的場(chǎng)景,讀請(qǐng)求數(shù)比寫(xiě)請(qǐng)求數(shù)高兩個(gè)數(shù)量級(jí)也不罕見(jiàn),這使得拉模型消耗的 CPU 等資源反而更高。

          此外推送可以慢慢進(jìn)行,但是用戶(hù)很難容忍打開(kāi)頁(yè)面時(shí)需要等待很長(zhǎng)時(shí)間才能看到內(nèi)容(很長(zhǎng):指等一秒鐘就覺(jué)得卡)。因此拉模型讀取效率低下的缺點(diǎn)使得它的應(yīng)用受到了極大限制。

          我們回過(guò)頭來(lái)看困擾推模型的這個(gè)問(wèn)題「粉絲數(shù)多的時(shí)候會(huì)是災(zāi)難」,我們真的需要將文章推送給作者的每一位粉絲嗎?

          仔細(xì)想想這也沒(méi)有必要,我們知道粉絲群體中活躍用戶(hù)是有限的,我們完全可以只推送給活躍粉絲,不給那些已經(jīng)幾個(gè)月沒(méi)有啟動(dòng) App 的用戶(hù)推送新文章。

          至于不活躍的用戶(hù),在他們回歸后使用拉模型重新構(gòu)建一下關(guān)注 Timeline 就好了。因?yàn)椴换钴S用戶(hù)回歸是一個(gè)頻率很低的事件,我們有充足的計(jì)算資源使用拉模型進(jìn)行計(jì)算。

          因?yàn)榛钴S用戶(hù)和不活躍用戶(hù)常常被叫做「在線用戶(hù)」和「離線用戶(hù)」,所以這種通過(guò)推拉結(jié)合處理頭部作者發(fā)布內(nèi)容的方式也被稱(chēng)為「在線推,離線拉」。

          優(yōu)化存儲(chǔ)

          在前面的討論中不管是「關(guān)注 Timeline」還是關(guān)注關(guān)系等數(shù)據(jù)我們都存儲(chǔ)在了 MySQL 中。拉模型可以用 SQL 描述為:

          SELECT *
          FROM articles
          WHERE author_uid IN (
           SELECT following_uid
           FROM followings
           WHERE uid = ?
          )
          ORDER BY create_time DESC
          LIMIT 20;

          通過(guò)查看執(zhí)行計(jì)劃我們發(fā)現(xiàn),臨時(shí)表去重+Filesort、這個(gè)查詢(xún)疊了不少 debuff:

          至于推模型,關(guān)注 Timeline 巨大的數(shù)據(jù)量和讀寫(xiě)負(fù)載就不是 MySQL 能扛得住的。

          遇事不決上 Redis

          顯然關(guān)注 Timeline 的數(shù)據(jù)是可以通過(guò) articles 和 followings 兩張表中數(shù)據(jù)重建的,其存在只是為了提高讀取操作的效率。這有沒(méi)有讓你想起另外一個(gè)東西?

          沒(méi)錯(cuò)!就是緩存,關(guān)注 Timeline 實(shí)質(zhì)上就是一個(gè)緩存,也就是說(shuō)關(guān)注 Timeline 與緩存一樣只需要暫時(shí)存儲(chǔ)熱門(mén)數(shù)據(jù)。

          我們可以給關(guān)注 Timeline 存儲(chǔ)設(shè)置過(guò)期時(shí)間,若用戶(hù)一段時(shí)間沒(méi)有打開(kāi) App 他的關(guān)注 Timeline 存儲(chǔ)將被過(guò)期釋放,在他回歸之后通過(guò)拉模型重建即可。

          在使用「在線推,離線拉」策略時(shí)我們需要判斷用戶(hù)是否在線,在為 Timeline 設(shè)置了過(guò)期時(shí)間后,Timeline 緩存是否存在本身即可以作為用戶(hù)是否在線的標(biāo)志。

          就像很少有人翻看三天前的朋友圈一樣,用戶(hù)總是關(guān)心關(guān)注頁(yè)中最新的內(nèi)容,所以關(guān)注 Timeline 中也沒(méi)有必要存儲(chǔ)完整的數(shù)據(jù)只需要存儲(chǔ)最近一段時(shí)間即可,舊數(shù)據(jù)等用戶(hù)翻閱時(shí)再構(gòu)建就行了。

          魯迅有句話說(shuō)得好 ——「遇事不決上 Redis」,既然是緩存那么就是 Redis 的用武之地了。

          Redis 中有序數(shù)據(jù)結(jié)構(gòu)有列表 List 和有序集合 SortedSet 兩種,對(duì)于關(guān)注 Timeline 這種需要按時(shí)間排列且禁止重復(fù)的場(chǎng)景當(dāng)然閉著眼睛選 SortedSet。

          將 article_id 作為有序集合的 member、發(fā)布時(shí)間戳作為 score, 關(guān)注 Timeline 以及個(gè)人 Timeline 都可以緩存起來(lái)。

          在推送新 Feed 的時(shí)候只需要對(duì)目標(biāo) Timeline 的 SortedSet 進(jìn)行 ZAdd 操作。若緩存中沒(méi)有某個(gè) Timeline 的數(shù)據(jù)就使用拉模型進(jìn)行重建。

          在使用消息隊(duì)列進(jìn)行推送時(shí)經(jīng)常出現(xiàn)由于網(wǎng)絡(luò)延遲等原因?qū)е轮貜?fù)推送的情況,所幸 article_id 是唯一的,即使出現(xiàn)了重復(fù)推送 Timeline 中也不會(huì)出現(xiàn)重復(fù)內(nèi)容。

          這種重復(fù)操作不影響結(jié)果的特性有個(gè)高大上的名字 ——— 冪等性

          當(dāng) Redis 中沒(méi)有某個(gè) Timeline 的緩存時(shí)我們無(wú)法判斷是緩存失效了,還是這個(gè)用戶(hù)的 Timeline 本來(lái)就是空的。我們只能通過(guò)讀取 MySQL 中的數(shù)據(jù)才能進(jìn)行判斷,這就是經(jīng)典的緩存穿透問(wèn)題。

          對(duì)于時(shí)間線這種集合式的還存在第二類(lèi)緩存穿透問(wèn)題,正如我們剛剛提到的 Redis 中通常只存儲(chǔ)最近一段時(shí)間的 Timeline,當(dāng)我們讀完了 Redis 中的數(shù)據(jù)之后無(wú)法判斷數(shù)據(jù)庫(kù)中是否還有更舊的數(shù)據(jù)。

          這兩類(lèi)問(wèn)題的解決方案是一樣的,我們可以在 SortedSet 中放一個(gè) NoMore 的標(biāo)志,表示數(shù)據(jù)庫(kù)中沒(méi)有更多數(shù)據(jù)了。對(duì)于 Timeline 本來(lái)為空的用戶(hù)來(lái)說(shuō),他們的 SortedSet 中只有一個(gè) NoMore 標(biāo)志:

          最后一點(diǎn):拉取操作要注意保持原子性不要將重建了一半的 Timeline 暴露出去:

          總結(jié)一下使用 Redis 做關(guān)注時(shí)間線的要點(diǎn):

          • 使用 SortedSet 結(jié)構(gòu)存儲(chǔ),Member 為 FeedID,Score 為時(shí)間戳
          • 給緩存設(shè)置自動(dòng)過(guò)期時(shí)間,不活躍用戶(hù)的緩存會(huì)自動(dòng)被清除。使用「在線推,離線拉」時(shí)只給 Timeline 緩存未失效的用戶(hù)推送即可
          • 需要在緩存中放置標(biāo)志來(lái)防止緩存擊穿

          一層緩存不夠再來(lái)一層

          雖然 Redis 可以方便的實(shí)現(xiàn)高性能的關(guān)注 Timeline 系統(tǒng),但是內(nèi)存空間總是十分珍貴的,我們常常沒(méi)有足夠的內(nèi)存為活躍用戶(hù)緩存關(guān)注 Timeline。

          緩存不足是計(jì)算機(jī)領(lǐng)域的經(jīng)典問(wèn)題了,問(wèn)問(wèn)你的 CPU 它就會(huì)告訴你答案 —— 一級(jí)緩存不夠用就做二級(jí)緩存,L1、L2、L3 都用光了我才會(huì)用內(nèi)存。

          只要是支持有序結(jié)構(gòu)的 NewSQL 數(shù)據(jù)庫(kù)比如 Cassandra、HBase 都可以勝任 Redis 的二級(jí)緩存:

          附上一條 Cassandra 的表結(jié)構(gòu)描述:

          -- Cassandra 是一個(gè) Map<PartionKey, SortedMap<ClusteringKey, OtherColumns>> 結(jié)構(gòu)
          -- 必須指定一個(gè) PartionKey,順序也只能按照 ClusteringKey 的順序排列
          -- 這里 PartionKey 是 uid, ClusteringKey 是 publish_time + article_id
          -- publish_time 必須寫(xiě)在 ClusteringKey 第一列才能按照它進(jìn)行排序
          -- article_id 也寫(xiě)進(jìn) ClusteringKey 是為了防止 publish_time 出現(xiàn)重復(fù)
          CREATE TABLE taojin.following_timelins (
              uid bigint,
              publish_time timestamp,
              article_id bigin,
              PRIMARY KEY (uid, publish_time, article_id) 
          WITH default_time_to_live = 60 * 24 * 60 * 60;

          這里還是要提醒一下,每多一層緩存便要多考慮一層一致性問(wèn)題,到底要不要做多級(jí)緩存需要仔細(xì)權(quán)衡。

          還有一些細(xì)節(jié)要優(yōu)化

          分頁(yè)器

          Feed 流是一個(gè)動(dòng)態(tài)的列表,列表內(nèi)容會(huì)隨著時(shí)間不斷變化。傳統(tǒng)的 limit + offset 分頁(yè)器會(huì)有一些問(wèn)題:

          在 T1 時(shí)刻讀取了第一頁(yè),T2時(shí)刻有人新發(fā)表了 article 11 ,如果這時(shí)來(lái)拉取第二頁(yè),會(huì)導(dǎo)致 article 6 在第一頁(yè)和第二頁(yè)都被返回了。

          解決這個(gè)問(wèn)題的方法是根據(jù)上一頁(yè)最后一條 Feed 的 ID 來(lái)拉取下一頁(yè):

          使用 Feed ID 來(lái)分頁(yè)需要先根據(jù) ID 查找 Feed,然后再根據(jù) Feed 的發(fā)布時(shí)間讀取下一頁(yè),流程比較麻煩。若作為分頁(yè)游標(biāo)的 Feed 被刪除了,就更麻煩了。

          筆者更傾向于使用時(shí)間戳來(lái)作為游標(biāo):

          使用時(shí)間戳不可避免的會(huì)出現(xiàn)兩條 Feed 時(shí)間戳相同的問(wèn)題, 這會(huì)讓我們的分頁(yè)器不知所措。

          這里有個(gè)小技巧是將 Feed id 作為 score 的小數(shù)部分,比如 article 11 在 2022-10-27 13:55:11 發(fā)布(時(shí)間戳 1666850112), 那么它的 score 為 1666850112.11 小數(shù)部分既不影響按時(shí)間排序又避免了重復(fù)。

          大規(guī)模推送

          雖然我們已經(jīng)將推送 Feed 的任務(wù)轉(zhuǎn)移給了 MQ Worker 來(lái)處理,但面對(duì)將 Feed 推送給上百萬(wàn)粉絲這樣龐大的任務(wù), 單機(jī)的 Worker 還是很難處理。而且一旦處理中途崩潰就需要全部重新開(kāi)始。

          我們可以將大型推送任務(wù)拆分成多個(gè)子任務(wù),通過(guò)消息隊(duì)列發(fā)送到多臺(tái) MQ Worker 上進(jìn)行處理。

          因?yàn)樨?fù)責(zé)拆分任務(wù)的 Dispatcher 只需要掃描粉絲列表負(fù)擔(dān)和故障概率大大減輕。若某個(gè)推送子任務(wù)失敗 MQ 會(huì)自動(dòng)進(jìn)行重試,也無(wú)需我們擔(dān)心。

          總結(jié)

          至此,我們完成了一個(gè)關(guān)注 Feed 流系統(tǒng)的設(shè)計(jì)??偨Y(jié)一下本文我們都討論了哪些內(nèi)容:

          • 基本模型有兩種。推模型:發(fā)布新 Feed 時(shí)推送到每個(gè)粉絲的 Timeline;拉模型:打開(kāi) Timeline 時(shí)拉取所有關(guān)注的人發(fā)布的 Feed,重新聚合成粉絲的 Timeline。推模型讀取快,但是推送慢,粉絲數(shù)多的時(shí)候峰值負(fù)載很重。拉模型沒(méi)有峰值問(wèn)題,但是讀取很慢用戶(hù)打開(kāi) Timeline 時(shí)要等待很久,讀極多寫(xiě)極少的環(huán)境中消耗的計(jì)算資源更多。
          • 頭部用戶(hù)的幾十上百萬(wàn)粉絲中活躍用戶(hù)比例很少,所以我們可以只將他們的新 Feed 推送給活躍用戶(hù),不活躍用戶(hù)等回歸時(shí)再使用拉模型重建 Timeline。即通過(guò)「在線推、離線拉」的模式解決推模型的峰值問(wèn)題。
          • 雖然關(guān)注 Timeline 數(shù)據(jù)很多但實(shí)際上是一種緩存,沒(méi)必要全部存儲(chǔ)。我們按照緩存的思路只存儲(chǔ)活躍用戶(hù)、最近一段時(shí)間的數(shù)據(jù)即可,沒(méi)有緩存的數(shù)據(jù)在用戶(hù)閱讀時(shí)再通過(guò)拉模型重建。
          • Timeline 推薦使用 Redis 的 SortedSet 結(jié)構(gòu)存儲(chǔ),Member 為 FeedID,Score 為時(shí)間戳。給緩存設(shè)置自動(dòng)過(guò)期時(shí)間,不活躍用戶(hù)的緩存會(huì)自動(dòng)被清除。使用「在線推,離線拉」時(shí)只給 Timeline 緩存未失效的用戶(hù)推送即可。
          • 在 Redis 內(nèi)存不足時(shí)可以使用 Cassandra 作為 Redis 的二級(jí)緩存。

          以上就是本文的全部?jī)?nèi)容,如果覺(jué)得還不錯(cuò)的話歡迎點(diǎn)贊,轉(zhuǎn)發(fā)關(guān)注,感謝支持。


          推薦閱讀:

          瀏覽 54
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  热久久9| 婷婷精品在线 | 在线观看国产一级片 | 国产操逼的视频 | 日韩精品首页 |