WiredTiger引擎中不存在文档移动,其采用写时复制与页级压缩,更新生成新版本并更新索引,旧数据由后台回收;填充因子无效,空间重用基于数据页;snapshot()仅适用于MMAPv1。

文档移动只发生在MMAPv1引擎,WiredTiger中根本不会发生
WiredTiger是MongoDB 3.2+默认的存储引擎,它完全不使用“文档移动”这一机制。所谓“更新导致文档位置变化”,是MMAPv1时代的遗留认知,直接套用到WiredTiger上会产生严重误判。
WiredTiger采用写时复制(Copy-on-Write)和页级压缩,所有更新都在内存中生成新版本,再批量刷盘为新的B-tree节点。旧数据块被标记为可回收,后续由后台线程统一清理——没有原地扩展、没有指针跳转、也没有文档 relocation。
- 你执行
db.collection.updateOne({ _id: 1 }, { $set: { desc: "very long text..." } }),WiredTiger不会挪动原文档,而是写入新页,并更新索引指向新位置 -
_id对应的索引条目会被更新,但集合中原始数据块仍保留,直到空间回收完成 - 因此,
find().snapshot()在WiredTiger下无意义,该方法仅对MMAPv1有效
WiredTiger的空闲空间重用机制:不靠填充因子,靠page-level reuse
MMAPv1用填充因子(padding factor)预分配冗余空间来减少移动;WiredTiger彻底抛弃了这个思路。它的空间管理单位是4KB~64KB的数据页,空闲空间重用由引擎自动调度,与单个文档大小无关。
当一个文档被更新变大,WiredTiger会尝试在当前页内腾出空间(比如压缩相邻小文档),失败则分配新页;而被删除或覆盖的旧版本数据,会在后台合并(compact)阶段被真正擦除并归入空闲页池。
- 手动触发空间回收需运行
db.runCommand({ compact: "collection_name" }),但生产环境慎用——它会阻塞写入 - 更安全的做法是依赖WiredTiger内置的
cache_size和eviction策略,让内存压力驱动自动清理 - 填充因子(
collMod中的paddingFactor)在WiredTiger中已被忽略,设置也无效
什么情况下你会“感觉”文档位置变了?其实是游标快照失效
常见误解:顺序遍历游标时重复读到刚更新过的文档 → 归因于“文档移动到了末尾”。这在WiredTiger中不是移动问题,而是游标未启用快照语义导致的MVCC可见性问题。
WiredTiger默认使用“读已提交”(read-committed)隔离级别。如果你在一次 find() 游标遍历中途,另一个客户端更新了某文档,该更新可能在游标后续批次中被重新返回——因为游标每次 next() 都会获取最新快照,而非初始快照。
- 解决方法不是避免更新,而是显式启用快照:
db.collection.find().readConcern("majority").readPreference("primary"),或更严格地用db.collection.find().noCursorTimeout()+ 应用层去重 - 注意:
readConcern: "snapshot"仅在多文档事务中可用,普通查询不支持 - 真正需要
snapshot()的场景极少,且仅限MMAPv1;WiredTiger用户应转向事务或应用层状态控制
验证当前引擎类型和空间行为的三个命令
别猜,直接查。以下命令能立刻确认你面对的是哪种机制:
- 查引擎:
db.serverStatus().storageEngine.name—— 返回"wiredTiger"或"mmapv1" - 查集合统计:
db.collection.stats()—— 关注size(逻辑大小)、storageSize(物理占用)、nindexes,WiredTiger中二者差值常较大,说明有大量待回收空间 - 查写入模式:
db.collection.explain("executionStats").find({}).limit(1)—— 若出现"stage" : "IXSCAN"后紧跟"stage" : "FETCH",说明走索引+回表,这是WiredTiger典型路径;MMAPv1则更倾向全表扫描
最易被忽略的一点:即使你用的是WiredTiger,如果连接串里还带着 ?engine=MMAPv1 或配置文件残留旧参数,MongoDB可能静默降级——务必用 serverStatus 实锤。


















