save() 循环慢因每次都是独立请求,含校验、中间件、网络往返;insertmany() 可批量插入但需设 ordered: false、分片、手动赋值字段并注意绕过中间件与校验。

为什么 save() 在循环里写入慢得明显
因为每次 save() 都是一次独立的 MongoDB 请求,带完整校验、中间件(如 pre('save'))、类型转换和网络往返。100 条数据 = 100 次 round-trip,哪怕在本地 localhost,延迟也常超 50ms/次;上云后更可能卡在连接池或 TCP 建连上。
常见错误现象:save() 循环跑着跑着 CPU 不高但耗时暴涨,日志里看到大量 MongoTimeoutError 或连接拒绝;用 mongostat 观察发现 netIn/netOut 波动剧烈但 ops/sec 上不去。
- 每条文档都触发
validate()和所有pre('save')钩子 —— 即使你没写,Mongoose 默认也有 - 默认不启用
ordered: false,一条失败整批中断 - 没控制
insertMany()的batchSize,大数据量时 OOM 或超时
insertMany() 怎么设参数才不翻车
不是把数组一塞就完事。insertMany() 的行为和你传的选项强相关,尤其在错误处理和性能边界上。
- 加
{ ordered: false }:让失败项跳过,其余继续插(适合脏数据容忍场景);不加则遇到第一条错就停,返回的result里insertedCount可能远小于预期 - 显式设
{ lean: true }:跳过 Mongoose 文档包装,减少内存占用(插入后不需要调.toObject()或访问虚拟字段时必开) - 大数据量(>1000 条)务必分片,比如每 500 条一批调
insertMany();MongoDB 单次请求有 16MB 限制,且 Node.js V8 堆内存容易爆 - 避免在
insertMany()前对每条文档手动赋值_id—— 如果用的是ObjectId,不赋也能插,但赋了要确保不重复,否则duplicate key错误会触发整个 batch 失败(除非ordered: false)
校验和中间件还能不能用
不能直接用 —— insertMany() 绕过所有 pre('save')、post('save') 和 schema-level validate()。这是性能提升的代价,也是最容易被忽略的坑。
使用场景:如果你的 pre('save') 只是补时间戳(this.updatedAt = new Date()),那必须自己在插入前遍历赋值;如果校验逻辑复杂(比如检查关联 ID 是否存在),就得拆成两步:先查再插,或改用数据库级约束(如 MongoDB 5.0+ 的 JSON Schema validation)。
- 想保留部分校验?用
Model.validate()手动批量校验数组,但它不跑中间件 - 需要自动填充默认值?别依赖
default函数 ——insertMany()不执行,得自己map()初始化 - 事务内不能用
insertMany()+session吗?可以,但得传{ session }选项,且注意事务有 60 秒超时限制
并发写入时连接池和写关注怎么配
光换 insertMany() 不够,Node.js 进程和 MongoDB 连接池不匹配,照样堵在底层。
- Mongoose 连接初始化时,
maxPoolSize默认是 100,但如果你开 50 个并发insertMany()请求,每个请求又占多个连接(尤其带事务时),很容易打满;建议按压测结果调,一般maxPoolSize: 20–50更稳 - 写关注(
writeConcern)别盲目设{ w: 'majority' }—— 尤其在副本集跨机房部署时,它会让每次写入等多数节点落盘,延迟翻倍;开发/日志类数据用{ w: 1 }足够 - 如果用的是
serverSelectionTimeoutMS或socketTimeoutMS默认值(30s),大批量插入超时会被静默重试,导致数据重复 —— 显式设短一点(如socketTimeoutMS: 10000),并在 catch 里判断错误类型是否为WriteConcernError
真正卡住的地方,往往不在代码逻辑,而在连接池耗尽后请求排队、或写关注等副本同步——这些不会报错,只会让你觉得“怎么越优化越慢”。










