高负载下MongoDB文档计数不准致写入超限问题排查
问题描述
我是StackOverflow新手,若问题表述有误请见谅。我有一个Node.js Web服务器应用,向本地运行的MongoDB副本集写入数据,希望当集合中文档数达到上限(10个)时停止写入。
我最初的实现是先调用countDocuments()获取文档数,若小于10则写入,否则拒绝。该函数在低负载下正常,但高负载时会突破文档数上限。
之后我修改了函数,在写入后循环查询直到确认文档已写入再resolve,此版本可正常工作。我想了解为何高负载下MongoDB返回的文档计数不准确?
初始函数代码
async add_Server_Info_To_Db() { return new Promise(async (resolve, reject) => { try { if (await client.db("db").collection("col").countDocuments() === 10) { reject("max reached"); } else { const genid = await random_Things_Gen.id_Gen(); await client.db("db").collection("col").insertOne({ _id: genid }); resolve(); }; } catch (error) { reject(error); }; }); };
修改后函数代码
async add_Server_Info_To_Db() { return new Promise(async (resolve, reject) => { try { if (await client.db("db").collection("col").countDocuments() === 10) { reject("max reached"); } else { const genid = await random_Things_Gen.id_Gen(); await client.db("db").collection("col").insertOne({ _id: genid }); while (true) { await client.db("db").collection("col").findOne({ _id: genid }).then(() => { resolve(); }); }; }; } catch (error) { reject(error); }; }); };
问题解答
核心原因:竞态条件+非原子操作
你的初始代码逻辑是先查询计数,再执行写入,这两步操作不是原子性的——高负载下,多个请求会同时执行countDocuments(),此时集合文档数还没到10,所有请求都通过了检查,紧接着同时执行insertOne,最终就会插入超过10个文档,突破上限。
为什么修改后的代码能工作?
你修改后在写入后循环查询,直到确认自己插入的文档存在,本质是等待当前写入操作完全落盘并被主节点确认,后续的请求再执行countDocuments()时,就能读到这个新插入的文档,从而避免多个请求同时通过“计数小于10”的检查。不过这种方式属于“事后补救”,效率不高,极端情况下仍可能存在竞态。
更优的解决方案
要彻底解决这个问题,应该用原子操作保证“检查+写入”的一致性:
- 用
findOneAndUpdate维护计数文档:单独建一个计数文档,每次插入前先原子性地递增计数,同时判断是否超过上限。示例:
async add_Server_Info_To_Db() { const col = client.db("db").collection("col"); const counterCol = client.db("db").collection("counters"); // 原子性更新计数并检查上限 const result = await counterCol.findOneAndUpdate( { _id: "col_count" }, { $inc: { count: 1 } }, { upsert: true, returnDocument: "after" } ); if (result.value.count > 10) { // 超过上限,回滚计数 await counterCol.updateOne({ _id: "col_count" }, { $inc: { count: -1 } }); reject("max reached"); return; } // 计数合法,执行插入 const genid = await random_Things_Gen.id_Gen(); await col.insertOne({ _id: genid }); resolve(); }
- 使用MongoDB事务:把“查询计数+插入文档”放在一个事务里,保证操作的原子性(适合副本集环境)。
内容的提问来源于stack exchange,提问作者Sanithu Jayakody
相关产品推荐
相关产品推荐

