Skip to content

MongoDB 实现原理

MongoDB 是面向文档的 NoSQL 数据库,使用 BSON(Binary JSON)格式存储数据。理解 MongoDB 的实现,需要深入它的存储引擎、复制集、分片集群和查询处理。

文档模型

BSON 结构

BSON 是 JSON 的二进制编码,支持更多数据类型:日期、二进制、ObjectId、嵌套文档、数组。BSON 是自描述的,每个元素有类型和长度。BSON 的设计目标是:可遍历(支持流式解析)、高效(C 语言友好的内存布局)、可扩展(支持嵌套和数组)。

_id 与 ObjectId

每个文档必须有一个 _id 字段,作为主键。_id 可以是任何类型,默认是 ObjectId。ObjectId 是 12 字节的标识符:4 字节时间戳 + 5 字节随机 + 3 字节计数器。ObjectId 可以大致按时间排序,且在分布式环境下唯一。

文档更新

MongoDB 的更新是原子的:找到匹配的文档,读取整个文档,修改字段,写回整个文档。这是由于文档存储紧凑,没有固定结构,原地更新难以实现。对于大文档,更新可能较慢。文档大小限制是 16MB,超过需要使用 GridFS。

存储引擎

WiredTiger

WiredTiger 是 MongoDB 3.2+ 的默认存储引擎,支持文档级锁、压缩、checkpoint。WiredTiger 将数据存储在 B+ 树中,支持多种索引:B-tree 索引、唯一索引、TTL 索引、地理空间索引、全文索引、哈希索引。

WiredTiger 的写操作不直接修改数据页,而是写入修改日志,然后更新内存中的数据页。后台线程定期 checkpoint,将内存页刷盘。checkpoint 后,修改日志可以删除。MongoDB 可以配置 journal(WAL),每次提交前将修改日志刷盘,保证持久性。

压缩

WiredTiger 支持多种压缩算法:snappy(默认)、zlib、zstd。压缩可以减少磁盘空间和 I/O,代价是 CPU 开销。对于 CPU 充裕、I/O 受限的场景,压缩是值得的。

索引

_id 索引

_id 索引默认创建,唯一且不可删除。_id 索引保证文档唯一性,也用于分片集群的路由。

单字段索引

单字段索引支持升序或降序,可以加速等值查询和范围查询。

复合索引

复合索引支持多字段组合查询,遵循最左前缀原则。索引 (a, b, c) 可以加速查询 a、ab、abc,不能加速 b、bc、c。复合索引的字段顺序很重要:区分度高的字段放前面。

多键索引

多键索引是数组字段的索引。数组中的每个元素都会被索引,查询可以匹配数组的任意元素。多键索引不能是 shard key(分片键)。

地理空间索引

2dsphere 索引支持 GeoJSON 格式的地理数据,支持位置查询和范围查询。2d 索引支持旧版的坐标对查询。

全文索引

全文索引支持文本搜索,支持多种语言(包括中文)。全文索引基于词干提取和停用词,支持短语搜索和相关性排序。

索引属性

TTL 索引自动删除过期文档:索引字段是日期类型,文档在指定秒数后删除。Sparse 索引只索引包含该字段的文档,节省空间。Partial 索引只索引满足条件的文档,减少索引大小。

复制集

主从架构

复制集是一组 MongoDB 节点,一个主节点(Primary),多个从节点(Secondary)。所有写操作发送到主节点,主节点将操作记录到 oplog(操作日志),从节点异步拉取 oplog 并执行。读操作默认发送到主节点,可以配置读取从节点(牺牲一致性)。

自动故障转移

每个节点定期心跳其他节点,如果主节点宕机,从节点选举新主节点。选举使用 Raft 类似的算法:多数节点投票,票数超过半数的节点当选主节点。仲裁节点(Arbiter)只参与投票,不存储数据,用于凑够奇数个节点。

写关注

写关注(Write Concern)控制写操作的持久性:w:1(主节点确认)、w:majority(多数节点确认)、w:0(不等待确认,最快但不安全)、j:true(journal 刷盘)。写关注越强,延迟越高,但数据越安全。

读关注

读关注(Read Concern)控制读操作的一致性:local(读最新数据,可能未提交)、majority(读多数节点确认的数据)、linearizable(读线性化数据,最慢)、available(读任意节点的数据,最快)。

分片集群

架构组件

分片集群包括:配置服务器(Config Server,存储集群元数据)、路由服务器(Mongos,路由查询和写入)、分片服务器(Shard,存储数据)。配置服务器是复制集,保证元数据高可用。路由服务器无状态,可以部署多个。分片服务器是复制集,保证数据高可用。

分片键

分片键(Shard Key)是文档的一个字段,用于决定文档存储在哪个分片。分片键的选择至关重要:选择不当会导致数据倾斜(chunk 分配不均)或查询效率低(需要查询多个分片)。分片键一旦选定,不能更改。

分片策略

范围分片(Ranged Sharding):按分片键的范围分配 chunk,适合范围查询,但可能导致数据倾斜。哈希分片(Hashed Sharding):按分片键的哈希分配 chunk,数据均匀,但范围查询需要查询所有分片。地理空间分片(Geo Sharding):按地理位置分配 chunk,适合地理数据。

Chunk 迁移

每个 chunk 默认 64MB,当 chunk 超过阈值时,分裂成两个 chunk。当分片的 chunk 数量差异较大时,触发 chunk 迁移,将 chunk 从多的分片迁移到少的分片。迁移过程中,chunk 可读可写,源分片修改会同步到目标分片,迁移完成后删除源分片的 chunk。

查询路由

查询通过 mongos 路由:如果查询包含分片键,mongos 直接发送到对应分片(目标查询)。如果查询不包含分片键,mongos 发送到所有分片(分散聚集查询)。分散聚集查询性能差,应避免。

聚合管道

聚合管道是 MongoDB 的查询语言,由多个阶段(stage)组成:matchproject(投影)、groupsort(排序)、limitskip(跳过)、lookupunwind(展开数组)、$facet(多管道并行)。

聚合管道是数据流模型,每个阶段接收上一个阶段的输出,处理后输出到下一个阶段。MongoDB 会优化聚合管道:尽可能提前过滤、下推到存储引擎、利用索引。

MongoDB 是灵活、高性能、可扩展的文档数据库。理解它的实现,有助于更好地设计文档模型、选择索引和分片键,以及调优查询性能。