一、基础概念
MongoDB 的特点是什么?
MongoDB是一种NoSQL数据库,具有以下特点:
-
文档存储模型
- MongoDB 使用 BSON(Binary JSON) 格式存储数据,数据以
文档
的形式组织,类似于JSON对象。 - 文档可以包含嵌套结构(如数组和对象),非常适合存储复杂、非结构化的数据。
- MongoDB 使用 BSON(Binary JSON) 格式存储数据,数据以
-
高性能
- MongoDB支持索引,能够快速查询数据。
- 写入性能高,支持内存映射文件,能够高效处理大量数据。【内存映射文件是一种高效的文件访问技术,通过将文件直接映射到内存中,减少了传统文件 I/O 的开销。】
-
水平扩展能力
- MongoDB支持分片(Sharding),可以将数据分布到多个服务器上,实现水平扩展,适合处理大规模数据和高并发场景。
-
高可用性
- MongoDB通过 复制集 提供高可用性。复制集包含多个节点(主节点和从节点),当主节点故障时,系统会自动选举新的主节点,确保服务不中断。
-
丰富的查询功能
- 支持丰富的查询操作,包括范围查询、正则表达式查询、地理空间查询等。
- 提供 聚合管道,支持复杂的数据分析和处理。
-
事务支持
- 从 MongoDB 4.0 开始,支持多文档事务,能够在分布式环境中保证数据的一致性。
MongoDB 和关系型数据库术语对比
概念 | MongoDB | 关系型数据库 |
---|---|---|
数据库层级 | 数据库(Database) | 数据库(Database) |
数据集合 | 集合(Collection) | 表(Table) |
数据单元 | 文档(Document) | 行(Row) |
数据格式 | BSON(Binary JSON) | 行数据(Row Data) |
数据结构 | 字段(Field) | 列(Column) |
嵌套结构 | 嵌套文档(Embedded Document) | 关联表(Related Table) |
数组支持 | 数组(Array) | 多值列(Multi-value Column) |
唯一标识 | _id 字段 | 主键(Primary Key) |
查询语言 | MongoDB 查询语言 | SQL(Structured Query Language) |
聚合操作 | 聚合管道(Aggregation Pipeline) | SQL聚合函数(如GROUP BY) |
索引 | 索引(Index) | 索引(Index) |
复合索引 | 复合索引(Compound Index) | 复合索引(Composite Index) |
事务 | 多文档事务(Multi-Document Transactions) | 事务(Transactions) |
数据一致性 | 最终一致性(Eventual Consistency) | 强一致性(Strong Consistency) |
水平扩展 | 分片(Sharding) | 分区(Partitioning) |
高可用性 | 复制集(Replica Set) | 主从复制(Master-Slave Replication) |
BSON 格式的优势有哪些?
1. 二进制编码,高效存储和传输
- BSON 是二进制编码的 JSON,比文本格式的 JSON 更紧凑,减少了存储空间和网络传输的开销。
- 二进制格式解析速度更快,适合高性能场景。
2. 支持丰富的数据类型
- BSON 支持比 JSON 更多的数据类型,如 日期(Date)、二进制数据(BinData)、ObjectId、正则表达式(Regex)等。
3. 支持嵌套和复杂结构
- BSON可以表示嵌套的文档和数组,适合存储复杂的数据结构。
- 例如,一个文档中可以包含另一个文档或数组,而无需额外的关联表。
4. 高效查询
- BSON格式在存储时会记录字段的长度和类型信息,使得查询时可以直接定位数据,无需解析整个文档。
5. 支持索引
- BSON 格式允许 MongoDB 在文档的特定字段上创建索引,从而加速查询。
- 例如,可以在嵌套字段或数组字段上创建索引。
MongoDB 支持哪些数据类型?
1. 基本数据类型
- 字符串(String)
- 整数(Integer)
- 双精度浮点数(Double)
- 布尔值(Boolean
- 日期(Date)**
- 用于存储日期和时间,例如:
"createdAt": ISODate("2023-10-01T12:00:00Z")
。
- 用于存储日期和时间,例如:
- 空值(Null)
2. 特殊数据类型
-
ObjectId
- 用于唯一标识文档的12字节ID,例如:
"_id": ObjectId("507f1f77bcf86cd799439011")
。
- 用于唯一标识文档的12字节ID,例如:
-
二进制数据(BinData)
- 用于存储二进制数据,例如图片或文件,例如:
"file": BinData(0, "SGVsbG8gd29ybGQ=")
。
- 用于存储二进制数据,例如图片或文件,例如:
-
正则表达式(Regex)
- 用于存储正则表达式,例如:
"pattern": /^[A-Za-z]+$/
。
- 用于存储正则表达式,例如:
-
JavaScript代码(JavaScript)
- 用于存储JavaScript代码,例如:
"script": "function() { return this.age > 18; }"
。
- 用于存储JavaScript代码,例如:
-
时间戳(Timestamp)
- 用于存储时间戳,通常用于内部操作,例如:
"ts": Timestamp(1696156800, 1)
。
- 用于存储时间戳,通常用于内部操作,例如:
3. 复杂数据类型
- 数组(Array)**
- 用于存储一组值,例如:
"hobbies": ["reading", "traveling", "coding"]
。
- 用于存储一组值,例如:
- 嵌套文档(Embedded Document)**
- 用于存储嵌套的文档,例如:
"address": {"city": "New York","zip": "10001" }
- 用于存储嵌套的文档,例如:
- 地理空间数据(Geospatial Data)
二、数据操作
如何创建数据库和集合?
创建数据库:
- 使用
use
命令切换到指定数据库,如果数据库不存在则会自动创建。use myDatabase
创建集合:
- 使用
db.createCollection()
方法显式创建集合。db.createCollection("myCollection")
- 如果集合不存在,插入文档时会自动创建集合。
使用 insertOne()
和 insertMany()
插入文档?
{ name: “Alice”, age: 25 } 表示一个文档
insertOne()
:
- 插入单个文档。
db.myCollection.insertOne({ name: "Alice", age: 25 })
- 注意事项:
- 如果文档未指定
_id
字段,MongoDB 会自动生成一个唯一的ObjectId
。 - 如果文档已存在
_id
字段,且_id
已存在,则会抛出重复键错误。
- 如果文档未指定
insertMany()
:
- 插入多个文档。
db.myCollection.insertMany([{ name: "Bob", age: 30 },{ name: "Charlie", age: 35 } ])
- 注意事项:
- 如果插入的文档中有重复的
_id
,整个操作会失败(默认行为)。 - 可以通过
ordered: false
选项忽略重复键错误,继续插入其他文档。db.myCollection.insertMany([{ _id: 1, name: "Bob" },{ _id: 1, name: "Charlie" } ], { ordered: false })
- 如果插入的文档中有重复的
如何编写多条件查询语句(如年龄>25且性别为男)?
- 使用
$and
操作符或直接在查询对象中指定多个条件。db.myCollection.find({age: { $gt: 25 },gender: "male" })
如何使用投影操作符返回部分字段?
- 在
find()
的第二个参数中指定需要返回的字段(1
表示返回,0
表示不返回)。db.myCollection.find({ age: { $gt: 25 } },{ name: 1, age: 1, _id: 0 } )
如何对查询结果进行排序?
- 使用
sort()
方法,1
表示升序,-1
表示降序。db.myCollection.find().sort({ age: 1 })
如何使用聚合管道进行复杂数据处理?
- 使用
aggregate()
方法,结合多个阶段(如$match
、$group
、$sort
等)处理数据。
db.users.aggregate([// 1. 过滤年龄大于 25 的用户{ $match: { age: { $gt: 25 } } },// 2. 只保留 name 和 gender 字段{ $project: { name: 1, gender: 1, _id: 0 } },// 3. 按 gender 分组,计算每组人数{ $group: { _id: "$gender", total: { $sum: 1 } } },// 4. 按 total 字段降序排序{ $sort: { total: -1 } },// 5. 只返回前 5 条结果{ $limit: 5 }
])
如何更新满足条件的多个文档?
- 使用
updateMany()
方法。db.myCollection.updateMany({ age: { $gt: 25 } },{ $set: { status: "active" } } )
如何删除满足条件的文档?
- 使用
deleteMany()
方法。db.myCollection.deleteMany({ age: { $lt: 18 } })
如何实现文档的部分更新?
- 使用
$set
操作符更新指定字段。db.myCollection.updateOne({ name: "Alice" },{ $set: { age: 26 } } )
三、索引
索引底层原理?
索引数据通过 B 树来存储,所有节点都有 Data 域,只要找到指定索引就可以进行访问,
单次查询从结构上来看要快于MySql(B+ 树)。
B 树结构:
B 树的特点:
- 多路 非二叉树
- 每个节点 既保存数据 又保存索引
- 搜索时 相当于二分查找
B 树的分层结构
- 根节点(Root Node):树的顶层节点。
- 内部节点(Internal Node):存储索引键和数据指针,用于导航到子节点。
- 叶子节点(Leaf Node):存储索引键和数据指针,直接指向实际文档(通常是
_id
值或磁盘位置)。
B+ 树结构:
如何为字段创建索引?
创建单字段索引
为单个字段创建索引,可以是升序(1
)或降序(-1
)。
语法:
db.collection.createIndex({ field: 1 })
示例:
// 为 users 集合的 age 字段创建升序索引
db.users.createIndex({ age: 1 })// 为 users 集合的 name 字段创建降序索引
db.users.createIndex({ name: -1 })
创建复合索引
为多个字段创建复合索引,可以指定每个字段的排序方式。
语法:
db.collection.createIndex({ field1: 1, field2: -1 })
示例:
// 为 users 集合的 age 和 gender 字段创建复合索引
db.users.createIndex({ age: 1, gender: -1 })
创建唯一索引
确保字段的值唯一,可以创建唯一索引。
语法:
db.collection.createIndex({ field: 1 }, { unique: true })
示例:
// 为 users 集合的 email 字段创建唯一索引
db.users.createIndex({ email: 1 }, { unique: true })
创建文本索引
支持全文搜索,可以创建文本索引。
语法:
db.collection.createIndex({ field: "text" })
示例:
// 为 articles 集合的 content 字段创建文本索引
db.articles.createIndex({ content: "text" })
创建 TTL 索引
支持自动删除过期的文档,可以创建 TTL 索引。
语法:
db.collection.createIndex({ field: 1 }, { expireAfterSeconds: 3600 })
示例:
// 为 logs 集合的 createdAt 字段创建 TTL 索引,文档在 1 小时后自动删除
db.logs.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })
查看索引
可以使用 getIndexes()
方法查看集合中的所有索引。
语法:
db.collection.getIndexes()
删除索引
可以使用 dropIndex()
方法删除指定的索引。
语法:
db.collection.dropIndex("index_name")
升序索引和降序索引的区别是什么?
- 升序索引适合从小到大排序的查询,降序索引适合从大到小排序的查询。
- 在复合索引中,升序和降序的组合可以优化复杂的排序查询。
db.collection.createIndex({ age: 1, score: -1 })// 这个索引会先按 age 升序排序,再按 score 降序排序。
// 如果查询的排序方式与索引一致(如 sort({ age: 1, score: -1 })),MongoDB 可以直接利用索引。
如何查看和删除集合中的索引?
查看集合中的索引
使用 db.collection.getIndexes()
命令可以查看集合中的所有索引。
语法:
db.collection.getIndexes()
示例:
假设有一个 users
集合,查看其索引:
db.users.getIndexes()
输出示例:
[{"v": 2,"key": { "_id": 1 },"name": "_id_","ns": "test.users"},{"v": 2,"key": { "age": 1 },"name": "age_1","ns": "test.users"}
]
key
:索引的字段和排序方式(1
表示升序,-1
表示降序)。name
:索引的名称。ns
:索引所属的命名空间(数据库名.集合名
)。
删除集合中的索引
可以使用 db.collection.dropIndex()
或 db.collection.dropIndexes()
命令删除索引。
删除单个索引
使用 db.collection.dropIndex()
命令,可以指定索引的名称或键来删除索引。
语法:
db.collection.dropIndex(indexNameOrKey)
示例:
- 通过索引名称删除:
db.users.dropIndex("age_1")
- 通过索引键删除:
db.users.dropIndex({ age: 1 })
输出:
如果删除成功,会返回:
{ "nIndexesWas": 2, "ok": 1 }
删除所有索引(除了 _id
索引)
使用 db.collection.dropIndexes()
命令可以删除集合中的所有索引(_id
索引不会被删除)。
语法:
db.collection.dropIndexes()
示例:
db.users.dropIndexes()
输出:
如果删除成功,会返回:
{ "nIndexesWas": 2, "msg": "non-_id indexes dropped for collection", "ok": 1 }
注意事项
_id
索引:_id
索引是 MongoDB 自动为每个集合创建的,不能删除。- 索引名称:如果没有显式指定索引名称,MongoDB 会默认生成一个名称(如
age_1
表示age
字段的升序索引)。 - 删除索引的影响:删除索引后,依赖该索引的查询性能可能会下降,需谨慎操作。
复合索引的创建原则是什么?
在 MongoDB 中,复合索引(Compound Index)是指基于多个字段创建的索引。
1. 字段顺序原则
复合索引的字段顺序非常重要,因为它决定了索引的存储和查询效率。
ESR 原则(Equality, Sort, Range)
- Equality(等值查询):将用于等值查询的字段放在索引的最前面。
- Sort(排序):将用于排序的字段放在等值查询字段之后。
- Range(范围查询):将用于范围查询的字段放在最后。
示例
假设有一个查询:
db.users.find({ age: 25, score: { $gte: 80 } }).sort({ name: 1 })
根据 ESR 原则,复合索引应创建为:
db.users.createIndex({ age: 1, name: 1, score: 1 })
age
:等值查询字段,放在最前面。name
:排序字段,放在中间。score
:范围查询字段,放在最后。
2. 覆盖查询原则
如果查询的所有字段都在复合索引中,MongoDB 可以直接从索引中返回结果,而无需访问实际文档。这种查询称为覆盖查询(Covered Query)。
示例
假设有一个查询:
db.users.find({ age: 25 }, { name: 1, _id: 0 })// { name: 1, _id: 0 }
// 投影用于控制查询结果中返回哪些字段。1 返回;0 不返回
创建以下索引可以支持覆盖查询:
db.users.createIndex({ age: 1, name: 1 })
3. 排序原则
如果查询中包含排序操作,复合索引的字段顺序应与排序字段的顺序一致。
示例
假设有一个查询:
db.users.find({ age: 25 }).sort({ name: 1 })
创建以下索引可以支持排序:
db.users.createIndex({ age: 1, name: 1 })
如果排序方向不一致(如 sort({ name: -1 })
),索引应创建为:
db.users.createIndex({ age: 1, name: -1 })
4. 前缀原则
复合索引支持前缀查询(Prefix Query),即查询中只使用索引的前几个字段。
示例
假设有一个索引:
db.users.createIndex({ age: 1, name: 1, score: 1 })
以下查询可以利用该索引:
db.users.find({ age: 25 })
(使用前缀age
)db.users.find({ age: 25, name: "Alice" })
(使用前缀age
和name
)
以下查询不能利用该索引:
db.users.find({ name: "Alice" })
(跳过了前缀age
)
在什么情况下不适合创建索引?
- 数据量非常小。
- 查询模式不固定。
- 字段值分布不均匀或非常长。
- 查询频率低。
- 复合索引字段过多。
四、复制集
MongoDB 有三种常见的部署架构
- 单机版:只有一个单节点,一般用来做开发和测试
- 复制集:绝大部分 MongoDB 实例上线的时候都使用复制集、高可用模式,1主2从(或更多从节点),至少是三个节点的架构
- 分片集群:节点数明显增多,一般有 9 个实例
绝大部分使用场景是复制集。
什么是复制集?
复制集 由一组MongoDB实例(进程)组成,包含一个 Primary (主)节点和多个 Secondary (从)节点。
MongoDB Driver(客户端)的所有数据都写入 Primary,Secondary 从 Primary 同步写入的数据,以保持复制集内所有成员存储相同的数据集,提供数据的高可用。
下图是一个典型的 MongoDB 复制集,包含一个 Primary 节点和两个 Secondary 节点。
主节点和从节点的职责分别是什么?
职责 | 主节点(Primary) | 从节点(Secondary) |
---|---|---|
写操作 | 处理所有写操作。 | 不处理写操作。 |
读操作 | 默认处理读操作。 | 可以处理读操作,分担主节点负载。 |
数据复制 | 将操作日志(Oplog)发送给从节点。 | 从主节点拉取操作日志,并应用数据。 |
选举参与 | 不参与选举。 | 参与选举,可能成为新的主节点。 |
数据备份 | 不直接充当数据备份。 | 可以充当数据备份。 |
特殊角色 | 无特殊角色。 | 可配置为隐藏节点、延迟节点或只读节点。 |
复制集的同步过程(数据一致性)?
Primary 与 Secondary 之间通过 oplog
来同步数据。
Primary 上的写操作完成后,会向特殊的 local.oplog.rs 集合
写入一条 oplog
,Secondary 不断的从Primary 获取新的 oplog 并应用。
如下 oplog
的格式,包含 ts
、h
、op
、ns
、o
等字段。
{"ts" : Timestamp(1446011584, 2),"h" : NumberLong("1687359108795812092"), "v" : 2, "op" : "i", "ns" : "test.nosql", "o" : { "_id" : ObjectId("563062c0b085733f34ab4129"), "name" : "mongodb", "score" : "100" } }
字段说明如下:
- ts:操作时间,当前timestamp + 计数器,计数器每秒都被重置。
- h:操作的全局唯一标识。
- v:oplog版本信息。
- op:操作类型,取值说明:
- i:插入操作。
- u:更新操作。
- d:删除操作。
- c:执行命令(如createDatabase,dropDatabase)。
- n:空操作,特殊用途。
- ns:操作针对的集合。
- o:操作内容。
Secondary 初次同步数据时,会先执行 init sync
,从 Primary 同步全量数据(T1时间完成)。
然后不断通过执行tailable cursor
从 Primary
的 local.oplog.rs
集合里查询最新的 oplog
(T2-T1时间段)并应用到自身。
MongoDB 副本集 架构
- 一个 MongoDB 副本集由多个节点组成,包括:
- 主节点(Primary):负责处理所有写操作和读操作。
- 从节点(Secondary):复制主节点的数据,提供读操作(可选)。
- 仲裁节点(Arbiter):不存储数据,仅参与选举。
- 当主节点失效时,副本集会通过选举机制选出一个新的主节点。
选举机制
MongoDB 的选举机制基于 Raft 协议,这是一种分布式一致性算法。
选举过程由从节点(Secondary)和仲裁节点(Arbiter)参与,确保在大多数节点同意的情况下选出新的主节点。
Raft 是一种分布式一致性算法,旨在通过选举和日志复制来保证分布式系统的一致性和高可用性。MongoDB 在 Raft 的基础上进行了一些调整和优化,以适应其特定的需求。
Raft 算法的核心思想
Raft 算法通过以下机制保证分布式系统的一致性:
- Leader 选举:选出一个主节点(Leader),负责管理日志复制和客户端请求。
- 日志复制:主节点将日志复制到所有从节点,确保数据一致性。
- 安全性:通过大多数投票机制和日志匹配原则,保证系统的强一致性。
触发选举的条件
以下情况会触发选举:
- 主节点不可用(如宕机或网络故障)。
- 主节点与其他节点的通信中断超过一定时间(默认为 10 秒)。
- 主节点主动退出(如人为执行
rs.stepDown()
命令)。
手动触发选举
如果需要手动触发选举(如维护主节点),可以使用以下命令:
rs.stepDown() # 让当前主节点主动退出
选举的参与者
- 从节点(Secondary):存储数据,可以参与选举并成为主节点。
- 仲裁节点(Arbiter):不存储数据,仅参与选举投票。
选举的过程
选举过程包括以下步骤:
(1) 检测主节点故障
- 从节点和仲裁节点会定期与主节点通信(通过心跳机制)。
- 如果主节点在指定时间内(默认为 10 秒)未响应,从节点会认为主节点不可用。
(2) 发起选举
- 从节点会发起选举请求,向其他节点发送投票请求。
- 每个从节点和仲裁节点都会参与投票。
(3) 投票规则
- 新主节点必须获得大多数节点的投票(例如,在 3 个节点的复制集中,至少需要 2 票)。
- 优先级高的节点更有可能被选为主节点(可以通过
priority
参数设置优先级)。
(4) 选出新主节点
- 获得大多数投票的从节点将成为新的主节点。
- 新的主节点开始处理写操作,并继续将操作日志(Oplog)发送给其他从节点。
选举优先级
MongoDB 允许为副本集中的节点设置优先级(Priority),优先级高的节点更有可能被选为主节点。
- 优先级为 0 的节点永远不会成为主节点。
- 优先级高的节点在选举中更有可能获得投票。
选举的配置参数
可以通过以下参数配置选举行为:
(1) priority
- 设置节点的优先级,优先级高的节点更有可能被选为主节点。
- 默认优先级为
1
,优先级为0
的节点不能成为主节点。
(2) votes
- 设置节点是否有投票权(默认所有节点都有投票权)。
- 无投票权的节点不参与选举。
(3) arbiterOnly
- 将节点配置为仲裁节点(不存储数据,仅参与投票)。
示例配置
cfg = rs.conf()
cfg.members[0].priority = 2 # 设置第一个节点的优先级为 2
cfg.members[1].priority = 1 # 设置第二个节点的优先级为 1
cfg.members[2].arbiterOnly = true # 将第三个节点配置为仲裁节点
rs.reconfig(cfg)
选举超时
- 选举过程必须在
settings.electionTimeoutMillis
(默认 10 秒)内完成,否则选举失败,重新发起选举。
选举中的脑裂问题
- 在网络分区的情况下,可能会出现多个主节点(脑裂问题)。
- MongoDB 通过大多数投票机制来避免脑裂,只有获得大多数节点认可的节点才能成为主节点。
选举的影响
- 性能影响:选举期间,副本集无法处理写操作,直到选出新的主节点。
- 数据一致性:选举期间,可能会丢失部分未提交的写操作。
优化选举
- 增加节点数量:确保副本集中有足够多的节点,避免因节点故障导致选举失败。
- 合理配置优先级:将性能较好的节点设置为高优先级。
- 优化网络:减少网络延迟和分区,确保节点之间的通信稳定。
如何向复制集中添加新节点?
1. 准备工作
- 新节点:确保新节点的 MongoDB 实例已安装并启动。
- 网络:确保新节点与复制集中的其他节点可以互相通信。
- 配置文件:在新节点的
mongod.conf
中配置复制集名称(replSetName
),与现有复制集一致。
示例配置
replication:replSetName: "rs0"
2. 连接到主节点
使用 mongo
shell 连接到复制集的 主节点(Primary)。
mongo --host primary:27017
3. 添加新节点
使用 rs.add()
命令将新节点添加到复制集中。
语法
rs.add("新节点地址:端口")
4. 检查复制集状态
使用 rs.status()
命令检查复制集的状态,确保新节点已成功加入并正常运行。
rs.status()
在输出中,新节点应显示为 SECONDARY
状态。
5. 验证数据同步
在新节点上验证数据是否已从主节点同步。
步骤 1:连接到新节点
mongo --host new-node:27017
步骤 2:设置从节点可读
默认情况下,从节点不能处理读操作,需要设置 slaveOk
:
rs.slaveOk()
步骤 3:查询数据
db.test.find()
6. 配置节点属性(可选)
如果需要为新节点配置特殊属性(如优先级、隐藏节点、延迟节点等),可以使用 rs.reconfig()
命令。
示例:设置优先级
cfg = rs.conf()
cfg.members[3].priority = 1 // 假设新节点是第 4 个成员
rs.reconfig(cfg)
7. 添加仲裁节点(可选)
如果希望添加仲裁节点(Arbiter),可以使用 rs.addArb()
命令。
语法
rs.addArb("仲裁节点地址:端口")
五、分片
复制集与分片的区别?
复制时让多台服务器都拥有同样的数据副本,每一台服务器都是其他服务器的镜像。
而每一个分片都和其他分片拥有不同的数据子集
分片原理
当MongoDB存储海量的数据时,一台机器可能不足以存储数据,也可能不足以提供可接受的读写吞吐量。
这时,可通过在多台机器上分割数据
,使得数据库系统能存储和处理更多的数据。
即通过分片进行水平扩展
。
分片架构
主要组件
1. Shard(分片)
用于存储实际的数据块
。每个 Shard 是一个独立的 MongoDB 实例或复制集,负责存储数据的一部分。
特点
- 数据分片:数据根据分片键(Shard Key)被划分到不同的 Shard 中。
- 水平扩展:通过增加 Shard,可以扩展集群的存储容量和吞吐量。
示例
假设有一个分片集群,包含 3 个 Shard(分片):
shard1
:存储user_id
范围为[0, 1000)
的数据。shard2
:存储user_id
范围为[1000, 2000)
的数据。shard3
:存储user_id
范围为[2000, 3000)
的数据。
2. Config Server(配置服务器)
存储分片集群的 元数据
,包括集群的配置信息和分片数据的位置信息。
Query Routers(查询路由器)
Query Routers(也称为 mongos
)是 MongoDB 分片集群的查询路由组件,负责将客户端请求路由到正确的 Shard。
客户端无需知道数据的具体分布,只需连接到 mongos
即可。
分片的查询流程
- 客户端请求:客户端连接到
mongos
实例,发送查询或写操作请求。 - 查询路由:
mongos
根据分片键和 Config Server 中的元数据信息,将请求路由到相应的 Shard。 - 数据操作:Shard 执行查询或写操作,并将结果返回给
mongos
。 - 结果返回:
mongos
将结果返回给客户端。
shard key (分片键)
在集合中分发文档,MongoDB 使用 shard key 对进行进行分片。
shard key 既可以是集合的每个文档的索引字段也可以是集合中每个文档都有的组合索引字段。
MongoDB 将 shard keys 值按照块(chunks)划分,并且均匀的将这些chunks分配到各个分片上。
MongoDB使用基于范围划分或基于散列划分来划分chunks的。
注意:确定shard key时需要谨慎,以确保集群性能和效率。分片后不能更改shard key,也不能取消分片。
Shard Key 的特点
- 唯一性:每个文档的 Shard Key 值必须是唯一的,或者至少是高度唯一的。
- 不可变性:一旦文档插入集合,其 Shard Key 的值不能被修改。
- 分布性:Shard Key 的值应尽可能均匀分布,以确保数据在各个 Shard 上均衡分布。
Shard Key 的类型
(1) 单字段 Shard Key
- 使用单个字段作为 Shard Key。
- 示例:
{ user_id: 1 }
(2) 复合 Shard Key
- 使用多个字段的组合作为 Shard Key。
- 示例:
{ user_id: 1, timestamp: 1 }
Shard Key 的配置
在 MongoDB 中,可以通过以下步骤配置 Shard Key:
(1) 启用分片
sh.enableSharding("testDB")
(2) 选择 Shard Key
sh.shardCollection("testDB.users", { user_id: 1 })
Shard Key 的优化
(1) 避免写热点
- 如果 Shard Key 的值单调递增(如时间戳),可能导致写操作集中在某个 Shard 上。
- 解决方案:使用哈希 Shard Key 或将单调递增字段与其他字段组合。
(2) 避免跨分片查询
- 如果查询条件不包含 Shard Key,MongoDB 需要在所有 Shard 上执行查询,性能较差。
- 解决方案:确保查询条件包含 Shard Key。
分片策略
基于范围划分
MongoDB 通过 shard key
值将数据集划分到不同的范围就称为基于范围划分。
相邻的数据通常存储在同一个 Shard 上。
基于散列划分
MongoDB 计算每个字段的 hash 值,然后用这些 hash 值建立chunks。
相邻的数据可能存储在不同的 Shard 上。
基于散列值的数据分布有助于更均匀的数据分布,尤其是在shard key单调变化的数据集中。
但是,散列分布意味着对shard key的基于范围的查询不太可能以单个分片为目标,从而导致更多群集范围的广播操作。
基于范围和基于散列划分的对比
特性 | 基于范围的分片 | 基于散列的分片 |
---|---|---|
数据分布 | 相邻数据通常存储在同一个 Shard 上。 | 数据均匀分布,相邻数据可能存储在不同 Shard 上。 |
范围查询性能 | 性能较好,查询可以路由到特定 Shard。 | 性能较差,需要跨多个 Shard 查询。 |
精确查询性能 | 性能较好,查询可以路由到特定 Shard。 | 性能较好,查询可以路由到特定 Shard。 |
写分布 | 可能导致写热点。 | 写操作均匀分布,避免写热点。 |
适用场景 | 查询模式以范围查询为主。 | 查询模式以精确查询为主,或需要避免写热点。 |
MongoDB 默认使用 基于范围的分片。
在 MongoDB 分片集群中,分片键(Shard Key) 的选择对集群的性能、扩展性和数据分布至关重要。以下是选择合适分片键的原则和步骤:
分片键的选择原则
(1) 高基数(High Cardinality)
- 分片键的值应具有高基数(即大量唯一值),以确保数据能够均匀分布。
- 示例:
user_id
是一个高基数字段,而性别gender
是一个低基数字段。
(2) 低频率(Low Frequency)
- 分片键的值应具有低频率(即每个值出现的次数较少),以避免某些 Chunk 过大。
- 示例:
timestamp
是一个低频率字段,而status
可能是一个高频率字段。
(3) 查询模式(Query Patterns)
- 分片键应支持常见的查询模式,避免跨分片查询,以提高查询性能。
- 示例:如果查询模式主要基于
user_id
,选择user_id
作为分片键。
(4) 写分布(Write Distribution)
- 分片键应确保写操作能够均匀分布到各个 Shard 上,避免写热点。
- 示例:如果
timestamp
是单调递增的,直接使用它作为分片键可能导致写热点,可以使用哈希分片。
(5) 不可变性(Immutable)
- 分片键的值一旦插入文档后不能被修改,否则会导致数据迁移和性能问题。
分片键的类型
(1) 单字段分片键
- 使用单个字段作为分片键。
- 示例:
{ user_id: 1 }
(2) 复合分片键
- 使用多个字段的组合作为分片键。
- 示例:
{ user_id: 1, timestamp: 1 }
(3) 哈希分片键
- 使用哈希函数对字段值进行哈希计算,将数据均匀分布到不同的 Shard 上。
- 示例:
{ user_id: "hashed" }
分片键的配置
在 MongoDB 中,可以通过以下步骤配置分片键:
(1) 启用分片
sh.enableSharding("testDB")
(2) 选择分片键
sh.shardCollection("testDB.users", { user_id: 1 })
分片键的优化
(1) 避免写热点
- 如果分片键的值单调递增(如时间戳),可能导致写操作集中在某个 Shard 上。
- 解决方案:使用哈希分片或将单调递增字段与其他字段组合。
(2) 避免跨分片查询
- 如果查询条件不包含分片键,MongoDB 需要在所有 Shard 上执行查询,性能较差。
- 解决方案:确保查询条件包含分片键。
(3) 监控和调整 Chunk 大小
- 使用
sh.status()
监控 Chunk 分布,并根据需要手动拆分或迁移 Chunk。
分片键的监控
使用 sh.status()
监控 Chunk 分布,并根据需要手动拆分或迁移 Chunk。
六、性能优化
查询性能慢的可能原因有哪些?
1. 索引问题
原因
- 缺少索引:查询未使用索引,导致全表扫描。
- 索引不合理:索引未覆盖查询条件或排序字段。
- 索引过多:过多的索引会增加写操作的开销,影响性能。
解决方法
- 使用
explain()
分析查询计划,确保查询使用了索引。 - 创建合适的索引,覆盖查询条件和排序字段。
- 删除不必要的索引,减少写操作的开销。
2. 查询条件不合理
原因
- 范围查询过大:范围查询(如
{ age: { $gte: 0, $lte: 100 } }
)导致扫描大量文档。 - 正则表达式查询:正则表达式查询(如
{ name: /^A/ }
)性能较差。 - 跨分片查询:在分片集群中,查询条件不包含分片键,导致跨分片查询。
解决方法
- 优化查询条件,缩小范围查询的范围。
- 尽量避免使用正则表达式查询,或使用索引支持的正则表达式。
- 在分片集群中,确保查询条件包含分片键。
3. 数据分布不均匀
原因
- 分片键选择不合理:分片键导致数据分布不均匀,某些 Shard 负载过高。
- Chunk 分布不均匀:Chunk 分布不均匀,导致某些 Shard 负载过高。
解决方法
- 选择合适的分片键,确保数据均匀分布。
- 使用
sh.status()
监控 Chunk 分布,并根据需要手动拆分或迁移 Chunk。
4. 硬件资源不足
原因
- 内存不足:内存不足导致频繁的磁盘 I/O 操作。
- CPU 瓶颈:CPU 负载过高,无法及时处理查询请求。
- 磁盘性能差:磁盘 I/O 性能差,影响数据读取和写入速度。
解决方法
- 增加内存,确保 MongoDB 能够缓存更多的数据和索引。
- 升级 CPU,提高计算能力。
- 使用高性能磁盘(如 SSD),提高 I/O 性能。
5. 锁争用
原因
- 写锁争用:高并发的写操作导致锁争用,影响查询性能。
- 全局锁:某些操作(如创建索引)会占用全局锁,影响其他操作。
解决方法
- 优化写操作,减少锁争用。
- 在低峰期执行占用全局锁的操作。
6. 网络延迟
原因
- 网络带宽不足:网络带宽不足导致数据传输速度慢。
- 网络延迟高:网络延迟高导致查询响应时间增加。
解决方法
- 增加网络带宽,提高数据传输速度。
- 优化网络配置,减少网络延迟。
7. 查询计划缓存失效
原因
- 查询计划缓存失效:查询计划缓存失效导致 MongoDB 需要重新生成查询计划,影响性能。
解决方法
- 使用
explain()
分析查询计划,确保查询计划缓存有效。 - 优化查询条件,减少查询计划缓存失效的可能性。
explain
explain()
的基本用法
explain()
可以附加到查询操作(如 find()
、aggregate()
、update()
等)之后,返回查询的执行计划。
语法
db.collection.find(query).explain()
示例
db.users.find({ age: { $gte: 25 } }).explain()
explain()
的详细模式
explain()
支持三种模式,分别提供不同详细程度的执行计划信息:
(1) queryPlanner
模式
- 默认模式,提供查询计划的基本信息,如是否使用索引、扫描的文档数量等。
- 语法:
db.collection.find(query).explain("queryPlanner")
(2) executionStats
模式
- 提供查询执行的详细统计信息,如执行时间、扫描的文档数量、返回的文档数量等。
- 语法:
db.collection.find(query).explain("executionStats")
(3) allPlansExecution
模式
- 提供所有候选查询计划的执行统计信息,帮助分析 MongoDB 选择最优查询计划的过程。
- 语法:
db.collection.find(query).explain("allPlansExecution")
explain()
输出解析
以下是 explain()
输出的关键字段及其含义:
(1) queryPlanner
winningPlan
:MongoDB 选择的查询计划。stage
:查询阶段(如COLLSCAN
表示全表扫描,IXSCAN
表示索引扫描)。indexName
:使用的索引名称。direction
:索引扫描方向(如forward
或backward
)。
rejectedPlans
:被拒绝的候选查询计划。
(2) executionStats
executionTimeMillis
:查询执行时间(毫秒)。totalDocsExamined
:扫描的文档数量。totalKeysExamined
:扫描的索引键数量。nReturned
:返回的文档数量。executionStages
:查询执行的详细阶段信息。
(3) allPlansExecution
allPlansExecution
:所有候选查询计划的执行统计信息。
举例
{"queryPlanner": {"plannerVersion": 1,"namespace": "testDB.users","indexFilterSet": false,"parsedQuery": {"age": { "$gte": 25 }},"winningPlan": {"stage": "FETCH","inputStage": {"stage": "IXSCAN", // 使用了索引扫描"keyPattern": { "age": 1 },"indexName": "age_1", // 命中的索引名称"direction": "forward","indexBounds": {"age": ["[25, inf.0]"]}}},"rejectedPlans": []},"executionStats": {"executionSuccess": true,"nReturned": 10, // 查询返回了 10 条文档。"executionTimeMillis": 5, // 查询执行时间为 5 毫秒"totalKeysExamined": 10, // 扫描了 10 条文档"totalDocsExamined": 10, // 扫描了 10 个索引键。"executionStages": {"stage": "FETCH","nReturned": 10,"executionTimeMillisEstimate": 1,"inputStage": {"stage": "IXSCAN","nReturned": 10,"executionTimeMillisEstimate": 0,"keyPattern": { "age": 1 },"indexName": "age_1","direction": "forward","indexBounds": {"age": ["[25, inf.0]"]}}}},"serverInfo": {"host": "localhost","port": 27017,"version": "5.0.9","gitVersion": "abcdefg"},"ok": 1
}
使用 explain()
优化查询
通过 explain()
可以分析查询的性能瓶颈,并采取相应的优化措施:
(1) 检查是否使用索引
- 如果
stage
为COLLSCAN
,表示查询未使用索引,需要创建合适的索引。 - 示例:
db.users.find({ age: { $gte: 25 } }).explain("executionStats")
(2) 检查索引覆盖
- 如果
totalDocsExamined
大于nReturned
,表示索引未完全覆盖查询条件,需要优化索引。 - 示例:
db.users.find({ age: { $gte: 25 } }, { name: 1, _id: 0 }).explain("executionStats")
(3) 检查查询执行时间
- 如果
executionTimeMillis
较高,需要优化查询条件或索引。 - 示例:
db.users.find({ age: { $gte: 25 } }).explain("executionStats")
(4) 检查扫描的文档数量
- 如果
totalDocsExamined
较高,表示查询扫描了大量文档,需要优化查询条件或索引。 - 示例:
db.users.find({ age: { $gte: 25 } }).explain("executionStats")
七、高级应用
如何备份和恢复MongoDB数据?
1. 备份 MongoDB 数据
使用 mongodump
备份
mongodump
是 MongoDB 提供的备份工具,可以将数据导出为 BSON 文件。
语法
mongodump --uri <connectionString> --out <backupDirectory>
示例
备份整个数据库到 /backup
目录:
mongodump --uri "mongodb://localhost:27017" --out /backup
备份指定数据库(如 testDB
):
mongodump --uri "mongodb://localhost:27017/testDB" --out /backup
备份指定集合(如 testDB.users
):
mongodump --uri "mongodb://localhost:27017" --db testDB --collection users --out /backup
2. 恢复 MongoDB 数据
使用 mongorestore
恢复
mongorestore
是 MongoDB 提供的恢复工具,可以将 BSON 文件导入到 MongoDB 中。
语法
mongorestore --uri <connectionString> <backupDirectory>
示例
恢复整个数据库:
mongorestore --uri "mongodb://localhost:27017" /backup
恢复指定数据库(如 testDB
):
mongorestore --uri "mongodb://localhost:27017" --db testDB /backup/testDB
恢复指定集合(如 testDB.users
):
mongorestore --uri "mongodb://localhost:27017" --db testDB --collection users /backup/testDB/users.bson
示例完整流程
(1) 备份
mongodump --uri "mongodb://localhost:27017" --out /backup
(2) 恢复
mongorestore --uri "mongodb://localhost:27017" /backup
事务处理
MongoDB 从 4.0 版本开始支持多文档事务,并在 4.2 版本中扩展到了分片集群。
- ACID 特性:
- 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败。
- 一致性(Consistency):事务执行前后,数据库的状态保持一致。
- 隔离性(Isolation):事务执行过程中,其他操作无法看到未提交的数据。
- 持久性(Durability):事务提交后,数据将永久保存。
事务的使用条件
- MongoDB 版本:4.0 及以上版本支持副本集事务,4.2 及以上版本支持分片集群事务。
- 存储引擎:必须使用 WiredTiger 存储引擎。
- 集合类型:事务中的集合必须是已存在的集合。
实现事务的步骤
(1) 启动会话(Session)
事务需要在会话(Session)中执行。会话是一个 MongoDB 客户端与服务器之间的上下文。
语法
const session = db.getMongo().startSession();
(2) 启动事务
在会话中启动事务。
语法
session.startTransaction();
(3) 执行操作
在事务中执行多个操作(如插入、更新、删除等)。
语法
const collection = session.getDatabase("testDB").getCollection("users");
collection.insertOne({ name: "Alice", age: 25 });
collection.updateOne({ name: "Alice" }, { $set: { age: 26 } });
(4) 提交事务
如果所有操作成功,提交事务。
语法
session.commitTransaction();
(5) 回滚事务
如果任何操作失败,回滚事务。
语法
session.abortTransaction();
(6) 结束会话
事务结束后,关闭会话。
语法
session.endSession();
事务的完整示例
以下是一个完整的 MongoDB 事务示例:
// javascriptconst session = db.getMongo().startSession();try {session.startTransaction();const usersCollection = session.getDatabase("testDB").getCollection("users");const ordersCollection = session.getDatabase("testDB").getCollection("orders");// 插入用户usersCollection.insertOne({ user_id: 1, name: "Alice", balance: 100 });// 插入订单ordersCollection.insertOne({ user_id: 1, order_id: 1, amount: 50 });// 更新用户余额usersCollection.updateOne({ user_id: 1 }, { $inc: { balance: -50 } });// 提交事务session.commitTransaction();console.log("Transaction committed successfully.");
} catch (error) {// 回滚事务session.abortTransaction();console.log("Transaction aborted due to error:", error);
} finally {// 结束会话session.endSession();
}
预分配空间机制
MongoDB 使用预分配空间机制来管理数据文件,以提高写入性能和减少磁盘碎片。
特点
- 文件大小增长:每次新分配的数据文件大小是上一个文件的 2 倍,直到达到 2GB 的最大限制。
- 例如:第一个文件为 64MB,第二个为 128MB,第三个为 256MB,依此类推。
- 填充 0:预分配的文件会用 0 进行填充,确保文件在磁盘上是连续的,减少写入时的碎片化。
优点
- 减少频繁分配文件的开销,提高写入性能。
- 保持文件的连续性,减少磁盘碎片。
缺点
- 可能导致磁盘空间的浪费,尤其是在数据库初始化时。
文件存储结构
MongoDB 的数据文件和命名空间文件在磁盘上的存储结构如下:
数据文件
- 文件名为
dbname.0
、dbname.1
、dbname.2
等。 - 文件大小按 2 倍增长,最大为 2GB。
- 文件内部划分为多个 盘区(Extent),用于存储集合或索引的数据。
命名空间文件
- 文件名为
dbname.ns
。 - 存储集合和索引的命名空间元数据。
- 默认大小为 16MB,可通过
--nssize
参数调整。
$freelist
- 用于记录不再使用的盘区(如删除的集合或索引)。
- 当需要分配新盘区时,MongoDB 会优先从
$freelist
中回收空闲的磁盘空间。
盘区分配策略
MongoDB 的盘区分配策略旨在平衡空间利用率和数据连续性。
特点
- 盘区大小增长:每次分配的盘区大小是上一次的 2 倍。
- 非连续性:每个命名空间对应的盘区不一定是连续的。
- 空间复用:删除集合或索引后,其占用的盘区会被记录在
$freelist
中,供后续分配时复用。
优点
- 减少空间浪费,提高磁盘利用率。
- 保持数据的连续性,提高查询性能。