跳到主要内容

存储引擎 WiredTiger

MongoDB 3.2 起的默认引擎(取代 MMAPv1,后者在 4.2 被彻底移除)。面试问「MongoDB 为什么写入快」,答案几乎全在这里。

核心特性​

特性说明
文档级并发控制写锁粒度到单个文档,不同文档的写入互不阻塞
MVCC读操作看快照,读写不互斥
B+ 树存储和 InnoDB 一样,不是 LSM(但 WiredTiger 也支持 LSM 配置,MongoDB 默认不用)
压缩集合默认 Snappy,索引默认前缀压缩,可选 zlib / zstd
无 doublewrite靠 checkpoint + journal 保证一致,不需要双写缓冲

为什么写入快​

要分几层答,这是和 MySQL 对比的重点:

1. 锁粒度 —— MMAPv1 时代是库级锁(2.2 之前甚至是实例级),WiredTiger 做到文档级。InnoDB 是行级锁,两者在这一层其实相当,但 MongoDB 的乐观并发控制(冲突时重试而非阻塞)在低冲突场景下更轻。

2. 默认不等刷盘 —— 这是最关键的一点。MongoDB 默认 writeConcern: {w: 1, j: false},意思是主节点内存写入成功就返回,不等 journal 落盘。而 InnoDB 默认 innodb_flush_log_at_trx_commit = 1,每次提交都 fsync。

所以「MongoDB 写入比 MySQL 快」这个结论在很大程度上是默认持久化级别不同造成的,不完全是引擎优劣。把 MongoDB 调成 {w: "majority", j: true},写入延迟会显著上升。面试主动指出这一点会很加分。

3. 没有 JOIN 和强约束 —— 不需要检查外键、不需要维护跨表一致性。

4. 嵌入式文档减少了往返 —— 一次写入完成原本需要多张表多次插入的工作。

MVCC 与快照​

WiredTiger 为每个事务分配一个快照(snapshot),读操作只看该快照中已提交的数据。实现上和 InnoDB 的思路一致(版本 + 可见性判断),但有两个差异:

  • 版本存在内存的 update 链表里,而非 InnoDB 那样的 undo log 文件。所以长事务会撑爆内存(WiredTiger cache),而不只是让版本链变长
  • 乐观并发控制 —— 两个事务改同一个文档时,后提交的会收到 WriteConflict 错误。MongoDB 会在服务端自动重试(单文档操作)或把错误抛给应用(事务中需要应用层重试)

:::warning TransientTransactionError 必须重试 在多文档事务里遇到写冲突,MongoDB 返回带 TransientTransactionError 标签的错误。这是设计上要求应用重试整个事务,不是 bug。官方驱动的 withTransaction() 辅助方法内置了重试逻辑,手写事务代码则必须自己实现。这是一个很能区分「用过」和「只看过文档」的问题。 :::

Checkpoint 与 Journal​

双重机制保证持久性,容易混淆,要能讲清分工:

Checkpoint(检查点)

  • 每 60 秒(或 journal 写满 2GB)做一次,把内存中的脏页以一致性快照的形式写入磁盘
  • 采用写时复制 —— 新数据写到新位置,checkpoint 完成后才更新根页指针。所以 checkpoint 本身是原子的,不存在「写到一半」的状态
  • 崩溃后如果没有 journal,最多回退到上一个 checkpoint,最多丢 60 秒数据

Journal(WAL 日志)

  • 作用等同于 InnoDB 的 redo log,记录自上次 checkpoint 以来的所有变更
  • 默认每 100ms 刷盘一次(j: true 的写入会立即触发刷盘并等待)
  • 用 Snappy 压缩,单文件 100MB 滚动
  • 崩溃恢复:加载最后一个 checkpoint,然后重放之后的 journal

所以默认配置下的丢数据窗口是 100ms(journal 刷盘间隔),而不是 60 秒。但如果用默认的 j: false,客户端收到成功响应时数据可能还在内存里 —— 这个窗口内主节点崩溃,数据就丢了。

WiredTiger Cache​

默认大小 = max((物理内存 - 1GB) × 50%, 256MB)。

这个 50% 常被误解为「MongoDB 只用一半内存」。实际上剩下的内存并没有浪费 —— 它留给操作系统的文件系统缓存(page cache)。两者分工:

  • WiredTiger cache 存解压后的数据(B+ 树页)
  • OS page cache 存压缩后的数据块

所以调大 WiredTiger cache 不一定更快,可能挤占 page cache 反而变慢。官方建议不要超过 80%。

淘汰(eviction) —— cache 使用率达到 80% 开始后台淘汰,到 95% 时应用线程会被拉去参与淘汰(此时延迟剧增,是一个重要的告警指标)。

压缩​

对象默认可选
集合数据Snappyzlib(压缩率高、CPU 高)、zstd(4.2+,平衡最好)
索引前缀压缩可关闭
journalSnappy—

Snappy 典型压缩率 2~4 倍。这是 MongoDB 相对 MySQL 的一个实际优势 —— InnoDB 的页压缩(COMPRESSED 行格式)效果和易用性都不如 WiredTiger。

索引前缀压缩:相邻索引键共享前缀只存一次。对 email、url 这类有公共前缀的字段效果显著。

和 InnoDB 的对照​

WiredTigerInnoDB
数据结构B+ 树B+ 树
锁粒度文档级(乐观,冲突重试)行级(悲观,阻塞等待)
索引组织非聚簇 —— 索引叶子存 RecordId,数据另存聚簇 —— 主键索引叶子直接存行数据
MVCC 版本位置内存 update 链undo log(磁盘)
WALjournal(默认 100ms 刷)redo log(默认每次提交刷)
部分页写保护不需要(写时复制 + 原子 checkpoint)需要 doublewrite buffer
压缩默认开启 Snappy默认不压缩

:::tip 索引组织方式的差异很值得展开 InnoDB 是聚簇索引:主键查询一次定位就拿到数据,二级索引要回表。

WiredTiger 是非聚簇:所有索引(包括 _id)叶子节点存的都是 RecordId,再用 RecordId 去数据文件取文档。换句话说,MongoDB 连主键查询都相当于「回表」一次。

  • 好处:没有「主键大小影响所有二级索引大小」的问题;没有页分裂导致的插入性能悬崖
  • 坏处:主键查询比 InnoDB 多一跳

MongoDB 5.3+ 引入了 clustered collection(只能按 _id 聚簇),补上了这个短板,适合时序和日志类集合。 :::