2026年7月12日 · 11 分钟阅读

MongoDB 入门到进阶:文档模型、CRUD、聚合、索引与高可用

基于 MongoDB 课堂笔记,整理 MongoDB 的定位、BSON 文档模型、常用命令、聚合管道、WiredTiger 存储引擎、索引与 explain、Spring Boot 接入、复制集和分片集群。

MongoDB 是一个面向文档的 NoSQL 数据库。它不像关系型数据库那样把数据拆成表、行、列,而是把一条业务数据组织成一个 BSON 文档,再放进集合中。

如果从 Java 后端开发的视角看,MongoDB 最适合先按这条线理解:

  1. 它解决什么问题。
  2. 文档模型和关系模型有什么差异。
  3. 常用 CRUD、聚合和索引怎么写。
  4. WiredTiger 为什么能支撑高并发读写。
  5. 复制集和分片如何解决可用性与扩展性。

MongoDB 适合什么场景

MongoDB 介于关系型数据库和典型 NoSQL 数据库之间。它没有强表结构约束,但提供了比较丰富的查询、索引和聚合能力。

典型适用场景包括:

  • 网站实时数据,比如用户行为、内容信息、配置数据。
  • 对象和 JSON 数据存储,业务对象可以比较自然地映射成文档。
  • 游戏、社交、物流、直播等状态变化频繁、字段结构灵活的业务。
  • 物联网日志、设备信息等大规模半结构化数据。
  • 地理位置查询、文本查询等需要特殊索引支持的场景。

MongoDB 不是关系型数据库的完全替代品。强事务、复杂关联、多表一致性特别重的系统,仍然更适合优先考虑 MySQL、PostgreSQL 这类 RDBMS。MongoDB 的优势在于文档模型灵活、横向扩展方便、读写吞吐能力强。

与关系型数据库的映射

MongoDB 和 RDBMS 的概念可以这样类比:

RDBMSMongoDB
databasedatabase
tablecollection
rowdocument
columnfield
indexindex
joinembedded document 或引用
primary key_id 字段

关系型数据库通常强调范式化,把数据拆到多张表,再通过主外键关联。MongoDB 更强调把经常一起读取的数据放进同一个文档,减少跨集合关联和多次查询。

这带来一个关键建模问题:什么时候内嵌,什么时候引用?

建模方式适合场景
内嵌文档一对一、一对多包含关系;经常一起读取;聚合计算通常在同一个集合内完成
引用数据重复成本很高;复杂多对多关系;层级结构太大或嵌套太深

一个常见判断标准是:如果业务读取时总是一起拿出来,优先内嵌;如果数据生命周期不同、重复更新成本高,优先引用。

BSON 和文档模型

MongoDB 使用 BSON 存储数据。BSON 可以理解为二进制形式的 JSON,支持字符串、整数、浮点数、布尔值、数组、对象、日期、ObjectId、Null、二进制数据等类型。

例如一个用户文档可以写成:

{
  _id: ObjectId("..."),
  name: "hero",
  age: 18,
  status: "A",
  address: {
    city: "beijing",
    district: "chaoyang"
  },
  tags: ["java", "mongodb"]
}

文档模型的好处是业务表达直接。地址、标签、订单状态流转这类数据,不一定非要拆成多张表。代价是模型设计需要更关注读写路径,否则很容易出现文档过大、字段重复、数组无限增长等问题。

MongoDB 对单个 BSON 文档大小有限制。课堂笔记里还提到文件存储:小二进制内容可以转码存储,较大的文件通常使用 GridFS。GridFS 会通过 fs.filesfs.chunks 两个集合保存文件元信息和分片内容。

常用命令:数据库、集合、文档

MongoDB 的基础操作可以按三层记忆:数据库、集合、文档。

数据库操作:

use hero
show dbs
db.dropDatabase()

集合操作:

db.createCollection("users")
show collections
db.users.drop()

文档插入:

db.users.insertOne({
  name: "hero",
  age: 18,
  status: "A"
})

db.users.insertMany([
  { name: "benson", age: 42, status: "A" },
  { name: "yilia", age: 22, status: "A" },
  { name: "vincent", age: 34, status: "D" }
])

批量插入比循环单条插入更适合大批量数据写入,因为它可以减少大量零散网络请求和请求头开销。

查询语法:

db.users.find({ status: "A" })
db.users.find({ age: { $gt: 18 } })
db.users.find({ status: { $in: ["A", "P"] } })
db.users.find({ $or: [{ age: { $lt: 20 } }, { status: "D" }] })
db.users.find({ "address.city": "beijing" })

分页和排序:

db.users
  .find({ status: "A" })
  .sort({ age: -1 })
  .skip(20)
  .limit(10)

更新和删除:

db.users.updateOne(
  { name: "hero" },
  { $set: { age: 19 } }
)

db.users.updateMany(
  { status: "A" },
  { $set: { active: true } }
)

db.users.replaceOne(
  { name: "hero" },
  { name: "hero", age: 20, status: "P" }
)

db.users.deleteOne({ name: "hero" })
db.users.deleteMany({ status: "D" })

updateOneupdateMany 是局部更新,replaceOne 是整体替换除 _id 之外的文档内容。这个区别很重要,误用 replaceOne 很容易把原文档里的字段覆盖掉。

聚合:从 find 到管道思维

普通查询解决的是“找出哪些文档”,聚合解决的是“把文档加工成什么结果”。

MongoDB 聚合主要包括三类:

类型说明
单目的聚合count()distinct()
Aggregation Pipeline通过多个 stage 组合过滤、分组、投影、排序、限制
MapReduce用 JavaScript 的 map 和 reduce 处理复杂聚合逻辑

聚合管道最常用。每个 stage 接收上一阶段输出,再把处理结果交给下一阶段。

常见 stage:

stage作用
$match过滤文档
$group分组统计
$project控制字段、重命名字段、计算新字段
$sort排序
$limit限制返回数量

一个典型聚合例子:

db.authors.aggregate([
  { $match: { like: { $gte: 10 } } },
  { $group: { _id: "$author", count: { $sum: 1 } } },
  { $sort: { count: -1 } },
  { $limit: 10 }
])

这段逻辑可以理解为:先筛选点赞数大于等于 10 的文章,再按作者分组统计数量,最后按数量倒序取前 10。

聚合管道还支持算术表达式、字符串表达式、日期表达式、比较表达式和逻辑表达式。例如 $add$subtract$concat$toLower$month$eq$and$cond$ifNull 等。

MapReduce 的模型是先 Map 拆分,再 Reduce 合并。它更适合复杂、可并行的大规模统计,但日常业务查询中,优先考虑聚合管道即可。

WiredTiger 存储引擎

MongoDB 3.2 开始默认使用 WiredTiger 存储引擎。课堂笔记把它和早期 MMAPv1 做了对比,核心差异有几类:

维度WiredTiger
存储结构BTree / page 组织
并发控制文档级别锁
压缩支持 snappy、zlib 等压缩
内存可以配置 cache 大小
持久化cache、journal、checkpoint 协作

WiredTiger 写入时,数据会先进入 Cache,并写入 journal 日志。默认情况下,WiredTiger 会周期性执行 checkpoint,生成一致性快照。发生宕机时,MongoDB 可以先恢复到最近 checkpoint,再根据 journal 重做后续操作。

可以把它简化为三层:

  1. Cache 承接读写,减少直接磁盘访问。
  2. Journal 通过 WAL 保障崩溃恢复。
  3. Checkpoint 周期性固化一致性视图。

WiredTiger 还使用 Copy on Write 思路管理写操作。写入持久化时,不直接在原 leaf page 上修改,而是写入新分配的 page;每次 checkpoint 都可能产生新的 root page。

这和 MySQL InnoDB 的 WAL、脏页、checkpoint 思想有相似之处:都不是每次业务写入都直接随机落盘,而是通过内存、日志和刷盘机制平衡性能与可靠性。

索引:不是越多越好

MongoDB 默认会为集合的 _id 创建唯一索引。索引的目标是减少扫描文档数量,提高查询效率。

创建索引:

db.users.createIndex({ age: 1 })
db.users.createIndex({ status: 1, age: -1 })
db.users.getIndexes()
db.users.totalIndexSize()
db.users.dropIndex("age_1")

索引能提升查询速度,但会降低写入速度,并占用额外存储空间。因此索引设计要围绕高频查询条件、排序字段和聚合过滤字段展开。

常见索引类型:

类型说明
单字段索引对单个字段创建升序或降序索引
复合索引对多个字段组合建索引,字段顺序很关键
多键索引对数组字段的每个元素建立索引
地理空间索引支持地理位置查询,如 2dsphere
全文索引支持字符串文本检索,但中文分词能力有限
哈希索引支持等值查询,不适合范围查询

复合索引要特别注意字段顺序。例如 { status: 1, age: -1 } 更适合先按 status 过滤,再按 age 排序或筛选的查询。数组字段会触发多键索引,explain 里可以看到 isMultiKey

explain 和慢查询分析

MongoDB 的 explain() 用来观察查询计划。常用模式有三种:

模式说明
queryPlanner默认模式,查看优化器选择的执行计划
executionStats返回执行统计信息,开发中最常用
allPlansExecution查看最优计划和候选计划的详细执行信息

示例:

db.users.find({ age: 18 }).explain("executionStats")

重点看这些字段:

字段关注点
executionTimeMillis查询总耗时,越低越好
nReturned返回文档数量
totalKeysExamined扫描索引条目数
totalDocsExamined扫描文档数
winningPlan.stage最终采用的执行阶段

比较理想的状态是扫描的索引和文档数量接近返回数量。常见较好的 stage 包括 IXSCANFETCH + IXSCANIDHACK;常见危险信号是 COLLSCAN 全集合扫描、无索引排序导致的 SORT

慢查询可以通过 profiler 打开:

db.setProfilingLevel(1, 100)
db.system.profile.find().sort({ millis: -1 }).limit(3)

慢查询原因通常有几类:数据模型不合理、缺少索引、索引顺序不匹配、硬件资源不足、查询返回范围过大。分析时不要只盯耗时,要把 nReturnedtotalKeysExaminedtotalDocsExamined 和 stage 一起看。

Java 和 Spring Boot 接入

Java 原生驱动可以直接使用 MongoClientMongoDatabaseMongoCollection<Document> 操作文档。

MongoClient mongoClient = new MongoClient("127.0.0.1", 27017);
MongoDatabase database = mongoClient.getDatabase("hero");
MongoCollection<Document> collection = database.getCollection("employee");

Document doc = Document.parse("{name:'benson', city:'beijing', salary:18000}");
collection.insertOne(doc);

collection
    .find(Filters.gt("salary", 10000))
    .sort(Document.parse("{salary:-1}"))
    .forEach(document -> System.out.println(document.toJson()));

mongoClient.close();

Spring Boot 项目通常引入 spring-boot-starter-data-mongodb,然后有两种常见使用方式。

MongoTemplate 更灵活,适合复杂查询和更新:

Query query = new Query(where("id").is("11"));
Update update = new Update().set("lastName", "hero110");
mongoTemplate.updateMulti(query, update, Employee.class);

MongoRepository 更接近 Spring Data 的声明式 Repository 风格:

@Document("employee")
public class Employee {
    @Id
    private String id;
    private int empId;
    private String firstName;
    private String lastName;
    private float salary;
}

public interface EmployeeRepository extends MongoRepository<Employee, String> {
}

简单 CRUD 用 Repository 会更省代码;复杂动态条件、聚合、局部更新,用 MongoTemplate 更可控。

高可用:复制集

早期 MongoDB 有 master-slave 主从结构,但它没有自动故障转移能力,而且 MongoDB 4.0 后已经不再支持主从复制。生产环境应该使用复制集。

复制集由一组拥有相同数据集的 MongoDB 实例组成,常见角色包括:

角色说明
Primary主节点,负责写入,也可以读取
Secondary从节点,从 Primary 同步数据,可用于读取
Arbiter仲裁节点,只参与投票,不保存业务数据

Primary 会把改变数据库状态的操作写入 oplog。Secondary 持续拉取 oplog 并在本地重放,从而保持数据一致。

复制集同步分为:

  • 初始化同步:节点第一次加入,或落后超过 oplog 保留范围时触发全量同步。
  • 增量同步:初始化完成后持续基于 oplog 追赶主节点。

复制集通过心跳检测和选举实现故障转移。Primary 会和其他节点维持通信;如果 Primary 无法和大多数成员通信,会主动降级。Secondary 在发现没有可用 Primary 时,会发起选举。获得多数票的节点成为新的 Primary。

复制集主要解决三个问题:

  1. 高可用,主节点故障后自动选举。
  2. 数据冗余,多副本降低单点风险。
  3. 读写分离,分析、报表等读请求可以放到从节点。

水平扩展:分片集群

当单机磁盘、内存或 IOPS 无法支撑数据规模时,就需要分片。

分片集群包含三类核心组件:

组件作用
Shard Server存储实际数据,每个 shard 通常是一个复制集
Router Servermongos,作为请求入口,负责路由请求
Config Server保存分片元数据、路由信息和 chunk 分布

MongoDB 通过片键把集合切分成多个 chunk,再把 chunk 分布到不同 shard 上。

选择片键时要同时考虑查询和写入:

  • 查询时最好能命中尽量少的分片。
  • 写入时最好能均匀分散到多个分片。
  • 片键基数不能太小,否则数据容易集中。
  • 没有合适单字段时,可以考虑组合片键。

常见分片策略有两种:

策略优点缺点
范围分片范围查询效率好递增片键容易造成写入热点
哈希分片写入分布更均匀范围查询通常要访问更多分片

所以分片不是“数据大了就开”。真正要先回答的是:业务最核心的查询路径是什么,写入是否存在明显热点,片键能否同时兼顾定位能力和分布均匀性。

小结

MongoDB 可以按一条主线串起来:

  1. 数据以 BSON 文档存储,集合类似关系型数据库的表。
  2. 建模时优先围绕读写路径选择内嵌或引用。
  3. CRUD 是基础,聚合管道是复杂统计和数据加工的核心。
  4. WiredTiger 通过 Cache、journal、checkpoint 平衡性能和可靠性。
  5. 索引能减少扫描,但会增加写入和存储成本。
  6. explain("executionStats") 是分析查询性能的关键工具。
  7. Java 项目中,简单 CRUD 可以用 Repository,复杂查询和更新更适合 MongoTemplate。
  8. 复制集解决高可用,分片集群解决水平扩展。

学 MongoDB 不能只记命令。真正重要的是文档模型、索引设计和高可用架构:数据怎么组织,查询怎么命中,故障时怎么恢复,规模变大后怎么拆分。把这几个问题想清楚,MongoDB 才能从“会用”进入“用得稳”。