2026年7月12日 · 11 分钟阅读
MongoDB 入门到进阶:文档模型、CRUD、聚合、索引与高可用
基于 MongoDB 课堂笔记,整理 MongoDB 的定位、BSON 文档模型、常用命令、聚合管道、WiredTiger 存储引擎、索引与 explain、Spring Boot 接入、复制集和分片集群。
MongoDB 是一个面向文档的 NoSQL 数据库。它不像关系型数据库那样把数据拆成表、行、列,而是把一条业务数据组织成一个 BSON 文档,再放进集合中。
如果从 Java 后端开发的视角看,MongoDB 最适合先按这条线理解:
- 它解决什么问题。
- 文档模型和关系模型有什么差异。
- 常用 CRUD、聚合和索引怎么写。
- WiredTiger 为什么能支撑高并发读写。
- 复制集和分片如何解决可用性与扩展性。
MongoDB 适合什么场景
MongoDB 介于关系型数据库和典型 NoSQL 数据库之间。它没有强表结构约束,但提供了比较丰富的查询、索引和聚合能力。
典型适用场景包括:
- 网站实时数据,比如用户行为、内容信息、配置数据。
- 对象和 JSON 数据存储,业务对象可以比较自然地映射成文档。
- 游戏、社交、物流、直播等状态变化频繁、字段结构灵活的业务。
- 物联网日志、设备信息等大规模半结构化数据。
- 地理位置查询、文本查询等需要特殊索引支持的场景。
MongoDB 不是关系型数据库的完全替代品。强事务、复杂关联、多表一致性特别重的系统,仍然更适合优先考虑 MySQL、PostgreSQL 这类 RDBMS。MongoDB 的优势在于文档模型灵活、横向扩展方便、读写吞吐能力强。
与关系型数据库的映射
MongoDB 和 RDBMS 的概念可以这样类比:
| RDBMS | MongoDB |
|---|---|
| database | database |
| table | collection |
| row | document |
| column | field |
| index | index |
| join | embedded 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.files 和 fs.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" })
updateOne 和 updateMany 是局部更新,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 重做后续操作。
可以把它简化为三层:
- Cache 承接读写,减少直接磁盘访问。
- Journal 通过 WAL 保障崩溃恢复。
- 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 包括 IXSCAN、FETCH + IXSCAN、IDHACK;常见危险信号是 COLLSCAN 全集合扫描、无索引排序导致的 SORT。
慢查询可以通过 profiler 打开:
db.setProfilingLevel(1, 100)
db.system.profile.find().sort({ millis: -1 }).limit(3)
慢查询原因通常有几类:数据模型不合理、缺少索引、索引顺序不匹配、硬件资源不足、查询返回范围过大。分析时不要只盯耗时,要把 nReturned、totalKeysExamined、totalDocsExamined 和 stage 一起看。
Java 和 Spring Boot 接入
Java 原生驱动可以直接使用 MongoClient、MongoDatabase、MongoCollection<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。
复制集主要解决三个问题:
- 高可用,主节点故障后自动选举。
- 数据冗余,多副本降低单点风险。
- 读写分离,分析、报表等读请求可以放到从节点。
水平扩展:分片集群
当单机磁盘、内存或 IOPS 无法支撑数据规模时,就需要分片。
分片集群包含三类核心组件:
| 组件 | 作用 |
|---|---|
| Shard Server | 存储实际数据,每个 shard 通常是一个复制集 |
| Router Server | mongos,作为请求入口,负责路由请求 |
| Config Server | 保存分片元数据、路由信息和 chunk 分布 |
MongoDB 通过片键把集合切分成多个 chunk,再把 chunk 分布到不同 shard 上。
选择片键时要同时考虑查询和写入:
- 查询时最好能命中尽量少的分片。
- 写入时最好能均匀分散到多个分片。
- 片键基数不能太小,否则数据容易集中。
- 没有合适单字段时,可以考虑组合片键。
常见分片策略有两种:
| 策略 | 优点 | 缺点 |
|---|---|---|
| 范围分片 | 范围查询效率好 | 递增片键容易造成写入热点 |
| 哈希分片 | 写入分布更均匀 | 范围查询通常要访问更多分片 |
所以分片不是“数据大了就开”。真正要先回答的是:业务最核心的查询路径是什么,写入是否存在明显热点,片键能否同时兼顾定位能力和分布均匀性。
小结
MongoDB 可以按一条主线串起来:
- 数据以 BSON 文档存储,集合类似关系型数据库的表。
- 建模时优先围绕读写路径选择内嵌或引用。
- CRUD 是基础,聚合管道是复杂统计和数据加工的核心。
- WiredTiger 通过 Cache、journal、checkpoint 平衡性能和可靠性。
- 索引能减少扫描,但会增加写入和存储成本。
explain("executionStats")是分析查询性能的关键工具。- Java 项目中,简单 CRUD 可以用 Repository,复杂查询和更新更适合 MongoTemplate。
- 复制集解决高可用,分片集群解决水平扩展。
学 MongoDB 不能只记命令。真正重要的是文档模型、索引设计和高可用架构:数据怎么组织,查询怎么命中,故障时怎么恢复,规模变大后怎么拆分。把这几个问题想清楚,MongoDB 才能从“会用”进入“用得稳”。