跳转到主要内容

数据库/协同工具类型

MongoDB 深度指南

MongoDB 的作用定位、何时该用、核心机制(文档/collection/副本集/分片)、常见问题排查(_id、嵌套vs引用、内存)、难点解决方案(事务、分片 shard key、索引、聚合)与生产要点。

  • MongoDB
  • 数据库

这是 MongoDB 的深度参考文档。上手实操见 Nest 课程第十八章

一、基础入门(5 分钟上手)

这一节带你把 MongoDB 本身跑起来、用 mongosh 存几条「类 JSON 文档」;Nest 课程那篇讲的是在应用里用 mongoose 连——两篇不重复。

1. 用 Docker 起一个 MongoDB

docker run -d --name mongo -p 27017:27017 mongo:7
  • 应用连接地址:mongodb://localhost:27017

2. 连上它

docker exec -it mongo mongosh

v6 起官方 shell 叫 mongosh(旧的 mongo 命令已移除)。看到 > 提示符就连上了。

3. 最基础的几条命令

// 切到(不存在会自动建)demo 库
> use demo
switched to db demo

// 插一条
> db.users.insertOne({ name: '张三', age: 20, tags: ['vip'] })
{ acknowledged: true, insertedId: ObjectId('64a1f2...') }

// 插多条
> db.users.insertMany([{ name: '李四', age: 25 }, { name: '王五' }])
{ acknowledged: true, insertedCount: 2 }

// 查全部
> db.users.find()
[
  { _id: ObjectId('64a1f2...'), name: '张三', age: 20, tags: [ 'vip' ] },
  { _id: ObjectId('64a1f3...'), name: '李四', age: 25 },
  { _id: ObjectId('64a1f3...'), name: '王五' }
]

// 条件查:age > 22
> db.users.find({ age: { $gt: 22 } })
[ { _id: ObjectId('64a1f3...'), name: '李四', age: 25 } ]

// 改
> db.users.updateOne({ name: '张三' }, { $set: { age: 21 } })
{ acknowledged: true, matchedCount: 1, modifiedCount: 1 }

// 删
> db.users.deleteOne({ name: '王五' })
{ acknowledged: true, deletedCount: 1 }

和 MySQL 最大的不同:文档结构随便加字段(王五没 age 也照存),不用 ALTER TABLE

4. 数据类型

文档的字段类型(BSON),常用的:

类型说明示例
String字符串,最常用"张三"
Number数字(Int32 / Int64 / Double / Decimal128)209.9
Boolean布尔true
Date日期new Date() / ISODate()
Nullnull
Array数组["vip","new"]
Object嵌套对象{ city: "北京" }
ObjectId_id 默认类型ObjectId(...)
Binary二进制(少用,大文件存对象存储)

5. 常用语句与操作符

查询db.集合.find(条件, 投影),条件用 $ 操作符(掘金《MongoDB 常用增删改查语句》):

db.user.find(); // 全部(= SELECT *)
db.user.find({ age: 22 }); // age = 22
db.user.find({ age: { $gt: 22 } }); // age > 22($gte >=、$lt <、$lte <=)
db.user.find({ age: { $gte: 23, $lte: 26 } }); // 范围
db.user.find({ age: { $in: [20, 25] } }); // 在集合里
db.user.find({ name: // }); // 模糊(含"张",^ 开头 / $ 结尾)
db.user.find({ $or: [{ age: 20 }, { age: 25 }] }); // 或
db.user.find({ age: { $gt: 20 } }, { name: 1, age: 1 }); // 第二参=投影(只取 name、age)

db.user.findOne({ name: "张三" }); // 取一条
db.user.distinct("name"); // 去重某列
db.user.find({ age: { $gt: 20 } }).count(); // 计数
db.user.find().sort({ age: -1 }); // 1 升序 / -1 降序
db.user.find().limit(10).skip(20); // 分页:skip = (页-1) * limit

更新db.集合.update(条件, 操作, 选项)

db.user.update({ name: "张三" }, { $set: { age: 21 } }); // 改字段
db.user.update({ name: "张三" }, { $inc: { age: 1 } }); // 自增
db.user.update({ name: "张三" }, { $push: { tags: "vip" } }); // 数组加元素
db.user.update({ sex: "男" }, { $set: { age: 30 } }, { multi: true }); // multi: 改所有匹配

⚠️ update 不写 $set 会整条文档被替换update({name:"张三"}, {age:16}) 会把文档变成只有 {age:16}!永远带 $set

删除

db.user.deleteOne({ name: "王五" }); // 删一条
db.user.deleteMany({ age: { $gt: 30 } }); // 删所有匹配

6. 基本使用规则

  • 嵌套 vs 引用:会一起读/改就嵌套(文档套对象),会独立变化就引用(存对方 _id)。
  • _id 是 ObjectId 字符串,别用自增数字(分布式下难生成、慢)。
  • 单文档上限 16MB,大文件/富文本别塞文档,存对象存储,文档里只存 URL。
  • update 永远带 $set,否则整条被替换(上面那个坑)。
  • find 加条件 + 投影,别无脑 find() 拉全字段。
  • 高频查询字段建索引,但别乱建(写时要维护索引)。

7. 核心词汇速记

术语一句话
database库,demo
collection集合,对应 MySQL 的表,users
document文档,一条类 JSON 记录
_id文档主键,默认 ObjectId 字符串
BSONMongoDB 存储的二进制 JSON
嵌套文档里可以套对象/数组,不用拆表 JOIN

二、作用与定位

MongoDB 是文档型数据库,存的是 BSON(类 JSON)文档。核心价值:

  • schema-less:同一个 collection 的文档结构可以不同,字段随时加,不用像 MySQL 那样 ALTER TABLE
  • 嵌套存储:一个文档可以层层嵌套对象/数组,不用拆表 JOIN。
  • 水平扩展:原生支持分片(sharding),适合海量数据。

一句话:数据结构多变、嵌套深、读写量大、不强依赖跨表事务的场景,用 MongoDB(日志、内容、商品动态属性、用户画像)。

三、何时该用 / 何时用 MySQL

场景选择
结构固定、强事务、关系复杂(订单、账户)MySQL
结构多变、嵌套深(日志、动态字段、内容)MongoDB
海量写入、要水平扩展MongoDB(分片)或 ClickHouse
需要复杂 JOIN 和多表事务MySQL(MongoDB 的 $lookup 弱)

很多系统两者并存:主业务数据 MySQL,日志/搜索辅助 MongoDB

四、核心机制速览

  • database → collection → document(对应 MySQL 的 库 → 表 → 行,但文档任意结构)。
  • _id:每个文档的主键,默认 ObjectId(12 字节,含时间戳),字符串。不是自增数字。
  • 索引:和 MySQL 类似,B 树,建索引加速查询。_id 自动建索引。
  • 副本集(Replica Set):一主多从,自动故障转移,高可用。
  • 分片(Shard):按 shard key 把数据分散到多台机器,水平扩展。难点在选 shard key。
  • 存储引擎 WiredTiger:默认,支持文档级锁、压缩。

副本集一主多从,Primary 宕机后的完整故障转移时序:

sequenceDiagram autonumber participant C as 客户端 participant P as Primary participant S as Secondary participant A as Arbiter C->>P: 写入 Note over P: 宕机 S->>A: 心跳超时 发起选举 A->>S: 投票 Note over S: 当选新 Primary C->>S: 写入切换到新 Primary

一张图看清分片集群的组件与请求流向:

flowchart TB C["客户端"] M["Mongos 路由"] CS["Config Server 元数据"] S1["Shard1 副本集"] S2["Shard2 副本集"] S3["Shard3 副本集"] C -->|"1. 请求"| M M -->|"2. 查询路由"| CS CS -->|"3. 返回 chunk 位置"| M M -->|"4. 分发到分片"| S1 M --> S2 M --> S3

五、常见问题与排查

1. _id 用法

_id 是字符串 ObjectId,不是数字。查询/参数类型写 string不要用自增 id(分布式下难生成、慢);如果业务要自增序号,用单独的计数器 collection。

2. 嵌套还是引用

  • 嵌套用户.地址.城市,一次查出,适合「一起读、一起改、一对一或一对很少」。
  • 引用:存对方 _id,查时用 $lookup(弱 JOIN),适合「一对多且多端会独立改、数据量大」。

原则:会一起用就嵌套,会独立变就引用。别什么都引用(退回到关系型的 JOIN 痛点)。

3. 文档太大

单文档上限 16MB。日志/富文本/附件别塞文档里,存引用(文件存对象存储)。文档太大会拖慢读写和迁移。

4. 查询慢

explain() 看是否走了索引(stageIXSCAN 走了,COLLSCAN 全表扫)。给高频查询字段建索引,但别乱建(写时要维护索引)。

5. 连接数

和 MySQL 一样用连接池。mongoose 默认有池。高并发下调 maxPoolSize

六、难点与解决方案

1. 事务(4.0+)

MongoDB 4.0 起支持多文档事务(副本集),4.2 起支持分片事务。但事务性能不如 MySQL,能用嵌套操作替代就别用事务。mongoose 里:

const session = await Model.startSession();
await session.withTransaction(async () => {
  await A.updateOne({...}, { session });
  await B.create([{...}], { session });
});

2. 分片与 shard key

分片要选 shard key(数据按它分散)。这是最难的决策

  • 选不好会导致数据倾斜(某个分片特别大)和热点(写入集中到一个分片)。
  • 选值基数大、分布均匀、查询常用的字段(如 userId 哈希)。
  • 避免单调递增的 key(如时间戳,新数据永远落最后一个分片)。
  • shard key 选了基本改不了(5.0 起有限支持改),上线前要想清楚。

小项目用副本集就够了,不要一上来就分片。

3. 索引与聚合性能

  • 覆盖查询的索引(查询字段 + 返回字段都在索引里)免回表。
  • 聚合管道$match$group$sort)把 $match 放最前面先过滤、配合索引,否则在大 collection 上极慢。
  • 大数据量分析别用 MongoDB 的聚合硬扛,导到 ClickHouse / Elasticsearch。

4. 一致性

默认读偏好 primary(读主,强一致)。读副本可设 secondaryPreferred 分担压力,但有复制延迟(最终一致)。写关注 w:1(主写即返回)vs w:majority(多数副本确认,更安全更慢)——重要数据用 majority

七、生产环境要点

  • 用副本集,别单点(数据安全 + 高可用)。
  • 建索引,但监控索引数量(写性能)。
  • 控制文档大小(< 1MB 为宜,硬上限 16MB),大字段外置。
  • 监控:连接数、慢查询、磁盘、副本集延迟、oplog 窗口。
  • 备份mongodump 逻辑备份 + oplog 增量,或文件级快照。

速查:常见问题对照

问题原因解决
查询慢没走索引explain()、建索引
文档写不进去超 16MB大字段外置到对象存储
嵌套查询难取嵌套太深该引用的改引用
分片数据不均shard key 选错选基数大分布均的字段
弱事务不够多文档要原子用 4.0+ 事务 / 改嵌套单文档