Web Locks API 怎么避免多个标签页重复执行任务
来源:17golang原创
时间:2026-10-05 03:25:12 348浏览 收藏
多个标签页同时启动缓存刷新、数据同步或后台轮询时,可以让它们请求同一个 Web Lock。只要使用默认的 exclusive 模式,同一时刻就只有一个同源浏览上下文能持有这把锁。
但要注意一个容易踩坑的区别:默认排队只能避免同时执行,不能自动保证任务永远只执行一次。等待中的标签页会在前一个回调结束后依次拿到锁。真正避免重复,要根据任务类型选择 ifAvailable 直接跳过,或在锁内重新检查完成标记;涉及服务端副作用时,还要加幂等键。
官方参考:MDN Web Locks API、LockManager.request()、Web Locks 规范。
| 任务目标 | 建议方式 | 还要补什么 |
|---|---|---|
| 忙时直接跳过,不排队 | { ifAvailable: true } | 之后要有新的触发机会 |
| 任务最终必须完成一次 | 默认排队 + 锁内检查完成标记 | 持久化状态与失败重试 |
| 多个标签页只保留一个轮询者 | 长期持有 exclusive 锁 | 退出信号与接管逻辑 |
| 会修改服务端数据 | Web Lock + 幂等键 | 服务端唯一约束或去重记录 |
先分清:互斥不等于只执行一次
navigator.locks.request(name, callback) 会请求一把按名称区分的锁。默认模式是 exclusive:同名锁被占用时,后续请求进入队列;获得锁后执行回调,回调返回的 Promise 完成或失败时,锁自动释放。
因此下面这段代码能保证三个标签页不会同时执行 refreshCache(),却不能保证它只执行一次。A 标签页完成后,B、C 仍可能依次进入回调:
await navigator.locks.request("cache-refresh:v1", async () => {
await refreshCache();
});
如果业务要求“今天的报表只生成一次”,锁只是把并发请求排成一列;你还需要在拿到锁之后检查“今天是否已经完成”。如果任务会向服务端扣款、创建订单或提交消息,浏览器锁之外还要让服务端识别同一个幂等键。

Web Locks 能覆盖哪些边界
Web Locks 适合协调共享同一存储桶的页面和 Worker。日常开发可以把它理解成“同源浏览上下文之间的命名锁”,但它不是跨设备、跨浏览器配置或跨用户的分布式锁。
- API 需要安全上下文,正式环境应运行在 HTTPS 下。
- 锁名由应用自定义,同名任务必须使用完全一致的名称。
- 默认
exclusive适合写入、同步和单领导者任务;shared允许多个持有者同时进入,不适合防止重复执行。 - 页面与 Worker 都可以使用,但不同设备、独立浏览器配置和隔离的隐私会话不会共享同一把锁。
- 锁名只表达浏览器内的抽象资源,不会自动锁住数据库记录或服务端接口。
上线前先做能力检测:
function supportsWebLocks() {
return "locks" in navigator;
}
如果不支持,不要用一个 localStorage 布尔值假装成原子锁,因为“读取未占用”和“写入占用”之间仍有竞争窗口。可靠的降级通常放到服务端幂等、唯一约束或任务队列中。
三种抢锁方式怎么选
选择方案时,先问两个问题:其他标签页要不要等待?当前持有者失败后,任务是否必须被接管?答案不同,锁的生命周期也不同。
默认排队:适合必须进入临界区再判断的任务
不传选项时,请求会等待。它适合“所有触发都要看到最新状态,但最终只允许一份结果”的任务。正确做法是在获得锁后重新读取持久化状态,而不是在请求锁之前判断。
ifAvailable:适合忙时直接放弃的任务
设置 ifAvailable: true 后,如果锁不能立即授予,回调仍会被调用,但参数是 null。这不会抛出“锁忙”错误,也不会进入等待队列。
长期持锁:适合轮询、WebSocket 或领导者任务
让回调在整个后台循环期间保持未完成,锁就会一直被持有。其他标签页等待;当前标签页关闭或回调结束后,下一个请求者可以接管。重点是让停止信号既能取消尚未获批的请求,也能让已经获批的循环主动返回。

方案一:任务忙时让其他标签页立即跳过
缓存预热、非关键刷新、可由下一次定时器再次触发的任务,通常不需要排队。下面的函数用返回值告诉调用方本次是否真正执行:
async function refreshIfFree() {
if (!("locks" in navigator)) {
return { ran: false, reason: "unsupported" };
}
let ran = false;
await navigator.locks.request(
"cache-refresh:v1",
{ ifAvailable: true },
async (lock) => {
if (!lock) return;
ran = true;
await refreshCache();
},
);
return { ran, reason: ran ? "completed" : "busy" };
}
核对点有两个:一是一定要判断 lock 是否为 null;二是必须 await refreshCache()。如果回调里只启动一个未等待的 Promise,回调会提前完成,锁也会提前释放。
这种方案的代价是:赢家执行失败时,已经跳过的标签页不会自动回来补做。因此它适合“稍后再触发也没关系”的工作,不适合必须交付一次结果的结算、迁移或提交任务。
方案二:锁内检查完成标记,挡住顺序重复
对于“最终必须完成一次”的任务,可以允许请求排队,但每个标签页拿到锁后都重新检查完成标记。完成状态建议放在 IndexedDB 等同源持久化存储中。下面的 jobState 表示项目自己的 IndexedDB 封装:
async function runDailyReport(dateKey) {
const jobKey = `daily-report:${dateKey}`;
await navigator.locks.request(jobKey, async () => {
const state = await jobState.get(jobKey);
if (state?.status === "done") {
return;
}
await createReport({
dateKey,
idempotencyKey: jobKey,
});
await jobState.put({
key: jobKey,
status: "done",
finishedAt: Date.now(),
});
});
}
为什么完成标记必须在锁内检查?因为两个标签页若先在锁外同时看到“未完成”,再依次拿锁,它们仍会各执行一次。把读取、任务和写入都放入同一个回调,后来的标签页才能看到前一个留下的最新状态。
仍有一个崩溃窗口:服务端已经创建报表,但页面在写入本地 done 前崩溃。所以上例把稳定的 jobKey 同时作为服务端幂等键;服务端应对相同键返回同一结果,而不是再次创建。
方案三:只让一个标签页承担持续轮询
实时通知、后台同步和定时拉取常常只需要一个活动标签页。可以让每个标签页都请求同一把长期锁,获批者运行循环,其余标签页排队等待:
async function startLeaderPoller(controller) {
try {
await navigator.locks.request(
"orders-poller:v1",
{ signal: controller.signal },
async () => {
while (!controller.signal.aborted) {
await pollOrdersOnce();
await delay(15_000, controller.signal);
}
},
);
} catch (error) {
if (error.name !== "AbortError") throw error;
}
}
const controller = new AbortController();
startLeaderPoller(controller);
// 应用卸载或用户退出时调用:
// controller.abort();
signal 能取消尚未获得锁的请求;请求已经获批后,锁管理器不会替你中止回调,因此循环本身也必须观察同一个信号并返回。delay 也应支持 AbortSignal,避免停止时还要白等一个完整周期。
容易导致重复或卡住的风险点
锁名没有按业务资源统一
"sync"、"data-sync" 和 "sync:v1" 是三把不同的锁。把锁名集中在一个模块定义,并在需要时包含租户、用户或资源标识;不要把密钥、Token 等敏感信息放进锁名。
在回调里启动任务后立即返回
锁的持有时间由回调返回值决定。所有受保护的异步工作都必须被 await,否则任务还在运行,锁已经释放。
把 query() 当成加锁前判断
navigator.locks.query() 适合诊断当前持有和等待状态,但查询结果只是一个快照。查询完成到真正请求锁之间,状态可能已经改变,不能用它替代 request() 的原子协调。
用 shared 模式保护写任务
shared 会允许多个同名共享锁同时获批,只适合多个读取者并行、写入者使用 exclusive 的读写锁模型。避免重复执行的任务通常应保持默认 exclusive。
随意使用 steal
steal 会抢占当前持有者,但原持有者的 JavaScript 代码可能仍继续运行,只是失去了独占保证。它是异常恢复出口,不是普通选主或超时重试方案。
落地检查清单
- 明确任务需要“互不并发”“忙时跳过”“最终一次”还是“单领导者”。
- 为同一业务资源统一锁名,并保持默认
exclusive。 - 回调里等待完整异步任务,不让锁提前释放。
- 默认排队方案必须在锁内重新读取完成标记。
- 服务端写操作使用稳定幂等键,并建立唯一约束或去重记录。
- 长期任务同时处理等待阶段的 AbortSignal 和获批后的主动退出。
- 对不支持 Web Locks 的环境准备服务端降级,不用脆弱的 localStorage 标记冒充锁。
- 记录任务开始、跳过、完成、失败和接管事件,但不要用
query()参与正确性判断。
最实用的记法是:Web Lock 管同源浏览器上下文之间的互斥,完成标记管排队后的业务去重,幂等键管浏览器边界之外的重复副作用。把这三层按任务风险组合起来,多个标签页才能既不同时抢活,也不会稍后又把同一件事做一遍。
相关问题
Web Locks 能跨浏览器或跨设备吗?
不能把它当成跨设备分布式锁。不同设备、浏览器配置或隔离会话应依赖服务端幂等、数据库唯一约束或任务队列协调。
ifAvailable 获取失败会抛异常吗?
不会因为“锁正忙”而抛异常。回调会收到 null,应用应据此跳过并记录本次未执行。
标签页关闭后锁会永远占着吗?
锁与持有它的执行上下文生命周期相关;正常设计仍应让回调可结束,并让长期任务响应应用卸载或退出信号,不要依赖页面强制关闭作为唯一释放方式。
BroadcastChannel 能替代 Web Locks 吗?
BroadcastChannel 适合广播“谁在工作”或同步状态,但消息本身不提供原子互斥。它可以配合 Web Locks 做通知,不能单独保证只有一个标签页进入临界区。
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
270 收藏
-
文章 · 前端 | 5小时前 | 前端 · javascript · 异步编程 · JavaScript Promise.all Array.fromAsync 异步可迭代对象 AsyncIterable for await of236 收藏
-
文章 · 前端 | 8小时前 | html · 前端 · javascript · slot Web Components 服务端渲染 Declarative Shadow DOM shadowrootmode Shadow Root227 收藏
-
文章 · 前端 | 10小时前 | 前端 · 性能监控 · javascript · 前端性能 PerformanceObserver INP Long Animation Frames API LoAF 卡顿脚本463 收藏
-
444 收藏
-
文章 · 前端 | 15小时前 | websocket · javascript · 异步编程 · JavaScript websocket AbortSignal 异步迭代器 Promise.withResolvers EventTarget431 收藏
-
文章 · 前端 | 17小时前 | javascript · JavaScript Fetch AbortController 取消请求 AbortSignal.any AbortSignal.timeout174 收藏
-
255 收藏
-
181 收藏
-
440 收藏
-
246 收藏
-
112 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习