如何解决IndexedDB事务内异步调用失效及多页面并发重复操作问题
解决方案
问题根因
你遇到的报错本质是IndexedDB的事务自动提交机制:读写事务在所有待处理请求完成后会立即自动提交,你在事务的onsuccess回调中执行异步fetch操作时,事务已无待执行的IDB请求,会直接提交关闭,后续执行put操作时操作的是已终止的事务,因此报错。
同时多页面共享IndexedDB存储,未做并发控制的情况下会出现重复发起fetch请求的问题。
具体实现方案
核心思路
拆分IDB事务+用IDB实现跨页分布式锁,保证同一时间只有一个页面执行fetch更新逻辑:
- 拆分事务:不要将异步网络请求放在IDB事务执行流程中,先读数据判断是否需要更新,网络请求完成后再单独开事务写入数据
- 加并发控制:利用IndexedDB读写事务的串行化特性,给对应记录加临时锁,避免多页面同时触发更新流程
实现代码
// 工具方法:封装indexedDB操作成promise形式 function promisifyRequest(request) { return new Promise((resolve, reject) => { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } // 加锁等待工具方法 function wait(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function start(message) { const clientName = message.client_name; let retryCount = 0; const maxRetry = 5; // 最多重试5次,避免无限等待 while (retryCount < maxRetry) { // 第一步:开只读事务读取当前数据 const readTx = db.transaction(["Client"], "readonly"); const readStore = readTx.objectStore("Client"); const data = await promisifyRequest(readStore.get(clientName)); await promisifyRequest(readTx.done); // 等待只读事务结束 const currentData = data || { client_name: clientName, sequence: 0, api_base_url: message.api_base_url || 'https://api.local', lock_until: 0, last_access: null }; // 检查是否需要更新 const needUpdate = !currentData.last_access || dateDiff(Date.parse(currentData.last_access), new Date(), "m") > 20; if (!needUpdate) { return currentData.sequence; } // 第二步:尝试加锁,开读写事务 const lockTx = db.transaction(["Client"], "readwrite"); const lockStore = lockTx.objectStore("Client"); const lockData = await promisifyRequest(lockStore.get(clientName)); // 校验锁状态:如果已经被别的页面加锁了,就重试 if (lockData && lockData.lock_until > Date.now()) { await promisifyRequest(lockTx.done); retryCount++; await wait(300); continue; } // 加锁:设置锁有效期30秒 currentData.lock_until = Date.now() + 30 * 1000; await promisifyRequest(lockStore.put(currentData)); await promisifyRequest(lockTx.done); try { // 第三步:执行fetch请求 const response = await fetch(currentData.api_base_url + '/v1/hello'); const responseData = await response.json(); currentData.id = responseData.id; currentData.last_access = responseData.dt; currentData.lock_until = 0; // 释放锁标记 // 第四步:开读写事务写入最终数据,写入前再校验一次避免被其他页面修改 const writeTx = db.transaction(["Client"], "readwrite"); const writeStore = writeTx.objectStore("Client"); const finalCheckData = await promisifyRequest(writeStore.get(clientName)); // 校验:只有锁还是我们持有的时候才写入 if (finalCheckData && finalCheckData.lock_until === currentData.lock_until + 30*1000) { await promisifyRequest(writeStore.put(currentData)); } await promisifyRequest(writeTx.done); return currentData.sequence; } catch (err) { // 异常时主动释放锁 const releaseTx = db.transaction(["Client"], "readwrite"); const releaseStore = releaseTx.objectStore("Client"); const releaseData = await promisifyRequest(releaseStore.get(clientName)); if (releaseData && releaseData.lock_until === currentData.lock_until) { releaseData.lock_until = 0; await promisifyRequest(releaseStore.put(releaseData)); } await promisifyRequest(releaseTx.done); throw err; } } throw new Error('获取锁超时,更新失败'); }
注意事项
- 锁的超时时间可以根据接口平均响应时间调整,避免正常请求还没完成锁就过期
- 重试次数和等待间隔也可以根据业务场景调整,平衡并发性能和等待成本
- 所有IDB事务都要等待执行完成,避免出现事务意外终止的问题
内容的提问来源于stack exchange,提问作者Andrew Harris
相关产品推荐
相关产品推荐

