Skip to content

MongoDB ​

Java 高级 · 存储与搜索

MongoDB 是文档型 NoSQL,以灵活的 schema 与水平扩展著称。但"灵活"是把双刃剑——建模错了就是灾难。本文聚焦文档模型设计(内嵌 vs 引用)、聚合管道、WiredTiger 存储引擎(B-tree + 多版本并发)、副本集选举与 oplog、分片 chunk 迁移,讲清它"快在哪、怎么保证高可用、何时该分片"。

一、BSON 与文档模型:内嵌还是引用 ​

MongoDB 用 BSON(Binary JSON) 存文档,支持嵌套、数组、多种类型(Date、ObjectId、Decimal128)。集合(Collection)内文档结构可不同,但良好设计仍建议字段一致。

建模的核心权衡是内嵌(Embed)vs 引用(Reference):

  • 内嵌(Embed):一对少、强一致读取(如"订单含商品列表"),一次查询即得,避免 join。代价:文档可能变大、更新需改大对象。
  • 引用(Reference):一对多/多对多、数据频繁独立变化(如"用户-评论"),用 _id 关联,$lookup 联查。代价:读需多次或聚合。

WARNING

  • 无脑内嵌导致文档过大:超过 16MB 限制或频繁整体更新大文档,性能反而差。大数组应评估是否引用拆分。
  • 用 MongoDB 做复杂多表事务:虽支持多文档事务(4.0+),但成本高、锁范围大、跨分片事务更慢。强事务场景仍应首选关系型。
  • shard key 选错:单调递增 key(如时间戳)会造成写热点集中在单分片,应选高基数且分布均匀的键。

二、聚合管道(Aggregation Pipeline) ​

聚合是 MongoDB 的数据分析利器,由多个 Stage 顺序处理,每个 Stage 把结果传给下一个:

javascript
db.orders.aggregate([
  { $match: { created: { $gte: new Date(Date.now()-30*86400000) } } }, // 1. 过滤
  { $group: { _id: "$userId",
              totalAmt: { $sum: "$total" },
              cnt: { $sum: 1 } } },                                     // 2. 分组聚合
  { $sort: { totalAmt: -1 } },                                          // 3. 排序
  { $limit: 10 }                                                        // 4. 取前10
]);

常见 Stage:$match(过滤,越早越好,能用索引)、$group(分组聚合)、$project(字段裁剪)、$lookup(左连接)、$unwind(数组展开)、$sort、$limit、$skip、$bucket(分桶)。管道可下推 $match/$sort 到索引扫描,性能可观。

三、WiredTiger:存储引擎与并发 ​

MongoDB 默认引擎 WiredTiger,核心特性:

  • B-tree 索引 + 文档存储:集合数据按 _id 的 B-tree 组织;每个集合/索引一个文件。查询走索引(B-tree)定位文档。索引结构类似关系型,支持复合索引最左前缀匹配。
  • 多版本并发控制(MVCC):写操作不阻塞读。WiredTiger 用 checkpoint(每 60s 落盘)+ journal(WAL,类似 redo log)。写先记 journal 再改内存,宕机可从 journal 恢复。
  • 文档级锁:WiredTiger 用文档级并发(非表锁),高并发写入互不阻塞(相比旧 MMAPv1 的库级锁是质的提升)。
  • 压缩:集合与索引默认 snappy 压缩,省磁盘(读时解压,CPU 换 IO)。

四、副本集:选举与 oplog(高可用真相) ​

副本集(Replica Set)是一主多从,Primary 处理写,Secondary 从 oplog 同步。

  • oplog(操作日志):Primary 的写操作记录到 oplog(固定集合,幂等格式),Secondary 拉取并重放,实现异步复制。oplog 是幂等的(重放安全)。
  • 选举:节点间心跳,Primary 不可达时,存活节点比较数据新鲜度(oplog 时间戳),获多数派投票者成为新 Primary。所以副本集至少 3 节点(容忍 1 个故障)。votes/priority 可影响谁优先当选。
  • 读写分离:客户端可设 readPreference=secondary 把读分流到从节点(牺牲一点一致性换读扩展),writeConcern 控制写需确认几个节点。

writeConcern / readConcern ​

  • writeConcern: { w: "majority" }:写需多数派确认才返回,主宕机不丢已确认数据(强可靠)。
  • readConcern: "majority":只读到"已被多数派确认"的数据,避免读到会被回滚的孤立数据。
  • 二者配合实现因果一致/线性一致(类似 Kafka 的 ISR 思路)。

五、分片(Sharding):水平扩展 ​

单副本集容量/写入到顶时,用分片把数据按 shard key 分布到多个分片:

  • mongos(路由):客户端只连 mongos,它把请求路由到对应分片。
  • config server:存集群元数据(哪些 chunk 在哪个分片)。
  • chunk:数据是按 shard key 范围切成的 chunk(默认 64MB)。写数据按 shard key 落到某 chunk、某分片。
  • chunk split:chunk 超 64MB 自动分裂成两个。
  • balancer(均衡器):后台把 chunk 在分片间迁移,使数据均衡。迁移有 IO 开销,可设窗口。
  • shard key 选择:高基数(值多)、分布均匀、且常出现在查询条件中。单调递增 key(时间戳)造成写热点,哈希分片可打散(但范围查询变扫全分片)。

TIP

分片键选错是不可逆的灾难——MongoDB 改 shard key 极难(需重导数据)。经验:写多且需范围查询 → 复合/哈希组合;避免单调 key 制造热点;让高频查询条件含 shard key,避免广播查询(每个分片都查一遍)。

六、生产落地 ​

  • 模型设计遵循"读多写少则内嵌、独立变化则引用",以查询模式驱动 schema。
  • 建与查询匹配的复合索引,用 explain("executionStats") 排查慢查询(看 IXSCAN vs COLLSCAN)。
  • 部署副本集(≥3 节点)+ 合理 writeConcern=majority/readPreference 平衡一致与性能。
  • 大表提前规划分片键,避免后期迁移成本。

📚 官方文档与参考资料 ​

✏️ 练习与自测 ​

  1. 给定"博客-评论"场景,说明何时内嵌评论、何时引用,并给出理由(文档大小/更新频率/查询方式)。
  2. 写一个聚合管道统计每个标签下文章数(数组字段多键展开 $unwind + $group)。
  3. 解释副本集选举与 writeConcern=majority 如何共同影响写入可靠性(多数派确认 + 多数派选主)。
  4. 为什么 shard key 不能选单调递增的时间戳?给出更优的 shard key 设计思路。

本节小结

MongoDB 的精髓是"以文档建模(内嵌换读快、引用换写快)+ 聚合管道做分析 + 副本集保高可用 + 分片保水平扩展"。WiredTiger 用 B-tree + MVCC + 文档级锁 + journal 实现高性能并发;副本集靠 oplog 异步复制 + 多数派选举保证高可用;分片靠 chunk split/balance 实现弹性。设计核心是按查询模式决定内嵌还是引用、shard key 选稳且均匀,它擅长灵活结构,但不应强行替代强事务关系库。