You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.19 05:24:03