一、基础概念
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实例(进程)组成,包含一个 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 的选举机制基于 Raft 协议,这是一种分布式一致性算法。
选举过程由从节点(Secondary)和仲裁节点(Arbiter)参与,确保在大多数节点同意的情况下选出新的主节点。
触发选举的条件
以下情况会触发选举:
- 主节点不可用(如宕机或网络故障)。
- 主节点与其他节点的通信中断超过一定时间(默认为 10 秒)。
- 主节点主动退出(如人为执行
rs.stepDown()
命令)。
选举的参与者
- 从节点(Secondary):存储数据,可以参与选举并成为主节点。
- 仲裁节点(Arbiter):不存储数据,仅参与选举投票。
选举的过程
选举过程包括以下步骤:
(1) 检测主节点故障
- 从节点和仲裁节点会定期与主节点通信(通过心跳机制)。
- 如果主节点在指定时间内(默认为 10 秒)未响应,从节点会认为主节点不可用。
(2) 发起选举
- 从节点会发起选举请求,向其他节点发送投票请求。
- 每个从节点和仲裁节点都会参与投票。
(3) 投票规则
- 新主节点必须获得大多数节点的投票(例如,在 3 个节点的复制集中,至少需要 2 票)。
- 优先级高的节点更有可能被选为主节点(可以通过
priority
参数设置优先级)。
(4) 选出新主节点
- 获得大多数投票的从节点将成为新的主节点。
- 新的主节点开始处理写操作,并继续将操作日志(Oplog)发送给其他从节点。
选举的配置参数
可以通过以下参数配置选举行为:
(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)
手动触发选举
如果需要手动触发选举(如维护主节点),可以使用以下命令:
rs.stepDown() # 让当前主节点主动退出
如何向复制集中添加新节点?
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分片的原理是什么?
- 如何选择合适的分片键?
- 如何启用分片功能?
- 配置服务器、路由服务器和分片服务器的作用分别是什么?
- 如何对已分片集合进行重新分片?
- 分片集群的性能瓶颈有哪些?如何优化?
- 如何监控分片集群的状态和性能?
- 数据在分片集群中是如何分布的?
六、性能优化
- MongoDB查询性能慢的可能原因有哪些?
- 如何通过索引优化查询性能?
- 如何避免全表扫描?
- 如何优化MongoDB的内存使用?
- 调整缓存大小对性能有何影响?
- 如何优化写入性能?
- 在高并发场景下如何优化性能?
- 如何使用性能分析工具定位问题?
七、安全
- 如何在MongoDB中启用身份验证?
- MongoDB内置角色的权限范围是什么?
- 如何创建自定义角色并分配权限?
- 如何配置网络访问安全(如绑定IP地址)?
- 如何使用SSL/TLS加密数据传输?
- 如何备份和恢复MongoDB数据?
- 如何管理多用户环境下的权限?
- 如何防止MongoDB受到常见安全攻击(如注入攻击)?
八、高级应用
- 如何在MongoDB中实现事务处理?
- 如何使用MongoDB的地理空间功能?
- 如何在MongoDB中实现全文搜索?
- 如何与Node.js、Python等技术集成使用MongoDB?
- 如何处理大规模数据的导入和导出?
- 如何对MongoDB进行水平扩展和垂直扩展?
- 如何设计和优化数据模型以适应复杂业务需求?
- 如何在MongoDB中实现数据的版本控制?