如何解决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
相关产品推荐
相关产品推荐

