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 的尽力而为方案,并明确互斥保证降低、服务端仍需兜底。