1. 題目
多個同源分頁會同時嘗試刷新 IndexedDB 中的快取並向伺服器同步。請設計一個協調器,保證同一時間最多一個上下文執行同步,其他分頁可以等待、跳過或在鎖釋放後重新檢查狀態。方案還要涵蓋分頁關閉、網路失敗和瀏覽器不支援 Web Locks 的情況。
2. 約束與釐清
- 鎖名在同源上下文之間共享,頁面和 Worker 都可能參與競爭。
- 鎖只保護協調範圍內的非同步回呼;它不是資料庫交易,也不能取代伺服器並發控制。
- 同步任務可能耗時,需要可取消、可重試,並在完成後廣播結果或版本號。
- 需要區分等待鎖、立即探測鎖和放棄請求三種呼叫意圖。
3. 核心思路
呼叫 navigator.locks.request(name, options, callback) 請求命名鎖。鎖會在回呼 Promise 完成後釋放;因此回呼必須包含完整的讀取、同步和狀態寫入流程。預設請求會排隊,ifAvailable: true 可以在鎖忙碌時立即收到 null,signal 可取消仍在等待的請求。
鎖管理器會在持有上下文終止時釋放鎖,但持鎖頁面崩潰不應成為業務正確性的唯一依據:同步記錄應帶版本、租期或冪等鍵,伺服器仍需校驗。其他分頁可透過 BroadcastChannel 或重新讀取 IndexedDB 發現新版本。
4. 參考實作
const LOCK = "shared-cache-sync";
async function runSync(signal) {
return navigator.locks.request(LOCK, { signal }, async (lock) => {
if (!lock) return { status: "busy" };
const current = await readSyncVersion();
if (await isFresh(current)) return { status: "fresh" };
const result = await fetchAndWriteCache({ signal, baseVersion: current });
await publishVersion(result.version);
return { status: "updated", version: result.version };
});
}
async function trySyncWithoutWaiting() {
return navigator.locks.request(
LOCK,
{ ifAvailable: true },
(lock) => lock ? runOneSync() : { status: "busy" },
);
}5. 正確性與故障處理
鎖保證的是同源上下文對同一名稱的互斥排隊,不保證網路請求或資料庫寫入自動回滾。回呼應先重新讀取版本,再執行條件更新;如果同步期間網路失敗,拋出錯誤讓鎖釋放,並由下一次請求重試。不要在鎖回呼之外保存「我擁有鎖」的布林值。
等待請求被 AbortSignal 取消時,呼叫端要區分取消與業務失敗。對於長時間等待,可以顯示「其他分頁正在同步」或直接跳過;鎖釋放後再讀取版本,避免重複刷新。同步結果透過 BroadcastChannel 通知只是最佳化,權威狀態仍應來自 IndexedDB 或伺服器。
6. 追問與陷阱
- Web Locks 只在同源上下文間協調,不能鎖住另一個網站、伺服器程序或資料庫資料列。
ifAvailable不會排隊;拿不到鎖時回呼收到null,不能把它當成例外。steal: true會破壞既有排隊語意,應只用於明確可丟棄的工作,並搭配版本檢查。- 不支援 API 時可用 BroadcastChannel 加 IndexedDB 記錄做盡力協調,但不能宣稱有同等互斥保證;伺服器必須繼續防重複。
7. 延伸閱讀
可以比較 Web Locks、IndexedDB 交易、Service Worker 訊息和 BroadcastChannel:鎖解決跨上下文互斥,交易解決單庫原子性,訊息通道負責通知。跨裝置或跨使用者的互斥應移到伺服器租約、冪等 API 或資料庫約束。
8. 面試評分點
能說明鎖的作用域
應指出命名鎖只協調同源視窗與 Worker,回呼 Promise 完成才釋放,不能取代伺服器或資料庫並發控制。
能區分三種請求模式
應分別解釋預設排隊、ifAvailable 立即探測和 AbortSignal 放棄等待的行為。
能處理崩潰與重複
應使用版本、冪等鍵和條件更新,說明分頁崩潰釋放鎖不等於業務寫入自動回滾。
能給出誠實降級
應提供 BroadcastChannel/IndexedDB 的盡力方案,並明確互斥保證降低、伺服器仍需兜底。