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

如何解决Promise内执行多个依赖await时出现的Mongo重复键错误

解决方案

你遇到的是典型的读写竞态问题,分开执行getRedis和setRedis不是原子操作,并发请求下就会出现判断穿透的情况,以下是按优先级排序的可行方案:

方案1:使用Redis原子命令SETNX实现无锁判断(最优,改动最小)

不要分开调用get和set,直接用Redis的SETNX(SET if Not eXists)命令,这个命令本身是原子性的,Redis单线程执行特性保证同一时间只有一个请求能设置成功,从根源避免竞态。
同时可以修正你原有代码的反模式:new Promise包裹async函数是完全冗余的,async函数本身就会返回Promise,无需额外嵌套。

// 可直接使用async-redis内置的setnx方法
const handler = async (payload) => {
  try {
    // 原子操作:payload不存在时才设置,返回1表示设置成功,0表示已存在
    // 建议key按业务前缀+唯一标识拼接,避免Redis键冲突
    const setResult = await redisClient.setnx(`unique:payload:${payload.唯一标识字段}`, '1');
    if (setResult === 1) {
      // 只有setnx成功的请求才执行Mongo写入
      await write2Mongo(payload);
      // 可选:给Redis key设置过期时间,避免永久占用内存,可按业务场景调整过期时长
      await redisClient.expire(`unique:payload:${payload.唯一标识字段}`, 86400); // 示例为1天
    }
    return;
  } catch (err) {
    // 异常兜底:如果setnx成功但写Mongo失败,要删除Redis对应key,避免后续同payload请求被误拦截
    if (err.message.includes('Mongo')) {
      await redisClient.del(`unique:payload:${payload.唯一标识字段}`);
    }
    throw err;
  }
};

该方案利用Redis原生原子性解决竞态,性能最高,同时适配单实例/多实例部署场景。

方案2:捕获MongoDB重复键错误做兼容(兜底方案)

如果不想改动Redis侧逻辑,可以直接在写Mongo的时候捕获唯一键冲突错误做兼容,保证程序不报错:

// 保留原有逻辑,仅新增Mongo错误捕获逻辑
const handler = async (payload) => {
  try {
    const exists = await getRedis(payload)
    if(exists === null) {
      await setRedis(payload)
      try {
        await write2Mongo(payload)
      } catch (mongoErr) {
        // 11000是MongoDB重复键的固定错误码,非重复键错误再抛出
        if (mongoErr.code !== 11000) throw mongoErr;
      }
    }
  } catch (err) {
    throw err
  }
};

该方案属于兜底兼容逻辑,实现成本最低,适合对写入成功率要求不极致的场景。

方案3:分布式锁(适合处理逻辑更复杂的场景)

如果你的处理流程不止写Redis和Mongo,还有多步依赖操作需要原子执行,可以用Redis分布式锁锁住同一个payload的处理流程,只有拿到锁的请求才能执行后续逻辑,注意锁的粒度要到单个payload级别,不要用全局锁影响整体吞吐。

内容的提问来源于stack exchange,提问作者CodeCook

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.30 20:15:05