iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0

昨天做出了最陽春的 PokeThreads,但故意跳過了一個問題:

GET /feed

這支 API 收到 Request 之後,到底該回傳哪些貼文?

Feed 要顯示誰的東西、用什麼順序

先把問題講具體一點。假設 Pikachu 追蹤了 Charmander、Squirtle、Bulbasaur,那麼 Pikachu 打開 PokeThreads 的時候:

  • 應該看到誰的貼文?
  • 這些貼文該怎麼排序?

直覺的答案很單純:

找出 Pikachu 追蹤的人,把他們的貼文抓出來,按時間排序,新的放前面。

這個題型其實不陌生,跟 LeetCode 355 Design Twitter 要處理的問題幾乎一樣:發文、Follow、Unfollow、取得 News Feed,核心都是「找出自己與追蹤對象的貼文,按時間排序取前幾筆」。

最簡單可行實作

先假設有這些資料:

Follow

Pikachu → Charmander
Pikachu → Squirtle
Pikachu → Bulbasaur

Post

Charmander → Post A, Post B
Squirtle   → Post C
Bulbasaur  → Post D, Post E

要組出 Pikachu 的 Feed,可以拆成三步:

Step 1:找出 Pikachu 追蹤了誰

Pikachu
 └── Following: [Charmander, Squirtle, Bulbasaur]

Step 2:找出這些人的貼文

Charmander → Post A, Post B
Squirtle   → Post C
Bulbasaur  → Post D, Post E

Step 3:全部混在一起,按時間排序

Post E, Post C, Post B, Post D, Post A   (由新到舊)

畫成流程就是:

Feed 產生流程圖

寫成 SQL 大概長這樣:

SELECT *
FROM posts
WHERE author_id IN (
    SELECT followee
    FROM follows
    WHERE follower = :user_id
)
ORDER BY created_at DESC
LIMIT 20;

這支 Query 一次做了三件事:

  1. 找出追蹤對象
  2. 抓出他們的貼文
  3. 依時間排序後取前幾筆

如果要把自己的貼文也放進 Feed,只要把 :user_id 一併加進 author_id 的條件即可。

這種「使用者打開 Feed 的當下,才去即時查詢、組出結果」的做法,就是最基礎的 Fan-out on Read

Feed 不能一次全部塞給前端

上面的 Query 加了 LIMIT 20,但使用者往下滑的時候呢?總不能每次都把 Pikachu 追蹤對象的所有貼文一次撈出來,這就需要 Pagination(分頁)

Offset Pagination

最直覺的做法:

GET /feed?offset=0&limit=20
GET /feed?offset=20&limit=20
GET /feed?offset=40&limit=20

對應的 SQL:

SELECT *
FROM posts
ORDER BY created_at DESC
LIMIT 20
OFFSET 20;

概念就是「跳過前 N 筆,再取下一批」,很好懂,但 Feed 有個麻煩的特性:它會一直有新資料進來。

假設 Pikachu 在 10:00 拿到第一頁 [Post A, Post B, Post C],這時候 Charmander 發了新文章 Post X,Feed 變成 [Post X, Post A, Post B, Post C]。如果 Pikachu 接著用 OFFSET 3 拿下一頁,因為前面多插入了一筆資料,位置全部往後移了一格,就可能重複拿到 Post C,或漏掉某一篇。

Cursor Pagination

另一種做法不是「跳過幾筆」,而是「從上次拿到的最後一筆位置,繼續往後拿」。

第一次:

GET /feed?limit=20

Server 回傳:

{
  "posts": [...],
  "next_cursor": "abc123"
}

下一次帶著 cursor 繼續拿:

GET /feed?cursor=abc123&limit=20

Cursor 通常不是單純的頁碼,而是「上一筆資料的定位資訊」,例如用 created_at + id 當作依據:

SELECT *
FROM posts
WHERE (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;

重點是 Server 不需要知道「現在是第幾頁」,只需要知道「上一批資料停在哪裡」,新資料插進來也不會打亂已經翻過的頁面。

業界常見方法比較

Offset Pagination Cursor Pagination
概念 跳過前 N 筆 從某個位置繼續
實作難度 簡單 稍微複雜
面對動態資料 容易錯位(重複 / 漏資料) 相對穩定
支援跳頁 可以 不適合
常見場景 後台列表、資料量小的頁面 動態 Feed、無限捲動

像 PokeThreads 這種持續有新內容產生的 Feed,業界普遍會選擇 Cursor Pagination,而不是 Offset Pagination。

與其他元件串接

目前的 Feed 完全建立在昨天的架構上:

瀏覽器 -> GET /feed -> Server -> Database

Server 收到 Request 後,靠 Follow 資料表找出追蹤對象,靠 Post 資料表撈出貼文,全部工作都交給 Database 的一次 Query 完成。

小結

今天把 Feed 從「一個沒有實作的 API」,補成一個看得懂、也解釋得出為什麼這樣做的功能:

找出 Follow 對象
      ↓
撈出他們的 Post
      ↓
依時間排序
      ↓
用 Cursor 做分頁

不過這個 GET /feed 目前回傳的還是整包固定格式的 JSON,這種溝通方式在單純的 Web 前端上很夠用,但如果之後要支援更多不同型態的前端(例如只想要某幾個欄位、或是想一次拿到使用者資訊加上貼文),現在這種 API 設計方式撐不撐得住,就要看前後端之間到底怎麼溝通,這是明天要討論的 REST vs GraphQL。


上一篇
Day 2 設計最簡單的社群網站
下一篇
Day 4 REST vs GraphQL:前後端到底該怎麼聊天?
系列文
系統設計就像九頭蛇:打造社群網站的 30 天10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言