MongoDB入门

发布于:2025-09-03 ⋅ 阅读:(15) ⋅ 点赞:(0)

本文是对 小白debug 发布的视频 8年程序员看完都哭了? MongoDB是什么?架构是怎么样的? 做的学习笔记。更清晰生动的讲解请看原视频。

引言

如果老板突然甩给你一个任务:
“做一个游戏平台,能支撑十亿量级用户的数据写入和存储,用户信息包括 ID、装备、节日活动参与情况,还要支持扩展各种属性字段以及多维度查询。”
第一反应会想到 MySQL,但当需求逐渐变复杂,MySQL 似乎并不是最合适的选择。

1. MySQL 的局限性

假设我们使用 MySQL 存储游戏用户数据,每条记录包括 ID、装备、活动参与情况 等字段。如果需要支持各种节日活动,比如春节、端午节、劳动节等,该怎么设计表结构?

最直接的想法是给每种可能的活动都预留一个字段,比如 is_join_springis_join_valentineis_join_halloween。为了查询方便,可能还会为这些字段加上索引。

但这样做有几个致命问题:

  1. 空间浪费
    大多数玩家不会参加所有活动,很多预留字段用不上。

  2. 表结构僵化
    游戏迭代频繁,每次增加新活动,都要改表结构,改索引,甚至重建表。

  3. 多维度查询复杂
    比如“没参加过情人节活动的用户有哪些?”
    如果活动字段越来越多,查询条件就会越来越复杂,索引也会越来越臃肿。

  4. 高并发下的写入瓶颈
    当数据规模上升到 十亿级,MySQL 的单机写入能力会迅速遇到瓶颈。

所以,当面对“字段动态扩展 + 多维度查询 + 高并发写入”的场景时,MySQL 逐渐显得力不从心。

二、MongoDB 是什么

一句话概括:MongoDB 是一个“文档型数据库”,可以简单理解为数据结构更灵活的 MySQL。

在 MySQL 中,表是由 行(Row)列(Column) 组成,每一行的数据结构必须严格一致。在 MongoDB 中,抛弃了“列”的概念。

文档(Document):将多个列聚合到一个类似 JSON 的数据结构里,不用预留没用到的字段,这个"JSON"数据结构就叫做文档。

  • 每个文档都有一个主键ID字段(和MySQL表的主键ID是一个意思) 用来唯一定位数据。
  • 文档内部可以随意添加字段,文档之间的字段不需要完全一致,比如A文档有姓名字段,B文档可以没有。这样就不需要像MySQL那样提前定义表结构。
  • 类似MySQL数据表里的一行数据,就是一个文档。

集合(Collection):类似MySQL的多行数据可以组合到一起构成一张数据表,多个文档也可以组合到一起构成一个集合(Collection)。

{
  "_id": 1,
  "username": "player001",
  "equipments": ["sword", "shield"],
  "events": {
    "valentine": true
  }
}

{
  "_id": 2,
  "username": "player002",
  "equipments": ["bow"],
  "events": {}
}

如果说MySQL是一个用于读写数据表行列的服务进程 那MongoDB本质上就是个用于读写集合文档的服务进程。

三、MongoDB的实现原理

3.1 存储层

3.1.1 BSON:更高效的存储格式

虽然 MongoDB 表面上看起来是 JSON,但底层用的是 BSON(Binary JSON)。原因很简单:JSON 只支持字符串和数字等基础类型,如果要存储二进制数据,比如图片或音频,必须先进行 Base64 编码,效率太低。BSON 扩展了 JSON,支持二进制、日期等类型。

3.1.2 数据页与 WT 文件

有了BSON文档 下一步就是考虑将它们持久化到磁盘中。

MongoDB 使用 WiredTiger 存储引擎(简称 WT)。当写入 BSON 文档时,WiredTiger 会把文档存储在一个个 32KB 的数据页(Page) 中,并把这些数据页组织成一个个以 .wt 为后缀的磁盘文件。

为什么要拆成 32KB 的数据页?

  • 如果所有文档直接拼成一个大文件,每次查询都要全量读入,非常低效。
  • 拆分成小的数据页,只需加载需要的部分,大大降低磁盘 IO。
3.1.3 B+树索引与 Copy-on-Write

问题:如果我们已知某个文档主键ID,怎么快速找到包含这个文档的数据页呢?

为了快速查找某条文档,MongoDB 为 _id 字段(主键)默认创建 B+树索引
为每个数据页加入页号,由于每个文档本身就自带一个主键ID,可以按主键大小排序 将每个数据页里最小的主键序号 和所在页的页号提出来放到一个新生成的数据页中 并给数据页加入层级的概念,这样可以通过上层的数据页快速缩小查找范围,最终定位到要查的数据页。
在这里插入图片描述

为其他文档字段创建索引,比如用户名字段 这样能快速查找到 “名字为XX的用户有哪些”,这是辅助索引。

MySQL更新B+树的数据页时,为了防止并发写冲突,从根到叶子节点的搜索中会加入短暂内存锁,并对目标页的行记录加锁。MongoDB写数据时,几乎不对数据页加锁,直接复制个新的数据页出来写,也就是所谓的写时复制(Copy and Write),这样原来的数据页还能对外提供读操作,写操作则在新的数据页上进行,两者互不影响。之后再找机会将复制出来的页合并到原有的B+树结构中,这样并发读写性能更好。

3.1.4 Cache + WAL + Checkpoint

有了索引,查询数据是变高效了,但数据本质上还是在磁盘里,每次查询都要读磁盘。
MongoDB 为了优化读写性能,引入了三层机制:

  • Cache(缓存)
    经常访问的数据页会被加载到内存缓存中,查询优先从缓存读取。

  • WAL(Write-Ahead Logging)
    对所有写操作 都先将变更行为 记录到一个叫 Journal Buffer的缓冲区里 然后再更新到数据页中。Journal Buffer的数据,会定时刷新到磁盘的 Journal 文件中。如果服务进程崩溃了,那进程重启后就能通过Journal 文件找到历史操作记录重做数据,尽可能保证数据不丢失。

相比于将Cache的数据直接写入到磁盘,Journal 文件是顺序写入的,效率更高。

  • Checkpoint(检查点)
    脏页(Cache 中修改过但未落盘的数据页)会在合适的时机批量写入磁盘,减少频繁随机写带来的性能损耗。
    因为数据已经安全写入磁盘,所以在这个时间点之前的Journal 日志就可以删除了,不再需要保留这些历史操作记录。

Wired Tiger是什么?

  • 到这里我们通过Bson文档代替了MySQL的行列的概念,让存储格式更加灵活,将文档放入数据页和WT文档中,实现了高效的磁盘存储。
  • 通过变种B+树索引和写时复制机制实现了快速数据查找和高并发写入。
  • 为了进一步提升性能引入了Cache 把热点数据放到内存中,大幅减少了磁盘IO。
  • 用写前日志Journal 和Checkpoint 机制保证了数据持久化。

它们共同构成了Wired Tiger存储引擎 并对外提供一系列函数接口,比如Update用于更新数据,Search用于查询数据。MongoDB查询语句最终都会转换成 Wired Tiger提供的函数接口调用,比如Update会转换为Update方法,Find会转换为Search方法。

问题:读写MongoDB用的查询语句是怎么转成存储引擎的函数接口的呢?这就需要介绍Server层了。

3.2 Server 层

存储引擎 WiredTiger 提供了底层的读写接口,但我们平时写的是 MongoDB 的查询语句:

db.players.find({ "events.valentine": false })

MongoDB 的 Server 层就是连接客户端与存储引擎的中间层,负责以下工作:

  1. 连接管理:管理所有客户端的请求连接
  2. 查询解析器:解析查询语句语法,判断有无语法错误
  3. 查询优化器:根据查询条件选择最优索引,生成执行计划。
  4. 执行器:按照执行计划调用 WiredTiger 提供的接口。

Server层和存储引擎层共同构成了一个完整的文档数据库 它就是我们常说的MongoDB数据库。Server 层与存储引擎通过接口完全解耦,这也是 MongoDB 能支持多种存储引擎(如 MMapV1、WiredTiger)的原因。

四、MongoDB的高扩展与高可用

当数据规模达到 十亿级,单机 MongoDB 无论在 CPU、内存还是磁盘上都会遇到瓶颈。
MongoDB 提供了 分片(Sharding)+ 副本集(Replica Set) 机制来解决高扩展性和高可用性的问题。

4.1 分片(Sharding)实现高扩展

  • 按主键 ID 范围切分数据,比如:
    • 0 ~ 1000 万 → 分片 1
    • 1000 万 ~ 2000 万 → 分片 2
    • 以此类推。
  • 每个分片就是一个独立的 MongoDB 实例。
  • 分片分散部署在不同机器(每个机器叫一个Node)上,通过增加节点实现水平扩展。

问题:客户端应用怎么知道某条数据存储到哪个分片上?
答:通过Mongos 路由服务,客户端不会直接访问分片,而是通过 Mongos 路由:

  • Mongos 根据查询条件计算数据在哪个分片;
  • 把请求转发到对应分片,汇总结果并返回给客户端。
    Mongos的配置信息来自于**配置服务器 Config Server **,每个分片都连接Config Server 并主动上报自身信息,服务器存储了有哪些分片 以及每个分片负责哪些数据范围等信息

4.2 副本集(Replica Set)实现高可用

问题: 如果其中一个Node挂了,Node里所有分片都无法对外提供服务,怎么做到高可用?
每个分片内部通常是一个副本集,类似于 MySQL 主从复制

  • 主节点(Primary):负责写入请求。
  • 副本节点(Secondary):实时同步主节点数据,并支持只读请求。
  • 当主节点宕机时,副本节点通过选举自动升级为新主节点,保证系统不中断。

在这里插入图片描述

五、举例说明

下面以一次查询为例说明的完整的数据流。

  • 客户端发送查询请求到 Mongos
  • Mongos 根据缓存的分片信息确定目标分片。(必要时向Config Server刷新分片信息)
  • 将请求转发到对应的分片副本集;
  • 请求先到达分片的Server层,经过解析查询,优化器选择索引,生成执行计划。
    • 对于读操作
      • 调用 WiredTiger:
        • 先查 Cache,有则直接返回;
        • 如果 Cache 没有,则加载磁盘上的数据页到Cache;
      • 分片返回查询结果,Mongos 汇总并返回给客户端。
    • 对于写操作
      • 变更操作记录到Geno文档中,开启写时复制;
      • WideTiger结合CheckPoint机制 将修改后的数据页写回磁盘;
      • 写操作完成,分片主节点将数据实时同步给副本节点,当主节点和足够数量的副本节点都写入成功后会返回写入确认给Mongos;
      • Mongos 收到所有相关分片的写入确认后向客户端返回响应。

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到