mongo
1 · MongoDB#
MongoDB 是一个以 document 为基本单位的数据库。document 是一个自包含的业务对象,通常承载一起读取、一起更新的数据;系统以单文档原子性为核心,再提供索引、复制、分片和事务能力。
用文档模型围绕访问模式建模,用索引优化查询路径,用副本集保证高可用,用分片做水平扩展,并尽量把一致性边界收敛在单文档内。
- S:业务数据天然以
聚合体存在,例如订单和商品列表一起读写,访问模式变化快。 - C:关系型数据库通常先拆表,再靠 JOIN 拼回对象;schema 变更和对象关系映射成本较高。
- Q:MongoDB 的核心优势是什么,应该怎么用?
- A:核心是文档模型。MongoDB 把数据组织成文档,优先让一个业务对象尽量一次存进去、一次取出来。
1.1 · 1. 文档型:围绕访问模式建模#
MongoDB 的出发点不是如何把世界拆成表,而是:
这个数据,是否应该作为一个整体被读取和更新?
文档型数据库会把对象需要的内容装进一个自包含的数据单元里。一个 document 往往就是一个业务聚合体,例如订单和订单项、用户和部分画像信息、文章和标签列表。
所以 MongoDB 的建模起点通常不是 ER 图,而是 access pattern(访问模式):
- 这个对象最常被怎样查询
- 哪些字段会一起返回
- 哪些数据会一起更新
- 哪些边界需要原子性
对比着看会更直观:
- MySQL:先把世界拆成表,再靠关系拼回来。
- MongoDB:先把世界组织成聚合对象,尽量围绕对象本身读写。
灵活 schema 和水平扩展都很重要,但它们本质上是文档模型带来的结果,不是最核心的设计起点。
MongoDB 以 BSON 文档(类 JSON)为基本存储单元,不需要预定义固定列。嵌套子文档和数组可以直接表达一对多关系,因此很多原本依赖 JOIN 的读操作,可以改成一次文档读取完成。
这类模型特别适合:
- 结构会演进的业务
- 读写模式变化快的业务
- 对象天然是聚合体的业务
例如内容管理、用户画像、事件日志等场景,通常都比强关系建模更适合文档模型。
1.1.1 · 1.1 嵌入 vs 引用#
MongoDB 建模的关键决策,通常不是要不要分表,而是嵌入(embed)还是引用(reference)。
核心原则:
- 冗余主要是为了读性能
- 引用主要是为了一致性和独立演化
| 场景 | 选择 | 理由 |
|---|---|---|
| 读多写少,数据很少变 | 嵌入(冗余) | 一次查询拿到全部数据,几乎无一致性问题 |
| 数据频繁更新,需保障一致性 | 引用 | 只改一处,避免多处同步 |
| 对读延迟极度敏感 | 嵌入(冗余) | 消除额外查询开销 |
可以先用两个简单判断做初筛:
- 一起读、一起写、生命周期一致 -> 倾向 embed
- 独立变化、共享引用、多对多、会无限增长 -> 倾向 reference
1.2 · 2. 索引:更快的查询路径#
MongoDB 的查询虽然面向 document,但性能瓶颈往往不在能不能查到,而在是精准定位,还是扫了大量文档才查到。
索引的作用,就是把查询路径提前组织好,让系统能沿着字段路径快速定位数据,而不是每次都做全集合扫描。
1.2.1 · 2.1 索引在 MongoDB 里的意义#
- 查询本质上是在文档字段路径上定位数据,例如
status、address.city - 是否命中合适索引,决定了查询是快速定位还是大量扫描
- 排序、过滤、分页是否顺畅,也常常取决于索引设计是否贴合访问模式
MongoDB 的索引设计,通常不是给每个常用字段都建一个,而是围绕高频查询模式来设计。
1.2.2 · 2.2 设计索引时看什么#
- 先看最常见的过滤条件,再看排序条件
- 高频查询如果总是一起出现,应优先考虑复合索引
- 低选择性字段单独建索引,收益往往有限
- 建太多索引会拖慢写入,也会增加存储和维护成本
索引设计本质上是在读性能、写成本和存储开销之间做权衡。
1.2.3 · 2.3 用 explain() 验证是否真的生效#
MongoDB 里建了索引和查询真的高效使用了索引不是一回事。判断索引是否有效,最直接的方法是看查询计划。
- 用
explain()看查询是否命中了预期索引 - 关注是否出现大量扫描文档或扫描键的情况
- 如果查询模式已经稳定,而执行计划仍不理想,通常要回到索引顺序或文档结构重新看
1.3 · 3. 高可用与扩展#
这一层可以拆成两件事:副本集负责备,分片负责拆。
1.3.1 · 3.1 副本集(备):解决高可用#
副本集 = 1 个 primary + N 个 secondary,通过复制和选举提供高可用(类raft协议:pv1)
副本集的职责是备,不是拆。
- primary 处理写入
- secondary 异步复制 oplog
- primary 宕机后,副本集自动选举新的 primary
- 可以通过 read preference 把部分读请求分散到 secondary
生产环境里,副本集几乎是默认配置,因为它同时承担了:
- 高可用
- 故障切换
- 数据冗余
- 一定程度的读扩展
1.3.2 · 3.2 分片(拆):解决容量与写入扩展#
ref: https://www.mongodb.com/docs/manual/sharding ref: https://www.mongodb.com/zh-cn/docs/v6.0/sharding
当单机容量、单副本集写入能力或热点压力成为瓶颈时,MongoDB 通过分片做水平扩展。
分片的职责是拆,不是备。
1.3.2.1 · 3.2.1 架构三组件#
| 组件 | 职责 |
|---|---|
| shard(数据分片) | 存储数据子集,通常每个分片本身是一个副本集 |
| mongos(查询路由) | 接收客户端请求,根据分片键路由到目标分片 |
| config server | 存储分片元数据与 chunk 到分片的映射 |
1.3.2.2 · 3.2.2 分片键决定数据分布#
- 分片是在集合级别生效的,把一个集合拆到多个分片
- 分片键由文档中的一个或多个字段组成
- 系统会基于分片键把数据切成不重叠的范围,每个范围对应一个 chunk
分片能不能真正带来扩展效果,核心往往取决于 shard key 选得对不对。一个不好的分片键,可能会把写流量集中到少数分片,形成热点。
1.3.2.3 · 3.2.3 常见分片策略#
| 策略 | 特点 |
|---|---|
| 范围分片 | 连续值落在同一分片,利于范围查询;热点风险较高 |
| 散列分片 | 哈希打散,写入均匀;但范围查询常需访问更多分片 |
| 区域分片 | 按地理或业务规则把数据固定到指定分片 |
1.3.2.4 · 3.2.4 Chunk 与均衡#
- chunk 超过阈值后会自动切分
- balancer 会在分片之间迁移 chunk,保持负载均衡
- 每个数据库会有一个 primary shard,用来保存该库的未分片集合
1.3.3 · 3.3 副本集 vs 分片#
这两个机制解决的是不同问题:
- 副本集管
备:同一份数据复制多份,核心目标是高可用和容灾 - 分片管
拆:不同数据分散到不同机器,核心目标是容量和写扩展
生产环境里二者通常会组合使用:先把数据拆成多个 shard,再让每个 shard 自己成为一个副本集。
┌─────────────────────────────────────┐
│ mongos (路由) │
├───────────┬───────────┬─────────────┤
│ Shard A │ Shard B │ Shard C │ ← 分片:数据拆开
│ ┌───────┐ │ ┌───────┐ │ ┌───────┐ │
│ │Primary│ │ │Primary│ │ │Primary│ │
│ │Sec 1 │ │ │Sec 1 │ │ │Sec 1 │ │ ← 副本集:每份复制
│ │Sec 2 │ │ │Sec 2 │ │ │Sec 2 │ │
│ └───────┘ │ └───────┘ │ └───────┘ │
└───────────┴───────────┴─────────────┘
1.4 · 4. 一致性与事务边界#
设计得好的前提下,很多高频业务操作都应当落在一个 document 内完成,因为单文档天然就是事务边界。
MongoDB 最理想的建模状态,是把大多数核心业务写操作压缩在单文档内完成。这样天然具备原子性,不需要额外引入跨文档事务协调。
跨文档事务当然可以用,但更适合当补充机制,而不是默认建模手段。若一个业务动作频繁依赖多文档事务,通常说明文档边界还可以再调整。
1.4.1 · 4.1 运行与稳定性#
除了建模、复制、分片和索引,MongoDB 是否好用还取决于运行时是否可控。
- 数据安全通常依赖 journaling + 副本集一起保证
- 运维上要关注连接池配置、慢查询日志、oplog 大小
- 分片环境下还要持续观察热点、chunk 分布和 balancer 行为