Mongoose如何处理异步并行修改操作?
问题:Mongoose并行保存文档时的冲突处理
我正在使用Node.js和MongoDB(借助Mongoose包)进行开发。编写了一个用于更新文档的异步函数,当前需要并行执行10次该函数并传入不同参数。我的疑问是:若文档已被提前保存,Mongoose/MongoDB会在保存前自动重新获取文档吗?还是会抛出错误?
以下是我的代码:
const crawlSitemap = async (projectId) => { try { const project = await Project.findById(projectId); const browser = await puppeteer.launch({ headless: "new" }); const crawl = (sitemap) => async () => { const page = await browser.newPage(); page.setDefaultNavigationTimeout(120 * 1000); console.log(sitemap.url); if (!validator.isURL(sitemap.url) || sitemap.crawlStatus !== "queued") return; await page.goto(sitemap.url, { waitUntil: "networkidle0" }); await page.evaluate(() => { const scripts = document.querySelectorAll("script"); scripts.forEach((script) => script.remove()); const images = document.querySelectorAll("img"); images.forEach((img) => img.remove()); const styles = document.querySelectorAll( 'link[rel="stylesheet"], style' ); styles.forEach((style) => style.remove()); }); const textContent = await page.evaluate(() => document.body.textContent); await page.close(); await promisify(fs.writeFile)( path.join(__dirname, `tmp/${sitemap.url.replaceAll("/", "-")}.txt`), textContent, "utf-8" ); sitemap.crawlStatus = "ok"; await project.save(); }; await parallelLimit(project.sitemap.map(crawl), 10); await browser.close(); } catch (error) { console.log(error); } };
回答
Mongoose默认不会自动重新获取文档,你当前的写法大概率会遇到更新丢失或者版本冲突错误,原因如下:
- 函数开头就把
project文档从数据库拉到了内存中,所有并行的crawl任务都是在修改同一个内存对象的sitemap数组项。 - 当第一个并行任务修改完
sitemap.crawlStatus并调用project.save()后,数据库里的文档版本(默认通过__v字段标识)已经更新,但其他并行任务手里的还是旧的内存对象。 - 后续任务再调用
project.save()时,如果Schema开启了版本校验(默认开启),会直接抛出版本冲突错误;如果没开版本校验,新的保存会直接覆盖数据库里的最新数据,导致前面任务的修改丢失。
推荐解决方案:使用原子更新
不要先拉取整个文档到内存,而是直接用Mongoose的原子更新方法,单独修改目标sitemap项。这样每次更新都是直接操作数据库,完全避免并行冲突问题:
// 替换原代码中的await project.save() await Project.updateOne( { _id: projectId, 'sitemap._id': sitemap._id // 定位到具体的sitemap子文档 }, { 'sitemap.$.crawlStatus': 'ok' } // 更新该子文档的状态 );
其他可选方案(不推荐)
- 每次保存前重新获取最新的
project文档:但并行场景下还是可能出现冲突,且频繁查询会降低性能。 - 实现分布式锁:复杂度高,增加系统维护成本,没必要为这个场景引入。
内容的提问来源于stack exchange,提问作者Zullu Balti
相关产品推荐
相关产品推荐

