当前位置:首页 > 文章列表 > 文章 > 前端 > Web Locks API 怎么避免多个标签页重复执行任务

Web Locks API 怎么避免多个标签页重复执行任务

来源:17golang原创 2026-10-05 03:25:12 0浏览 收藏

多个标签页同时启动缓存刷新、数据同步或后台轮询时,可以让它们请求同一个 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 互斥、完成标记与服务端幂等的静态关系图
图1:exclusive 锁负责浏览器内互斥,完成标记负责挡住排队后的重复,幂等键负责覆盖浏览器边界之外的重复提交。这是静态说明图,不是运行截图。

Web Locks 能覆盖哪些边界

Web Locks 适合协调共享同一存储桶的页面和 Worker。日常开发可以把它理解成“同源浏览上下文之间的命名锁”,但它不是跨设备、跨浏览器配置或跨用户的分布式锁。

  • API 需要安全上下文,正式环境应运行在 HTTPS 下。
  • 锁名由应用自定义,同名任务必须使用完全一致的名称。
  • 默认 exclusive 适合写入、同步和单领导者任务;shared 允许多个持有者同时进入,不适合防止重复执行。
  • 页面与 Worker 都可以使用,但不同设备、独立浏览器配置和隔离的隐私会话不会共享同一把锁。
  • 锁名只表达浏览器内的抽象资源,不会自动锁住数据库记录或服务端接口。

上线前先做能力检测:

function supportsWebLocks() {
  return "locks" in navigator;
}

如果不支持,不要用一个 localStorage 布尔值假装成原子锁,因为“读取未占用”和“写入占用”之间仍有竞争窗口。可靠的降级通常放到服务端幂等、唯一约束或任务队列中。

三种抢锁方式怎么选

选择方案时,先问两个问题:其他标签页要不要等待?当前持有者失败后,任务是否必须被接管?答案不同,锁的生命周期也不同。

默认排队:适合必须进入临界区再判断的任务

不传选项时,请求会等待。它适合“所有触发都要看到最新状态,但最终只允许一份结果”的任务。正确做法是在获得锁后重新读取持久化状态,而不是在请求锁之前判断。

ifAvailable:适合忙时直接放弃的任务

设置 ifAvailable: true 后,如果锁不能立即授予,回调仍会被调用,但参数是 null。这不会抛出“锁忙”错误,也不会进入等待队列。

长期持锁:适合轮询、WebSocket 或领导者任务

让回调在整个后台循环期间保持未完成,锁就会一直被持有。其他标签页等待;当前标签页关闭或回调结束后,下一个请求者可以接管。重点是让停止信号既能取消尚未获批的请求,也能让已经获批的循环主动返回。

Web Locks 默认排队、ifAvailable 与长期持锁策略静态对照图
图2:默认排队适合锁内检查,ifAvailable 适合忙时跳过,长期持锁适合只保留一个活动领导者。这是静态结构图,不是执行流程图。

方案一:任务忙时让其他标签页立即跳过

缓存预热、非关键刷新、可由下一次定时器再次触发的任务,通常不需要排队。下面的函数用返回值告诉调用方本次是否真正执行:

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 代码可能仍继续运行,只是失去了独占保证。它是异常恢复出口,不是普通选主或超时重试方案。

落地检查清单

  1. 明确任务需要“互不并发”“忙时跳过”“最终一次”还是“单领导者”。
  2. 为同一业务资源统一锁名,并保持默认 exclusive。
  3. 回调里等待完整异步任务,不让锁提前释放。
  4. 默认排队方案必须在锁内重新读取完成标记。
  5. 服务端写操作使用稳定幂等键,并建立唯一约束或去重记录。
  6. 长期任务同时处理等待阶段的 AbortSignal 和获批后的主动退出。
  7. 对不支持 Web Locks 的环境准备服务端降级,不用脆弱的 localStorage 标记冒充锁。
  8. 记录任务开始、跳过、完成、失败和接管事件,但不要用 query() 参与正确性判断。

最实用的记法是:Web Lock 管同源浏览器上下文之间的互斥,完成标记管排队后的业务去重,幂等键管浏览器边界之外的重复副作用。把这三层按任务风险组合起来,多个标签页才能既不同时抢活,也不会稍后又把同一件事做一遍。

相关问题

Web Locks 能跨浏览器或跨设备吗?

不能把它当成跨设备分布式锁。不同设备、浏览器配置或隔离会话应依赖服务端幂等、数据库唯一约束或任务队列协调。

ifAvailable 获取失败会抛异常吗?

不会因为“锁正忙”而抛异常。回调会收到 null,应用应据此跳过并记录本次未执行。

标签页关闭后锁会永远占着吗?

锁与持有它的执行上下文生命周期相关;正常设计仍应让回调可结束,并让长期任务响应应用卸载或退出信号,不要依赖页面强制关闭作为唯一释放方式。

BroadcastChannel 能替代 Web Locks 吗?

BroadcastChannel 适合广播“谁在工作”或同步状态,但消息本身不提供原子互斥。它可以配合 Web Locks 做通知,不能单独保证只有一个标签页进入临界区。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
特效变音魔术师免费吗?授权、广告提示与版本信息怎么核对特效变音魔术师免费吗?授权、广告提示与版本信息怎么核对
上一篇
特效变音魔术师免费吗?授权、广告提示与版本信息怎么核对
横风动漫乐园和横风动漫是什么关系?应用商店名称与产品身份核对
下一篇
横风动漫乐园和横风动漫是什么关系?应用商店名称与产品身份核对
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    331次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    390次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    384次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    352次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    176次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码