題幹與適用場景
Node.js 24 的 node:sqlite 提供同步 API。DatabaseSync 代表單個 SQLite 連線,createSession() 可追蹤會話開始後的變化,session.changeset() 回傳二進位 changeset,目標資料庫可用 applyChangeset() 套用並在衝突處理器中選擇中止、忽略或取代。請設計離線客戶端與伺服器之間的增量同步協定。
題目重點是資料庫變更如何安全傳播,包括主鍵、舊值校驗、衝突策略、冪等、交易邊界和權限。changeset 不是可直接執行的 SQL,也不等於跨資料庫自動合併。
面試官考察點
強回答會區分來源庫產生 changeset、傳輸層可靠投遞、目標庫原子套用和業務級衝突決策。面試官會追問 Session 生命週期、changeset 與 patchset 的取捨、SQLITECHANGESETDATA 等衝突類型、重複套用、schema 演進,以及 DatabaseSync 同步執行對事件迴圈的影響。
只說「序列化資料庫再覆蓋」會丟失並發更新;只回傳 SQLITECHANGESETREPLACE 會靜默覆蓋目標資料,無法證明業務正確。
回答前需要釐清的問題
同步拓撲與權威副本
確認是單向上行、雙向同步還是多端匯聚,誰是每個實體的權威副本。若兩端都能編輯同一列,需要版本、裝置 ID 或業務事件來裁決,不能把 SQLite 的預設衝突回調當成業務規則。
版本與 schema 生命週期
確認 source 和 target 是否保證相同 schema、主鍵和欄位型別。changeset 依賴表結構;遷移必須先完成並標記協定版本,否則套用舊 changeset 可能失敗或產生錯誤映射。
延遲與安全邊界
確認 changeset 大小、重試窗口、是否允許離線數天以及資料是否含敏感欄位。二進位 payload 需要完整性校驗、身份認證、重放保護和加密;不能把它當作可信 SQL。
30 秒回答框架
「我為每個同步批次分配 ID 和基於來源庫版本的游標。來源庫建立 Session,完成本地交易後匯出 changeset;伺服器先校驗 schema、簽章、批次順序和冪等狀態,再在目標資料庫交易中呼叫 applyChangeset。衝突回調預設中止並記錄列、欄和原因,只有業務明確允許時才忽略或取代。成功後持久化批次狀態,失敗使用指數退避重試,重複批次回傳已處理結果。同步 API 放在 worker 或佇列邊界之外,避免長時間同步 SQLite 阻塞 Node.js 事件迴圈。」
分步驟深入解答
第一步:建立可追蹤的批次
建立 Session 後只把一個明確的本地交易範圍納入批次。記錄 batchId、來源裝置、起始游標、schema 版本、changeset 雜湊和建立時間。呼叫 session.changeset() 取得 Uint8Array 後關閉或重用 Session,避免長期 Session 包含無法重放的過大歷史。
第二步:選擇 changeset 或 patchset
changeset 包含用於判斷衝突的舊值資訊,適合需要校驗目標列是否仍符合預期的同步;patchset 更緊湊,但提供的舊值資訊較少。先按頻寬和衝突稽核要求選擇,不能只因 payload 較小就犧牲可診斷性。
第三步:驗證與冪等投遞
傳輸層使用 TLS、裝置身份和 payload 簽章;伺服器校驗大小、雜湊、schema 版本和批次來源。以 batchId 和來源游標建立唯一記錄,已成功套用的批次重複到達時回傳已處理結果。後續批次必須按游標順序套用,遺失批次進入等待佇列而不是跳過。
第四步:在交易中套用並處理衝突
目標端開啟交易並呼叫 applyChangeset。衝突處理器應把表、主鍵、衝突類型和目標值寫入稽核緩衝區;預設回傳 SQLITECHANGESETABORT,讓整個批次回滾。只有經業務規則確認的欄位才允許 OMIT 或 REPLACE,並把決策寫入可重播日誌。
第五步:設計業務合併
欄位級可合併資料可以按時間戳、單調版本或集合聯集處理;金額、庫存和權限等不可盲目合併資料應生成人工佇列或補償事件。衝突解決後不要直接修改原 changeset,生成帶父批次和決策原因的新批次,保持稽核鏈完整。
第六步:處理型別與整數精度
Node.js 與 SQLite 的型別集合不同。超出 JavaScript 安全整數範圍的 SQLite INTEGER,在未啟用 readBigInts 時讀取可能拋出 ERROUTOF_RANGE;協定應統一使用 BigInt、字串或明確範圍。BLOB 使用 Uint8Array,不能把任意物件直接寫入 SQLite。
第七步:控制同步執行成本
DatabaseSync 的 API 同步執行,長交易、巨大 changeset 或頻繁 applyChangeset 會阻塞事件迴圈。把同步工作放到 worker、獨立程序或受控佇列,限制 payload 大小和每批列數,監控套用耗時、衝突率、回滾率和待處理批次年齡。
高品質示範回答
我會把同步批次視為不可變事件。裝置為每批生成 ID、游標、schema 版本和雜湊;Session 只涵蓋已提交的本地交易,匯出 changeset 後透過認證且具重放保護的通道傳送。伺服器驗證結構與順序,以批次 ID 做冪等,然後在目標 SQLite 交易中呼叫 applyChangeset。
衝突回調預設中止,讓批次原子回滾並記錄主鍵、衝突類型和目標值。庫存、金額和權限不使用通用取代,交給業務合併或補償佇列;可安全覆蓋的欄位才允許忽略或取代。成功批次寫入狀態表後才確認游標推進。因為 DatabaseSync 會同步阻塞,我會把大批次放入 worker,並測試重複投遞、遺失批次、schema 遷移、整數溢位和程序崩潰恢復。
常見錯誤
- 錯誤表現: 每次同步都覆蓋目標資料庫。→ 失敗原因: 會刪除目標端的獨立更新。→ 修正方法: 傳遞 changeset,依舊值檢查衝突並記錄決策。
- 錯誤表現: 所有衝突都回傳
REPLACE。→ 失敗原因: 業務資料被靜默覆蓋。→ 修正方法: 預設中止,按欄位和業務不變量授權取代。 - 錯誤表現: 重試時產生新批次而沒有冪等鍵。→ 失敗原因: 重複套用或游標跳躍會造成重複副作用。→ 修正方法: 用 batch ID、來源游標和套用狀態去重。
- 錯誤表現: 在主事件迴圈中套用超大 changeset。→ 失敗原因:
DatabaseSync同步執行,HTTP 和定時工作會被阻塞。→ 修正方法: worker 或佇列隔離,並設定批次上限。
追問及應對
追問一:什麼時候用 patchset?
當頻寬受限且目標端已有足夠上下文、衝突稽核要求較低時可以用 patchset。需要比較舊值、解釋衝突或做可稽核合併時選擇 changeset,並接受較大的 payload。
追問二:目標端 schema 少了一欄怎麼辦?
先拒絕批次並回傳相容錯誤,不能讓應用層猜測欄位映射。執行目標端遷移,確認 schema 版本與主鍵一致後再重播;若必須相容多個版本,伺服器按協定版本轉換並保留原始 payload。
追問三:如何避免衝突回調中的副作用?
回調只收集結構化衝突資訊並回傳常量決策,不呼叫外部服務、不修改其他表。交易提交後再非同步寫稽核或通知,讓回滾不會留下與資料庫狀態不一致的外部副作用。
追問四:為什麼不直接傳 SQL?
SQL 缺少來源列舊值、表結構和批次邊界,重試時難以判斷是否已經套用,也容易把未授權語句傳到目標端。changeset 由 SQLite 產生並可在目標端逐項處理衝突,更適合受控同步協定。
追問五:如何驗證整數和 BLOB 的三語協定一致?
建立跨語言固定樣例:最大安全整數、超範圍整數、負值、NULL、UTF-8 文字和二進位 payload。對每個樣例記錄 SQLite 型別、Node.js 讀寫選項和序列化表示,確保重播前後位元組和數值相同。