題幹與適用場景
題目考察後端工程師能否把分頁提升為穩定的資料讀取協定。訂單、評論和日誌會在請求之間插入、刪除或更新;單純 OFFSET 依賴變動位置,深頁還會掃描並丟棄大量資料。回答應涵蓋排序、游標編碼、過濾器綁定、一致性邊界、索引和前後分頁語意。
面試官考察點
強回答會先問是否需要跳頁、總數和即時性,再選擇 keyset/cursor 或 offset。游標必須綁定排序與過濾條件,排序鍵必須唯一、穩定且有索引;伺服器驗證簽名和過期時間,避免客戶端偽造。也要說明新增資料不會讓下一頁重複、刪除如何處理,以及 API 如何返回 next_cursor。
回答前需要釐清的問題
- 資料按什麼排序?排序欄位會更新嗎,是否有唯一 tie-breaker?
- 需要上一頁、跳到第 N 頁、總數,還是只支援向前無限滾動?
- 讀取期間要凍結快照,還是允許最終一致的即時列表?
- 過濾條件、租戶權限和排序是否必須綁在游標中?游標有效多久?
- 刪除、軟刪除、權限變化和跨分片查詢如何處理?
30 秒回答框架
「我會用穩定複合排序鍵,例如 (createdat, id),按降序做 keyset 查詢。游標是不透明簽名 token,包含最後一筆的排序值、過濾器雜湊、方向和版本;伺服器驗證後執行 WHERE (createdat,id) < (:time,:id),依賴同序索引。新增記錄留到下一次刷新,不會插入已讀窗口;刪除可能令結果變少,但不會製造重複。若必須絕對一致,再增加 snapshot boundary 並說明成本。」
分步深入解答
第一步:定義列表語意
先確定是歷史稽核列表、即時 feed 還是管理後台。歷史列表通常需要穩定邊界;即時 feed 可允許新記錄只在刷新時出現。不要同時承諾即時、任意跳頁、精確總數和低成本。
第二步:選擇穩定排序鍵
時間戳可能相同或被修改,必須加入唯一 id 作 tie-breaker。排序欄位不能依賴展示文字;會改變順序的欄位應改用不可變建立序列,或說明更新項可能移動。
第三步:設計不透明游標
游標至少含排序值、方向、過濾器雜湊、API 版本和過期時間。用簽名或伺服器儲存防篡改;Base64 只提供編碼,不提供安全性。過濾器改變時拒絕舊游標。
第四步:撰寫 keyset 查詢
降序 (createdat,id) 的下一頁條件是 createdat < t OR (created_at = t AND id < id0),並配合同序複合索引。使用參數化查詢和頁面上限,避免把游標直接拼進 SQL。
第五步:處理動態寫入與刪除
第一頁之後新增的記錄不應插入第二頁,它們在下一次刷新出現。已讀記錄被刪除會讓頁面少一筆,這是可接受語意;若不能漏項,使用 snapshot boundary 或版本化讀取。
第六步:綁定權限和過濾
游標中的過濾器雜湊、租戶和授權範圍必須與請求一致。權限收緊時重新計算,不能用舊游標繞過控管。跨分片時由協調器合併局部游標,並說明放大成本。
第七步:定義 API 回應與錯誤
回應返回 items、nextcursor、hasmore 和可選快照識別。游標過期、簽名失敗或過濾器變化要用穩定業務錯誤碼,客戶端清空舊游標回到第一頁。
第八步:用並發測試驗證
測試兩頁之間插入、刪除、同時間戳、過濾器變化、游標篡改和深頁查詢。驗證同一會話無重複,索引命中且延遲不隨頁碼線性增加;記錄重複率、跳過率和 p95 延遲。
查詢偽代碼
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 可非同步或獨立接口,避免每頁掃描全表。