題幹與適用場景
商品資料庫是搜尋系統的唯一事實來源。請設計一個近即時 CDC 管道,把新增、更新與刪除可靠地同步到搜尋索引;要求支援初始全量、增量追平、重複投遞、消費中斷、Schema 變更與索引重建,並說明如何證明沒有漏資料或舊事件覆蓋新事件。
這道題適合後端、資料基礎設施、搜尋平台與系統設計面試。題目假設資料庫能提供提交順序或等價的日誌位置,搜尋索引是可重建的衍生系統;不把 Kafka、Debezium 或 Elasticsearch 當成必選答案,候選人需要先說清楚語義與失敗邊界。
面試官考察點
面試官會看候選人能否把「資料庫寫入成功」與「索引最終可見」拆成兩個可監控階段。強回答會先定義每條事件的主鍵、操作類型、交易或日誌位置與 Schema 版本,再選擇日誌型 CDC 而不是依賴輪詢時間戳;接著處理快照與增量重疊、至少一次重播、按鍵順序、刪除墓碑與索引切換。只畫資料庫、訊息佇列與搜尋框,卻沒有 checkpoint、重播和對帳規則,無法證明可靠性。
回答前需要澄清的問題
- 新鮮度目標是什麼? 是提交後 5 秒內可搜尋,還是允許分鐘級延遲?這決定緩衝、告警與降級預算。
- 必須保留什麼順序? 通常只要求同一商品按來源提交順序套用,不要求跨商品全域排序;若商品和庫存跨表有一致性約束,需要額外設計聚合事件。
- 刪除是硬刪除還是軟刪除? 硬刪除需要可靠墓碑或刪除事件;軟刪除則要把可見性條件編碼進索引文件。
- 全量期間允許寫入嗎? 若允許,就必須定義快照位置,並讓該位置之後的增量補償全量讀取期間的變化。
- Schema 如何演進? 新欄位能否被舊消費者忽略,欄位刪除是否需要雙寫、重建索引或版本化 mapping?
- 重建是否要求零停機? 若要求,使用新索引 alias 切換,並為舊消費者保留重播起點。
30 秒回答框架
「我會把主資料庫當唯一事實來源,優先從提交日誌捕獲已提交的 insert、update、delete,並讓事件帶主鍵、操作、來源端 LSN、交易 ID、Schema 版本與前後值。首次同步從一致性快照開始,同時記錄快照對應的日誌位置;快照之後的事件進入同一條可重播流,消費者按商品鍵冪等寫入索引。
事件處理採用至少一次交付,checkpoint 只在索引寫入成功後推進,重複事件用主鍵加版本或 LSN 條件寫擋住。消費者中斷後從 checkpoint 重播;積壓、最老事件年齡、來源端 slot 保留量與索引對資料庫抽樣對帳都做成指標。重建時把同一事件流寫入新索引,追平後再原子切 alias,並保留重播窗口。」
分步驟深入解答
第一步:定義事件契約與擷取邊界
日誌型 CDC 讀取資料庫提交日誌,能看到交易提交後的變更,並保留來源順序或日誌位置。以 PostgreSQL 為例,logical decoding 從 WAL 擷取變更,replication slot 表示可以按來源順序重播的變更流;slot 會保留所需 WAL,因此必須監控保留量,避免連接器停滯拖滿主庫磁碟。
事件至少包含 entityid、operation、sourceposition、transactionid、schemaversion、before 與 after。把 source_position 當作稽核與去重依據,而不是把消費者收到訊息的時間當作業務順序。一個交易影響多個商品時,要決定索引是否允許逐條可見,還是先在流中按交易邊界聚合。
第二步:用快照位置連接全量與增量
全量與增量最危險的窗口是:快照讀到舊值之後,流又收到同一主鍵的新值。啟動快照時記錄日誌位置 P0;快照產生的文件只代表開始時狀態,P0 之後的事件必須繼續保留並在快照結果後套用。
P0 = captureSourcePosition()
startStreaming(after=P0)
for row in consistentSnapshot():
indexUpsert(row, version=P0)
for event in stream:
if event.position > indexedVersion[event.key]:
applyIdempotently(event)
checkpoint(event.position) # only after index write succeeds真實連接器可能用快照窗口、主鍵分塊與緩衝區處理 READ 與 UPDATE 的碰撞;面試中要說明這是為了避免舊的快照列覆蓋已提交的新事件,而不是簡單地「快照完成後再開流」。
第三步:設計冪等、順序與失敗恢復
消費者按 entity_id 分區,讓同一商品的事件保持來源順序;不同商品可以並行。索引寫入使用條件版本、外部版本號或帶版本的文件,只有事件位置更新時才覆蓋。DELETE 寫入墓碑或刪除操作,並保留足夠版本資訊,防止遲到 UPDATE 把已刪除文件復活。
checkpoint 表示「該事件的副作用已成功持久化」,不能在拉到訊息或送出 HTTP 請求後提前提交。程序在索引寫成功、checkpoint 寫入前崩潰時會重複投遞,所以目標寫入必須冪等;如果 checkpoint 領先於索引,會造成漏資料,必須把兩者放在同一可驗證提交邊界,或使用可重播的索引任務與對帳修復。
第四步:處理重播、Schema 與重建
每個消費者使用獨立 slot 或等價進度,避免多個消費者爭搶同一個單次消費游標。重播前凍結或標記目標索引的版本策略,限制重播範圍,並讓舊事件只能寫入較舊版本。Schema 事件要有相容規則:新增可選欄位通常允許舊消費者忽略;刪除或改型別可能需要新事件版本、雙讀雙寫或重新索引。
重建不應直接清空線上索引。建立新索引並從快照位置開始重播,直到新索引已套用位置達到切換門檻;隨後原子切換 alias,並繼續消費同一條流。若切換失敗,保留舊 alias 與新索引進度,修復後繼續追平,不重新猜測起點。
第五步:用指標與對帳證明可靠性
核心指標包括 CDC 讀取延遲、分區積壓、最老事件年齡、slot WAL 保留量、每個消費者 checkpoint、索引寫入失敗率、重試與死信數量,以及資料庫與索引按主鍵抽樣比較的版本差。對刪除尤其要統計墓碑處理與殘留文件。
驗證應故意停止消費者、重複投遞、打亂不同分區的訊息、在快照期間更新同一商品、傳送遲到刪除、改變 Schema 並執行主庫故障切換。對帳工具要能從資料庫重查目前版本、從事件流重播到某個位置,並輸出最小不一致樣本。沒有可重播位置和樣本,單看「佇列為空」不能證明沒有漏資料。
高品質示範回答
「我先定義契約:資料庫是事實來源,索引是可重建副本;目標是提交後 5 秒內可見,同一商品按來源提交順序處理,跨商品不承諾全域排序。事件攜帶主鍵、insert/update/delete、LSN、交易 ID、Schema 版本與前後值。
我會用日誌型 CDC。啟動一致性快照時記下位置 P0,然後從 P0 之後的流繼續消費。快照 READ 與流中的 UPDATE 可能碰撞,所以要用快照窗口或等價的主鍵版本去重,不能讓舊 READ 覆蓋新 UPDATE。消費者按商品鍵分區,索引使用外部版本或條件寫;DELETE 要保留墓碑版本,避免遲到 UPDATE 復活。
checkpoint 只有在索引副作用成功後推進,崩潰會導致至少一次重播,因此寫入必須冪等。連接器停機時我監控 slot 保留的 WAL,恢復後從最後安全位置繼續。重建時寫新索引並從同一位置重播,追平後原子切 alias。
驗收不只看佇列長度。我會注入快照期間更新、重複與亂序、遲到刪除、消費者崩潰、Schema 變更與主庫切換,再按主鍵比較資料庫版本、索引版本與 checkpoint。重點指標是最老事件年齡、WAL 保留、索引延遲、死信與不一致樣本;出現缺口時能從保存的日誌位置重播和修復。」
常見錯誤
- 用更新時間輪詢代替可靠 CDC → 時鐘精度、回撥與長交易會漏掉變化 → 讀取提交日誌或明確可證明的游標。
- 快照和增量各自啟動卻沒有共同位置 → 快照舊值可能覆蓋新事件 → 記錄 P0,並處理 READ/UPDATE 碰撞。
- checkpoint 在送出索引請求後立即推進 → 崩潰窗口會造成漏資料 → 只在副作用可驗證成功後推進。
- 假設至少一次等於 exactly-once → 重複投遞仍會發生 → 用版本條件寫和冪等刪除。
- 只按訊息到達時間排序 → 網路重試會打亂來源順序 → 按主鍵分區並使用來源 LSN 或版本。
- 刪除直接從索引移除且不留版本 → 遲到更新會讓文件復活 → 保存墓碑或刪除版本。
- 多個消費者共享一個 replication slot → 變化可能被其中一個獨占,其他消費者靜默缺資料 → 每個獨立消費者使用獨立 slot 或明確的廣播層。
- 重建時清空線上索引 → 重播失敗會造成大面積不可搜尋 → 新索引追平後原子切換 alias。
- 只看佇列為空 → 可能已跳過事件或索引寫入失敗 → 做位置、版本與主庫抽樣對帳。
追問及應對
追問一:如果一個交易更新了商品和庫存,索引能逐條可見嗎?
可以,但要明確業務允許短暫中間態;如果搜尋結果必須同時反映兩者,就要讓事件攜帶交易邊界並在消費者聚合後一次更新文件,或把可搜尋視圖建成一個已提交投影。不要用跨索引寫入的「幾乎同時」冒充原子性。
追問二:連接器停機很久導致 slot 佔滿 WAL,怎麼止損?
先保護主庫:告警並限制繼續寫入或暫時降級消費者,確認 slot 是否仍有可用起點。若 slot 已失效,不能隨意建立新 slot 繼續跑,因為中間 LSN 可能遺失;應從備份或全量快照重建,並用對帳證明缺口已補齊。
追問三:事件流只有分區級順序,如何處理跨商品的搜尋排序?
搜尋排序讀取的是索引目前版本,不應假設全域事件順序。若排序欄位需要全域一致時間,使用來源提交時間加版本規則,或由獨立聚合器產生穩定排序鍵,並明確允許的暫時偏差。跨分區強制總序會犧牲吞吐,只有業務確實需要才採用。
追問四:Schema 刪除欄位時,舊索引怎麼辦?
先發布能同時讀新舊事件的消費者,再停止產生舊欄位,確認積壓和重播窗口已清空,最後重建或遷移索引 mapping。若欄位改變影響分析或權限語義,不能只刪 JSON 欄位;應保留版本化事件和回滾路徑。