具代表性的面試主題

後端面試:如何設計在並發寫入下仍一致的游標分頁?

後端中等
OfferCC 編輯團隊發佈 更新

題幹

無限滾動的訂單列表在使用者翻頁時出現重複或漏項。資料仍持續寫入,你會如何設計分頁介面和資料庫查詢?

題幹與適用場景

題目考察後端工程師能否把分頁提升為穩定的資料讀取協定。訂單、評論和日誌會在請求之間插入、刪除或更新;單純 OFFSET 依賴變動位置,深頁還會掃描並丟棄大量資料。回答應涵蓋排序、游標編碼、過濾器綁定、一致性邊界、索引和前後分頁語意。

面試官考察點

強回答會先問是否需要跳頁、總數和即時性,再選擇 keyset/cursor 或 offset。游標必須綁定排序與過濾條件,排序鍵必須唯一、穩定且有索引;伺服器驗證簽名和過期時間,避免客戶端偽造。也要說明新增資料不會讓下一頁重複、刪除如何處理,以及 API 如何返回 next_cursor。

回答前需要釐清的問題

  • 資料按什麼排序?排序欄位會更新嗎,是否有唯一 tie-breaker?
  • 需要上一頁、跳到第 N 頁、總數,還是只支援向前無限滾動?
  • 讀取期間要凍結快照,還是允許最終一致的即時列表?
  • 過濾條件、租戶權限和排序是否必須綁在游標中?游標有效多久?
  • 刪除、軟刪除、權限變化和跨分片查詢如何處理?

30 秒回答框架

「我會用穩定複合排序鍵,例如 (created_at, id),按降序做 keyset 查詢。游標是不透明簽名 token,包含最後一筆的排序值、過濾器雜湊、方向和版本;伺服器驗證後執行 WHERE (created_at,id) < (:time,:id),依賴同序索引。新增記錄留到下一次刷新,不會插入已讀窗口;刪除可能令結果變少,但不會製造重複。若必須絕對一致,再增加 snapshot boundary 並說明成本。」

分步深入解答

第一步:定義列表語意

先確定是歷史稽核列表、即時 feed 還是管理後台。歷史列表通常需要穩定邊界;即時 feed 可允許新記錄只在刷新時出現。不要同時承諾即時、任意跳頁、精確總數和低成本。

第二步:選擇穩定排序鍵

時間戳可能相同或被修改,必須加入唯一 id 作 tie-breaker。排序欄位不能依賴展示文字;會改變順序的欄位應改用不可變建立序列,或說明更新項可能移動。

第三步:設計不透明游標

游標至少含排序值、方向、過濾器雜湊、API 版本和過期時間。用簽名或伺服器儲存防篡改;Base64 只提供編碼,不提供安全性。過濾器改變時拒絕舊游標。

第四步:撰寫 keyset 查詢

降序 (created_at,id) 的下一頁條件是 created_at < t OR (created_at = t AND id < id0),並配合同序複合索引。使用參數化查詢和頁面上限,避免把游標直接拼進 SQL。

第五步:處理動態寫入與刪除

第一頁之後新增的記錄不應插入第二頁,它們在下一次刷新出現。已讀記錄被刪除會讓頁面少一筆,這是可接受語意;若不能漏項,使用 snapshot boundary 或版本化讀取。

第六步:綁定權限和過濾

游標中的過濾器雜湊、租戶和授權範圍必須與請求一致。權限收緊時重新計算,不能用舊游標繞過控管。跨分片時由協調器合併局部游標,並說明放大成本。

第七步:定義 API 回應與錯誤

回應返回 items、next_cursor、has_more 和可選快照識別。游標過期、簽名失敗或過濾器變化要用穩定業務錯誤碼,客戶端清空舊游標回到第一頁。

第八步:用並發測試驗證

測試兩頁之間插入、刪除、同時間戳、過濾器變化、游標篡改和深頁查詢。驗證同一會話無重複,索引命中且延遲不隨頁碼線性增加;記錄重複率、跳過率和 p95 延遲。

查詢偽代碼

sql
SELECT id, created_at, total
FROM orders
WHERE tenant_id = :tenant
  AND (created_at, id) < (:cursor_time, :cursor_id)
ORDER BY created_at DESC, id DESC
LIMIT :page_size;

設計取捨與邊界

需求選擇代價
大表無限滾動keyset cursor不支援任意跳頁
小型後台表offset深頁變慢且動態寫入不穩定
絕對一致結果snapshot boundary快照儲存與清理成本
精確總數獨立 count 或非同步統計額外查詢且可能過時

游標解決位置穩定與查詢效率,不自動解決跨頁業務去重、權限變化或資料更新移動。搜尋結果還要綁定查詢版本;聚合結果可能需要資料庫快照。

落地計畫與證據

先選一個高讀寫列表,測量現有重複、遺漏、深頁延遲和 count 成本。建立覆蓋索引,發布版本化游標,增加並發寫入測試與監控。Django REST framework 將 cursor pagination 定義為不透明游標;Hello Interview 和 TechInterview 材料都強調動態資料下 cursor/keyset 比 offset 穩定。

試點退出條件

並發插入和刪除測試中重複/遺漏符合宣告語意;深頁 p95 穩定;篡改或過濾器變化被拒絕;客戶端能安全回到第一頁;權限稽核確認 token 不洩露資料。

如何證明收益不是巧合

在相同資料規模和寫入率下比較 offset 與 cursor 的 p95/p99 延遲、掃描行數、重複率、遺漏率和資料庫 CPU,並分離快取命中率影響。

常見誤區與追問

把 Base64 當成安全游標

Base64 只是編碼,客戶端可以篡改 id 或租戶。使用簽名或伺服器 token,綁定過濾器、版本和過期時間。

只按時間戳排序

同一毫秒可能有多行,邊界會不確定。加入唯一 tie-breaker 並建立複合索引。

游標可以跳任意頁嗎?

標準 cursor 不適合任意跳頁。可提供受限 offset、預計算錨點或搜尋引擎頁碼,並說明一致性成本。

記錄更新後會重複嗎?

排序欄位可變時記錄可能移到另一頁。使用不可變建立序列,或採 snapshot/version 語意。

如何處理上一頁按鈕?

保存前一頁游標堆疊,或反向查詢後在伺服器反轉結果。不要讓客戶端猜游標內部方向。

精確總數一定要返回嗎?

不一定。無限滾動通常只需 has_more;精確 count 可非同步或獨立接口,避免每頁掃描全表。

公開來源

同類題目