IndexedDB跨标签页写冲突需用Web Locks API协调,必须将打开数据库、事务执行及落盘全过程封装在锁内,锁名须语义明确且全局一致,按业务实体控制粒度,读操作仅在读-改-写等强一致性场景下复用写锁名。

IndexedDB 本身不提供跨标签页的并发协调能力,单靠事务无法避免多标签同时写同一数据导致的静默覆盖。真正有效的解法是把“打开数据库 → 启动事务 → 执行写入 → 等待事务落地”整个流程用 Web Locks API 封装起来,并配合合理的锁粒度和操作习惯。
必须用 Web Locks 包裹完整操作链
IndexedDB 的 readwrite 事务只在单个连接内生效,每个标签页调用 indexedDB.open() 都会创建独立连接,彼此完全隔离。两个标签页同时对同一 key 调用 put(),后提交者会直接覆盖前者的修改,且不报错。
- 锁的作用不是操作数据库,而是协调“谁有资格执行整段逻辑”
- 必须将
db.transaction(...).objectStore(...).put(...)和await tx.done(或等效的 Promise 包装)全部放进navigator.locks.request()的回调里 - 仅调用
put()或监听oncomplete不够——锁会在回调退出时立即释放,而此时事务可能还没真正落盘
锁名要语义明确、全局一致
锁名是协调的关键标识,必须稳定可预测,不能带随机数、时间戳或 session ID,否则不同标签页拿不到同一把锁,形同虚设。
- 推荐命名方式:
'user-profile-write'、'cart-write'、'note-edit-123' - 避免使用
'lock-' + Date.now()或Math.random(),这类动态名会让锁失效 - 读-改-写场景(如计数器+1)也应复用同一写锁名,而不是另建读锁——混用不同锁名极易引发死锁
按业务实体控制锁粒度
一把全局锁(如 'idb-write-global')虽简单,但会让所有写操作串行,哪怕改头像和记日志完全无关。更合理的是按数据边界划分:
- 用户级操作:用
'user-write-${userId}' - 笔记编辑:用
'note-write-${noteId}' - 购物车更新:用
'cart-write'(通常单用户一个) - 跨实体操作(如转账需同时改 A/B 账户)无法用单个细粒度锁保护,此时应回退到更粗的锁,或改用服务端协调
读操作要不要加锁?看场景
多数只读请求不需要锁,用 readonly 事务即可,接受最终一致性。但以下情况必须加锁(且复用对应写锁名):
- 读-改-写流程:比如读配置版本 → 判断是否需要更新 → 再写新值,不加锁可能两次都读到旧版,导致重复更新
- 强顺序依赖:A 标签页写完某状态后,B 标签页才允许读取该状态,否则可能读到过期中间态
- 依赖写操作严格完成:例如日志上报后立刻查最新统计,不加锁可能命中未刷新的缓存

















