01是什麼決定誰出現在我的牌堆裡——偏好、距離,還是兩者都要?
使用者設定偏好(年齡範圍、興趣)與一個最大距離;牌堆只包含同時滿足兩者的候選人。
免費完整指南
設計Tinder。請涵蓋Swipe ingestion as a high-volume, append-only event write path (like/pass events) decoupled from match detection, Match detection: checking whether A likes B AND B likes A, where that...
面試節奏保持精簡,讓頁面能把注意力花在真正的設計決策上。
不要只是陳述需求,要主動問出來。每張卡把設計限制和一句你可以在畫架構前說出口的釐清問句配成一組。
01是什麼決定誰出現在我的牌堆裡——偏好、距離,還是兩者都要?
使用者設定偏好(年齡範圍、興趣)與一個最大距離;牌堆只包含同時滿足兩者的候選人。
02滑動是一次一張嗎,一個檔案有可能再回來嗎?
使用者一次一張、對一個檔案往左或往右滑,而滑過的檔案永遠不會再出現——重複出現的檔案會讓人覺得系統壞了。
03在互相往右滑的那一刻會發生什麼?
兩位使用者都立刻收到配對通知——在第二個往右滑落地的那一刻,而不是幾分鐘後。
04使用者可以撤銷一次往左滑(rewind)嗎?
在一個短窗口內可以——而且被 rewind 的檔案可以再次出現在牌堆裡。
05取消配對做了什麼?
取消配對把配對移除並關閉聊天,是同一個動作——一個撤銷到一半的配對(聊天還活著、配對沒了)是一個信任 bug。
06使用者在中途改了偏好——他們的牌堆會怎樣?
新偏好立即生效——下一個顯示的檔案就必須滿足更新後的過濾條件,而不是舊的。
範圍外相片上傳管線 · 配對之後的訊息 · 付費功能(super swipe、boost)
01如果兩個人幾乎在同一瞬間互相往右滑,能保證恰好一個配對嗎——絕不是零個、也絕不是兩個?
恰好一個配對,並立即偵測到——絕不是零個、絕不是兩個,無論時間點多接近。
02我們是為多大的滑動量做規劃?
2000 萬日活躍使用者(DAU)× 每人約 100 次滑動 ≈ 每日 20 億次滑動——平均約每秒 23K 次滑動。
03牌堆必須多快出現?
300 ms 以內。
04「絕不重複出現」這條規則有多硬?
很硬:它必須跨 session、跨裝置、跨重新安裝都成立——重複出現一個檔案,比悄悄跳過一個候選人更糟。
05其他使用者到底能看到什麼位置資料?
只給一個粗略的距離桶(「約 5 km 外」)——原始 lat/lng 在任何 payload 裡都永遠不離開後端。
真實面試探得比一份整齊清單深得多。這些範圍問句,區分出真正拷問問題的人和只是背誦的人。
把每個估算都當成一種壓力,用來合理化一個元件:快取、佇列、分片、副本、worker pool 或退路。
滑動寫入率
2000 萬日活躍使用者(DAU)× 每人每日約 100 次滑動2B ÷ 86,400 s ≈ 23K swipes/s 平均,晚上 ×3-5 ≈ 100K/s 尖峰
滑動需要一個寫入優化的儲存;在這個速率下,互相檢查每次滑動必須維持 O(1)。
看過的檔案記憶體
假設一個重度使用者一生滑過約 5 萬個檔案Bloom filter 在 1% false positive 下 ≈ 每筆 10 bits → 50K × 10 b ≈ 每位使用者 62 KB
整個「絕不重複出現」的防護每位使用者只要幾 KB——精確的滑動歷史留在磁碟上。
牌堆預先計算成本
假設每位活躍使用者約 200 個候選人的牌堆,剩不多時刷新20M users × 200 IDs × 8 B ≈ 32 GB
為每位活躍使用者預先算好的牌堆放得進一層快取——這正是讓 <300 ms 可行的原因。
為什麼不即時查詢
一個密集的城市,在一個最大距離的圓圈內可能容納 2000 萬 DAU 的大約 1%——約 200K 名使用者。每一個都還要經過地理、年齡、偏好、與未看過的過濾。20 萬個候選人 × 每個約 1-2 µs 做過濾交集與看過清單檢查 ≈ 每次請求 200-400 ms——排名都還沒開始,整個 300 ms 預算就燒光了
feed 的預算排除了在請求路徑上跑即時查詢。改成預先算好牌組,並在背景補牌。
配對檢查成本
每次往右滑都檢查反方向每次滑動 1 次原子的 read-modify-write ≈ 記憶體中次毫秒
把這一對的滑動狀態放在一起(同一個 key)正是讓檢查維持一個操作的關鍵。
決策範例
尖峰每秒十萬次滑動。唯一絕不能出錯的時刻,是兩個人同時對彼此按下 yes。
我會把每一對的滑動狀態存在同一個 key 底下——兩個 user ID,小的在前。然後我把「記錄這次滑動」和「檢查反向滑動」做成一個原子操作。Redis 裡的一段 Lua 腳本一步就完成這個 read-modify-write,而持久副本在它背後寫進 swipe store。牌組按每個使用者預先算好,並由背景的地理查詢來補牌。每個使用者看過的檔案住在一個 Bloom filter 裡,所以「絕不重複顯示」只花幾 KB,而不是掃一遍歷史。
我不會做的事:把記錄滑動和檢查反向分成兩個獨立步驟。兩個同時的 yes 滑動會各自錯過對方,配對永遠不會觸發。那個「先檢查再動作」的空隙,跟把演唱會座位賣兩次的競爭條件是同一個——檢查和寫入必須是一個操作。我也不會把 feed 做成每請求一次的即時地理查詢,因為那會把整個 300 ms 的預算全燒在索引查找上。
如果 Redis 變成瓶頸,或叢集 failover 變得太複雜,我會把原子性搬進儲存層。一個複合分區 key(smaller_id:larger_id)讓兩次滑動都落在同一個分區。在那裡,一個輕量交易就能涵蓋這一對——用資料庫自己的 compare-and-set,比 Redis 慢,但是內建的。它每次滑動的成本更高,但省掉一個會動的系統。
先看一張完整的圖,再把每條路徑各自畫成一張圖 —— 寫入路徑與讀取路徑承載不同流量,合理化不同的元件。
完整全貌
滑動路徑是一致性關鍵的脊柱;feed 路徑是讀取優化且預先算好的。虛線 = 從持久滑動非同步維護,快取遺失後可重建。
路徑 1
一個原子操作記錄這次滑動並檢查反向。持久寫入接在後面。如果一個 Redis 節點掛掉,它從持久儲存重播,最多只丟掉最後幾筆還沒刷出的滑動——這是為了滑動路徑的速度而接受的取捨。
路徑 2
請求路徑只讀牌堆。當它剩不多時,背景查詢把它補滿,透過 Bloom filter 排除看過的檔案——false positive 會跳過一個候選人,但絕不重複一個。
在最佳化之前,先讓契約可被檢視:端點、實體、擁有權、重試與狀態。
POST/profile
請求{ age_min, age_max, distance_km, interested_in }
回應200 profile
身分來自 session token。偏好變更會讓預先算好的牌堆失效。
GET/feed
回應200 User[] (next slice of the deck)
服務預先算好的牌堆;當它剩不多時,一次新的 geo+偏好查詢把它補滿。位置來自 session context,不是查詢參數。
POST/swipe/{target_user_id}
請求{ decision: yes | no }
回應200 { matched: boolean } — matched:true fires both notifications
那個原子步驟:把滑動記錄下來,並在同一個操作裡檢查反向的滑動。
核心實體
Useruser_id (PK) · profile · preferences · geo_cell (coarse)
位置以一個粗略的 geo cell 儲存以供配對;精確座標永遠不會被服務給其他客戶端。
Swipeswiping_user · target_user · decision: yes/no · swiped_at
持久寫入 swipe store;同時折進滑動者「看過的檔案」Bloom filter。
Matchmatch_id (PK) · user_a · user_b · matched_at
在面試最後三分之一挑一條路線。每條路線給你主題、它該回答的面試官問題,以及要避免的失敗模式。
兩位使用者在同一個 10 ms 內對彼此往右滑。走一遍產生一個配對、而非零個的確切操作。
做成單一原子操作。把兩個使用者的滑動狀態存在同一個 key 底下,兩個 ID 用較小的排前面。在同一個 Redis Lua script 裡記錄這次滑動,並檢查反向的那次滑動。第二次滑動一定看得到第一次,所以剛好只會觸發一次配對。
分兩步 read-then-write——兩次滑動各自漏掉對方,配對就悄悄永不發生。
你如何在不掃描歷史的前提下,把 5 萬個滑過的檔案從每次牌堆補滿中排除?
每個使用者用一個 Bloom filter,記錄他滑過的每一個檔案。5 萬筆大約 62 KB。用它篩每一個 deck 候選;偽陽性頂多跳過一個候選,絕不會讓人重複出現。Bloom filter 沒辦法反學習,所以把最近幾次滑動留在一個小的精確 buffer 裡——rewind 靠的就是它。
把 Bloom false positive 當成 bug——跳過一個候選人是設計好的成本;重複一個才是失敗。
一位活躍使用者把整個預先算好的牌堆都滑完了。什麼把它補滿、它有多新鮮、他們又看到什麼延遲?
每個請求都從預先算好的 deck 快取回應。到達低水位線時就非同步觸發補充,所以一個背景的地理加偏好查詢,會在使用者滑到底之前把 200 個候選的 deck 重建好。就是這個預先計算,讓即時的多重篩選查詢不出現在 300 ms 的路徑上。偏好一改,deck 就丟掉,再用同樣方式補充。
在空牌堆的請求上同步補滿——300 ms 預算在查詢開始前就沒了。
配對需要距離,使用者卻絕不能看到座標。精度在哪裡被丟掉,API 實際回傳什麼?
精確座標只留在 server 端,用來挑候選。後端算出距離,把它四捨五入成一個粗略的區間,像「約 5 公里外」,才放進任何回應裡。任何 payload 的任何欄位,都不會帶 lat/lng。在客戶端做四捨五入是演戲——payload 本身就是漏洞。
把原始 lat/lng 送給客戶端、再在 UI 裡四捨五入——payload 本身就是那個洩漏。
一個非常熱門的檔案出現在數百萬個牌堆裡。什麼先變成熱點,你又如何讓曝光保持均衡?
這個檔案的配對 key 和滑動分區會最先變成熱點,所以要把那份狀態分片或做複本。接著給這個檔案一個曝光預算:限制它同時能占住多少個活躍的 deck,隨著滑動消耗掉再補回來。這樣一個熱門檔案就沒辦法塞滿附近每一個 deck,把其他人的曝光機會餓死。
忽略曝光偏斜——沒有上限,熱門檔案會宰制每一個牌堆,互動就崩了。
把 Tinder 大聲講一遍,讓 AI 為你的說明評分。