数据库05:MongoDB
从文档模型、副本集、分片集群到聚合管道,理解 MongoDB 在非结构化数据存储场景下的设计思路和适用边界。
MongoDB 是最流行的 NoSQL 数据库之一,它的文档模型特别适合存储半结构化数据。很多项目用它来存储用户行为、日志、配置等数据。
这篇文章围绕四个核心点:文档模型、副本集、分片集群、聚合管道,理解 MongoDB 的设计思路和适用边界。
文档模型:为什么需要 NoSQL
传统关系型数据库的表结构是固定的,不适合存储结构多变的数据。MongoDB 的文档模型解决了这个问题。
文档 vs 行
| 维度 | 关系型数据库 | MongoDB |
|---|---|---|
| 数据结构 | 固定表结构 | 动态文档 |
| 数据类型 | 基础类型 | 支持嵌套、数组 |
| 关系 | 外键关联 | 嵌入式文档 |
| 扩展性 | 需要 ALTER TABLE | 无 schema,灵活扩展 |
嵌入式文档 vs 引用
MongoDB 有两种方式处理关联数据:
嵌入式文档:
{
_id: ObjectId("..."),
name: "旋大爷",
address: {
city: "北京",
street: "中关村"
},
orders: [
{ orderId: "O001", amount: 100 },
{ orderId: "O002", amount: 200 }
]
}
引用方式:
// 用户文档
{
_id: ObjectId("..."),
name: "旋大爷",
orderIds: [ObjectId("..."), ObjectId("...")]
}
// 订单文档
{
_id: ObjectId("..."),
userId: ObjectId("..."),
amount: 100
}
选择建议
| 场景 | 嵌入式文档 | 引用方式 |
|---|---|---|
| 数据一起读取 | ✓ | ✗ |
| 数据独立更新 | ✗ | ✓ |
| 一对多关系 | ✓(文档大小 < 16MB) | ✓(大量子文档) |
| 多对多关系 | ✗ | ✓ |
副本集:高可用的基础
MongoDB 通过副本集实现高可用,副本集由一个主节点和多个从节点组成。
副本集架构
Primary(主节点)
├── Secondary(从节点1)
├── Secondary(从节点2)
└── Arbiter(仲裁节点)
选举机制
当主节点宕机时,副本集会自动选举新的主节点:
- 从节点检测到主节点不可达
- 触发选举,从节点投票
- 获得多数票的从节点成为新主节点
选举条件:
- 副本集必须有奇数个节点(保证多数派)
- 从节点必须同步到最新数据
- 从节点必须在选举超时时间内响应
读写策略
| 模式 | 读 | 写 | 一致性 |
|---|---|---|---|
| Primary Preferred | 优先读主节点 | 写主节点 | 强一致性 |
| Secondary Preferred | 优先读从节点 | 写主节点 | 最终一致性 |
| Nearest | 读最近节点 | 写主节点 | 最终一致性 |
分片集群:水平扩展的关键
当单节点数据量太大时,需要分片集群来水平扩展。
分片集群组件
- Shard:分片,存储数据的节点
- Config Server:配置服务器,存储集群元数据
- Mongos:路由服务器,负责查询路由
分片策略
| 策略 | 方式 | 优点 | 缺点 |
|---|---|---|---|
| 范围分片 | 按字段范围拆分 | 范围查询高效 | 数据分布不均匀 |
| Hash 分片 | 按字段哈希值拆分 | 数据均匀分布 | 范围查询低效 |
分片键选择
选择分片键需要考虑:
- 基数:区分度高的字段(如 userId)
- 写入分布:避免热点写入
- 查询模式:常见查询条件能命中分片
反模式:
- 使用单调递增字段(如时间戳)→ 热点写入
- 使用区分度低的字段(如性别)→ 数据分布不均
跨分片查询
- 路由查询:查询条件包含分片键,直接路由到对应分片
- 广播查询:查询条件不包含分片键,需要查询所有分片
聚合管道:数据处理的利器
MongoDB 的聚合管道是处理复杂数据的强大工具,可以完成数据转换、分组、排序等操作。
聚合管道阶段
db.orders.aggregate([
// 阶段1:过滤
{ $match: { status: "completed" } },
// 阶段2:分组
{
$group: {
_id: "$userId",
totalAmount: { $sum: "$amount" },
orderCount: { $count: {} }
}
},
// 阶段3:排序
{ $sort: { totalAmount: -1 } },
// 阶段4:限制
{ $limit: 10 }
])
常用聚合操作
| 操作 | 功能 | 示例 |
|---|---|---|
| $match | 过滤 | { $match: { status: "completed" } } |
| $group | 分组 | { $group: { _id: "$userId", total: { $sum: "$amount" } } } |
| $sort | 排序 | { $sort: { total: -1 } } |
| $limit | 限制 | { $limit: 10 } |
| $project | 投影 | { $project: { name: 1, total: 1 } } |
| $lookup | 关联 | { $lookup: { from: "users", localField: "userId", foreignField: "_id", as: "user" } } |
Map-Reduce
对于复杂的数据处理,可以使用 Map-Reduce:
db.orders.mapReduce(
function() { emit(this.userId, this.amount); },
function(key, values) { return Array.sum(values); },
{
query: { status: "completed" },
out: "user_totals"
}
)
索引设计:加速查询
MongoDB 的索引设计和关系型数据库类似,但也有一些差异。
索引类型
| 类型 | 说明 | 适用场景 |
|---|---|---|
| 单字段索引 | 单个字段 | 等值查询、排序 |
| 复合索引 | 多个字段 | 多条件查询 |
| 数组索引 | 数组中的每个元素 | 数组元素查询 |
| 文本索引 | 全文搜索 | 搜索场景 |
| 地理空间索引 | 地理位置 | 地图应用 |
| TTL 索引 | 自动过期 | 日志、缓存 |
索引使用原则
- 为常用查询条件创建索引
- 复合索引遵循最左前缀原则
- 避免创建过多索引(影响写入性能)
- 使用
explain()分析查询计划
项目判断清单
- 需要存储半结构化数据 → MongoDB
- 需要灵活扩展数据结构 → MongoDB
- 需要高并发写入 → MongoDB(配合分片)
- 需要事务支持 → MongoDB 4.0+(多文档事务)
- 需要复杂查询 → 考虑 PostgreSQL 或 Elasticsearch
- 数据量超过单节点限制 → 分片集群
- 需要自动过期数据 → TTL 索引