MongoDB高频面试题
MongoDB高频面试题
1、MongoDB是什么?
MongoDB是由C++语言编写的,是一个基于分布式文件存储的开源数据库系统。在高负载的情况下,添加更多的节点,可以保证服务器性能。 MongoDB旨在为WEB应用提供可扩展的高性能数据存储解决方案,将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。
MongoDB的核心存储格式是BSON(Binary JSON),它是JSON的二进制表示形式,支持比JSON更多的数据类型,如Date、Binary Data、ObjectId等。MongoDB属于文档型NoSQL数据库,其设计理念是让开发者能够以更自然的方式存储和使用数据,而不需要将数据强行拆分成关系型表格。
与关系型数据库的核心区别:
| 特性 | MongoDB | 关系型数据库(如MySQL) |
|---|---|---|
| 数据模型 | 文档(BSON) | 行和列(表) |
| Schema | 灵活/无固定Schema | 固定Schema |
| 查询方式 | JSON风格查询语言 | SQL |
| 事务支持 | 4.0+ 支持多文档事务 | 原生强事务支持 |
| 扩展方式 | 水平扩展(分片) | 主要垂直扩展 |
| JOIN操作 | $lookup(有限) | 原生JOIN |
| 存储格式 | BSON文档 | 行数据 |
2、MongoDB有哪些特点?
(1)MongoDB 是一个面向文档存储的数据库,操作起来比较简单和容易。文档结构灵活,可以存储嵌套对象和数组,使得数据建模更贴近业务对象。
(2)可以在MongoDB记录中设置任何属性的索引,支持复合索引、多键索引、地理空间索引、全文索引和哈希索引等多种索引类型。
(3)可以通过本地或者网络创建数据镜像,这使得MongoDB有更强的扩展性。支持副本集(Replica Set)实现高可用性,自动故障转移。
(4)如果负载的增加(需要更多的存储空间和更强的处理能力),它可以分布在计算机网络中的其他节点上这就是所谓的分片。支持自动分片(Sharding),数据可以自动分布在多个分片上。
(5)支持丰富的查询表达式,查询指令使用JSON形式的标记,可轻易查询文档中内嵌的对象及数组。支持聚合管道(Aggregation Pipeline)进行复杂的数据分析。
(6)支持MapReduce编程模型,可以处理大规模数据。
(7)采用内存映射文件(Memory-Mapped Files)技术,高效利用操作系统的文件缓存。
(8)支持GridFS规范,可以存储和检索超过16MB的大文件。
(9)MongoDB 4.0+ 支持多文档ACID事务,保证数据一致性。
在.NET项目中的实际应用场景:
// 典型的MongoDB文档模型
public class Order
{
[BsonId]
public ObjectId Id { get; set; }
public string OrderNo { get; set; } = string.Empty;
public List<OrderItem> Items { get; set; } = new(); // 嵌套数组
public CustomerInfo Customer { get; set; } = new(); // 嵌套对象
public OrderStatus Status { get; set; }
public DateTime CreatedAt { get; set; }
}
// 灵活的Schema — 不同类型的订单可以有不同的字段
// 某些订单可能有 CouponCode,某些可能有 GiftMessage
// 不需要预先定义所有可能的字段3、MySQL与MongoDB之间最基本的差别是什么?
MySQL和MongoDB两者都是免费开源的数据库。MySQL和MongoDB有许多基本差别包括数据的表示查询、关系、事务、schema的设计和定义、标准化、速度和性能。通过比较MySQL和MongoDB,实际上我们是在比较关系型和非关系型数据库,即数据存储结构不同。
深度对比:
设计理念:
- MySQL:数据关系优先,通过外键和JOIN维护数据一致性
- MongoDB:数据文档优先,通过嵌套和引用维护数据关系
数据建模方式:
- MySQL:需要预先定义表结构(DDL),修改Schema代价较高
- MongoDB:Schema-free,文档结构可以随时变化
查询能力:
- MySQL:SQL强大且标准化,复杂查询和报表能力强
- MongoDB:JSON查询灵活,适合文档级操作,复杂聚合通过管道实现
事务支持:
- MySQL:原生ACID事务,支持行级锁
- MongoDB:4.0之前只支持单文档原子操作,4.0+支持多文档事务
扩展性:
- MySQL:主要通过读写分离和分库分表实现水平扩展
- MongoDB:原生支持分片,水平扩展更自然
适用场景选择:
- MySQL:需要强事务、复杂JOIN、数据一致性要求高的场景
例如:财务系统、库存管理、银行交易
- MongoDB:数据结构灵活、读写量大、快速迭代的场景
例如:内容管理、日志分析、IoT设备数据、社交应用.NET项目选型建议:
// 选择MongoDB的场景
// 1. 产品属性差异大,需要灵活Schema
// 2. 数据嵌套层级深,JOIN成本高
// 3. 读写量大,需要水平扩展
// 选择MySQL的场景
// 1. 强事务需求(支付、库存扣减)
// 2. 复杂的多表关联查询(报表、统计)
// 3. 数据一致性要求极高
// 实际项目中经常混合使用
// MySQL存储核心交易数据
// MongoDB存储日志、缓存、非结构化数据4、MongoDB中的分片什么意思?
分片是将数据水平切分到不同的物理节点,当应用数据越来越大的时候,数据量也会越来越大。当数据量增长时,单台机器有可能无法存储数据或可接受的读取写入吞吐量,利用分片技术可以添加更多的机器来应对数据量增加以及读写操作的要求。
分片的核心组件:
1. Shard(分片服务器)
- 每个Shard存储数据的一个子集
- 每个Shard可以是一个副本集(Replica Set)
- 生产环境建议每个Shard至少3个节点
2. Config Server(配置服务器)
- 存储集群的元数据和配置信息
- 包括分片和Chunk的映射关系
- 生产环境必须部署3个Config Server(副本集模式)
3. mongos(路由服务器)
- 接收客户端请求
- 根据Config Server的路由信息将请求转发到正确的Shard
- 可以部署多个mongos实现负载均衡
4. Chunk(数据块)
- 数据被分割为多个Chunk
- 默认Chunk大小为64MB
- Balancer进程自动在Shard之间迁移Chunk分片过程示例:
// 1. 启动配置服务器
mongod --configsvr --replSet configReplSet --port 27019
// 2. 启动分片服务器
mongod --shardsvr --replSet shard1 --port 27017
mongod --shardsvr --replSet shard2 --port 27018
// 3. 启动路由服务器
mongos --configdb configReplSet/localhost:27019,localhost:27020,localhost:27021
// 4. 添加分片
sh.addShard("shard1/localhost:27017")
sh.addShard("shard2/localhost:27018")
// 5. 对数据库启用分片
sh.enableSharding("myDatabase")
// 6. 对集合分片(指定分片键)
sh.shardCollection("myDatabase.orders", { "customer_id": 1 })5、MongoDB中的命名空间是什么意思?
MongoDB存储BSON对象在集合(collection)中,数据库名字和集合名字以句点连结起来叫做名字空间。一个集合命名空间又有多个数据域(extent),集合命名空间里存储着集合的元数据,比如集合名称、集合的第一个数据域和最后一个数据域的位置等等。而一个数据域由若干条文档(document)组成,每个数据域都有一个头部,记录着第一条文档和最后一条文档的位置,以及该数据域的一些元数据。extent之间、document之间通过双向链表连接,索引的存储数据结构是B树,索引命名空间存储着对B树的根节点的指针。
命名空间的层次结构:
数据库级别:
- mydb: 数据库名称
- mydb.system.indexes: 系统索引命名空间
- mydb.system.namespaces: 系统命名空间
集合级别:
- mydb.orders: orders集合
- mydb.users: users集合
命名空间长度限制:120字节
命名空间命名规则:
- 不能包含空字符
- 不能以"system."开头(系统保留)
- 不能包含"$"(系统保留)
- 建议全部小写
WiredTiger存储引擎中:
- 使用B+树存储索引
- 数据和索引分别存储
- 支持文档级别的并发控制6、在MongoDb中索引是什么?
索引用于高效的执行查询,没有索引的MongoDB将扫描整个集合中的所有文档,这种扫描效率很低,需要处理大量的数据,索引是一种特殊的数据结构,将一小块数据集合保存为容易遍历的形式。索引能够存储某种特殊字段或字段集的值,并按照索引指定的方式将字段值进行排序。
MongoDB支持的索引类型:
// 1. 单字段索引
db.orders.createIndex({ order_no: 1 })
// 2. 复合索引
db.orders.createIndex({ customer_id: 1, created_at: -1 })
// 3. 多键索引(对数组字段自动创建)
db.orders.createIndex({ tags: 1 })
// 4. 地理空间索引
db.stores.createIndex({ location: "2dsphere" })
// 5. 全文索引
db.articles.createIndex({ title: "text", content: "text" })
// 6. 哈希索引(常用于分片键)
db.users.createIndex({ user_id: "hashed" })
// 7. 唯一索引
db.users.createIndex({ email: 1 }, { unique: true })
// 8. 稀疏索引(只索引包含该字段的文档)
db.users.createIndex({ phone: 1 }, { sparse: true })
// 9. TTL索引(自动过期删除)
db.logs.createIndex({ created_at: 1 }, { expireAfterSeconds: 3600 })
// 10. 部分索引(只索引满足条件的文档)
db.orders.createIndex(
{ status: 1, created_at: -1 },
{ partialFilterExpression: { status: "active" } }
)
// 查看索引使用情况
db.orders.aggregate([{ $indexStats: {} }])
// 查看查询执行计划
db.orders.find({ customer_id: 1001 }).explain("executionStats")索引优化原则:
- ESR原则(Equality, Sort, Range):
复合索引的字段顺序应该是:精确匹配 -> 排序 -> 范围查询
例:{ customer_id: 1, created_at: -1, amount: 1 }
- 覆盖查询(Covered Query):
如果查询只需要索引中包含的字段,MongoDB可以直接从索引返回结果,
无需访问文档数据,大幅提升查询性能
- 避免过度索引:
每个索引都会占用额外的存储空间,并增加写操作的开销
建议每个集合的索引数量控制在 5-10 个以内7、MongoDB成为最好NoSQL数据库的原因是什么?
以下特点使得MongoDB成为最好的NoSQL数据库:面向文档的存储方式使得数据建模更自然;高性能得益于内存映射文件和高效的索引机制;高可用性通过副本集和自动故障转移实现;易扩展性通过原生分片支持水平扩展;丰富的查询语言包括聚合管道、全文搜索和地理空间查询。
MongoDB的核心竞争力:
1. 开发效率高
- Schema-free减少数据库变更成本
- 驱动支持丰富(官方支持15+语言)
- 文档模型减少ORM映射层
2. 运维友好
- 云服务MongoDB Atlas提供托管方案
- Ops Manager提供可视化管理
- 自动化运维(备份、监控、扩缩容)
3. 生态系统完善
- Compass可视化工具
- BI Connector支持SQL查询
- Spark Connector支持大数据分析
4. 企业级特性
- 4.0+ 多文档ACID事务
- LDAP/Kerberos认证
- 审计日志
- 端到端加密8、解释一下什么是MongoDB中的GridFS?
为了存储和检索大文件,例如图像、视频文件和音频文件,使用GridFS。默认情况下,它使用两个文件 fs.files 和 fs.chunks 来存储文件的元数据和块。
GridFS详细原理:
// GridFS将大文件分割为多个chunk(默认255KB一个)
// 存储在两个集合中:
// fs.files — 文件元数据
{
_id: ObjectId("..."),
length: 10485760, // 文件总大小(10MB)
chunkSize: 261120, // 每个chunk的大小
uploadDate: ISODate("2024-03-15"),
filename: "video.mp4",
metadata: { // 自定义元数据
contentType: "video/mp4",
uploader: "user123"
}
}
// fs.chunks — 文件数据块
{
_id: ObjectId("..."),
files_id: ObjectId("..."), // 关联到fs.files
n: 0, // chunk序号
data: BinData(...) // 二进制数据
}
// 使用mongofiles命令行工具操作GridFS
// 上传文件
mongofiles -d mydb put /path/to/video.mp4
// 下载文件
mongofiles -d mydb get video.mp4
// 列出文件
mongofiles -d mydb listC#中使用GridFS:
using MongoDB.Driver.GridFS;
var gridFS = new GridFSBucket(database, new GridFSBucketOptions
{
BucketName = "videos",
ChunkSizeBytes = 4 * 1024 * 1024, // 4MB chunks
WriteConcern = WriteConcern.WMajority
});
// 上传文件
await using var uploadStream = File.OpenRead("video.mp4");
var fileId = await gridFS.UploadFromStreamAsync("video.mp4", uploadStream);
// 下载文件
await using var downloadStream = await gridFS.OpenDownloadStreamAsync(fileId);
await using var fileStream = File.Create("downloaded_video.mp4");
await downloadStream.CopyToAsync(fileStream);
// 查找文件
var filter = Builders<GridFSFileInfo>.Filter.Eq(x => x.Filename, "video.mp4");
var fileInfo = await gridFS.Find(filter).FirstOrDefaultAsync();GridFS vs 直接存储小文件的对比:
GridFS适用场景(文件 > 16MB):
- 视频文件、音频文件、大型图片
- 需要分段下载和断点续传
- 需要文件级别的元数据管理
直接存储适用场景(文件 < 16MB):
- 头像、小图标
- 配置文件
- BSON文档大小的限制是16MB9、分析器在MongoDB中的作用是什么?
MongoDB中包括了一个可以显示数据库中每个操作性能特点的数据库分析器。通过这个分析器你可以找到比预期的慢的查询(或写操作);利用这一信息,比如,可以确定是否需要添加索引。
数据库分析器的使用:
// 设置分析级别
// 0 — 关闭分析器
// 1 — 只记录慢操作(默认:超过100ms)
// 2 — 记录所有操作
db.setProfilingLevel(1, { slowms: 50 }) // 记录超过50ms的操作
// 查看当前分析级别
db.getProfilingStatus()
// 查看分析结果
db.system.profile.find().sort({ ts: -1 }).limit(10)
// 分析结果示例
{
op: "query", // 操作类型
ns: "mydb.orders", // 命名空间
query: { customer_id: 1001 }, // 查询条件
millis: 256, // 执行时间(毫秒)
execStats: { // 执行统计
totalDocsExamined: 50000, // 扫描文档数
totalKeysExamined: 1, // 扫描索引键数
executionTimeMillis: 256 // 执行时间
}
}
// 使用 explain() 分析单个查询
db.orders.find({ customer_id: 1001 }).explain("executionStats")
// 关注以下指标:
// - COLLSCAN: 全表扫描(需要优化)
// - IXSCAN: 索引扫描(好)
// - docsExamined: 扫描的文档数(越少越好)
// - keysExamined: 扫描的索引键数
// - executionTimeMillis: 执行时间10、MongoDB更新操作会立刻 fsync 到磁盘?
不会,磁盘写操作默认是延迟执行的。写操作可能在两三秒(默认在 60 秒内)后到达磁盘。例如,如果一秒内数据库收到一千个对一个对象递增的操作,仅刷新磁盘一次。
MongoDB的写入机制详解:
写入流程:
1. 客户端发送写请求
2. mongod将数据写入内存(WiredTiger Cache)
3. WiredTiger定期(默认60秒)将数据刷到磁盘
4. Journal日志每100ms刷一次(可以配置为每次写入都刷)
Write Concern控制写入确认级别:
- w: 1(默认)— 只需Primary确认
- w: majority — 大多数副本集成员确认
- w: 0 — 不需要确认(最快,可能丢失)
- j: true — 写入Journal后才确认
Read Concern控制读取一致性:
- local — 从Primary读取,可能看到未持久化的数据
- majority — 只读已确认的数据
- linearizable — 线性一致性读取
- available — 从任意节点读取,最快// .NET 中设置 Write Concern
var settings = new MongoClientSettings
{
WriteConcern = WriteConcern.WMajority,
ReadConcern = ReadConcern.Majority
};
var client = new MongoClient(settings);11、MongoDB副本集选举条件有那些?
1.复制集初始化。
2.主节点挂掉。
3.主节点脱离副本集(可能是网络原因)。
4.参与选举的节点数量必须大于副本集总节点数量的一半,如果已经小于一半了所有节点保持只读状态。
选举的详细条件:
触发选举的条件:
1. 副本集初始化(首次启动)
2. Primary节点心跳超时(默认10秒)
3. Primary节点主动stepDown
4. 副本集配置变更
5. 网络分区导致Primary无法与多数节点通信
参与选举的前提条件:
1. 节点必须处于SECONDARY状态
2. 节点的数据不能太落后(oplog延迟不能太大)
3. 节点的优先级(priority)不能为0
4. 选举必须得到"大多数"节点的投票
大多数 = Math.floor(总节点数 / 2) + 1
选举超时时间:
- 默认 electionTimeoutMillis = 10000ms(10秒)
- 建议设置为 2000-10000ms
- 如果设置太短,网络波动可能导致频繁选举
- 如果设置太长,故障恢复时间过长12、简单的描述下MongoDB选举流程
1、副本集中的主节点选举必须满足"大多数"的原则,所谓"大多数"是指副本中一半以上的成员。副本集中成员只有在得到大多数成员投票支持时,才能成为主节点。例如:有N个副本集成员节点,必须有N/2+1个成员投票支持某个节点,此节点才能成为主节点。注意:副本集中若有成员节点处于不可用状态,并不会影响副本集中的"大多数","大多数"是以副本集的配置来计算的。
选举流程详细步骤:
步骤 1:心跳检测
- 每个Secondary节点每2秒向其他节点发送心跳
- 如果10秒内没有收到Primary的心跳,认为Primary已下线
步骤 2:发起选举
- 检测到Primary下线的节点将自己的状态从SECONDARY切换为CANDIDATE
- CANDIDATE为自己投票,并向其他节点请求投票
步骤 3:投票过程
- 其他节点检查投票请求:
(a) 该节点是否比自己更新(oplog position更高)?
(b) 该节点是否比自己更新的oplog更新?
(c) 自己是否在本轮已经投过票?
- 满足条件则投赞成票,否则投反对票
步骤 4:选举结果
- 如果CANDIDATE获得"大多数"选票,成为新的Primary
- 如果没有节点获得"大多数"选票,等待新的选举周期
- 选举周期结束后重新发起选举
步骤 5:状态同步
- 新Primary开始接受写请求
- 其他Secondary节点开始从新Primary同步数据
关于优先级(Priority):
- 默认所有节点的priority为1
- priority为0的节点永远不能成为Primary
- 可以通过设置priority来控制选举偏好
- 适合将某个机房的节点设置为低优先级
关于隐藏节点(Hidden):
- hidden: true的节点对客户端不可见
- 优先级为0,不能成为Primary
- 适合用于报表查询、数据备份
关于延迟节点(Delayed):
- 可以设置oplog延迟时间
- 适合用于数据恢复(误操作回滚)
- 优先级为0,不能成为Primary// 配置副本集成员优先级
cfg = rs.conf()
cfg.members[0].priority = 2 // 主候选节点
cfg.members[1].priority = 1 // 普通节点
cfg.members[2].priority = 0 // 永远不成为Primary
cfg.members[2].hidden = true // 隐藏节点
rs.reconfig(cfg)13、什么是MongoDB分片集群?
Sharding cluster是一种可以水平扩展的模式,在数据量很大时特给力,实际大规模应用一般会采用这种架构去构建。sharding分片很好的解决了单台服务器磁盘空间、内存、cpu等硬件资源的限制问题,把数据水平拆分出去,降低单节点的访问压力。每个分片都是一个独立的数据库,所有的分片组合起来构成一个逻辑上的完整的数据库。因此,分片机制降低了每个分片的数据操作量及需要存储的数据量,达到多台服务器来应对不断增加的负载和数据的效果。
分片策略详解:
1. 范围分片(Range Sharding)
- 按分片键的值范围划分数据
- 优点:范围查询效率高
- 缺点:可能导致数据分布不均匀(热点问题)
- 适合:有明确范围查询需求的场景
2. 哈希分片(Hash Sharding)
- 对分片键值进行哈希计算后分布
- 优点:数据分布均匀,写入分散
- 缺点:不支持范围查询
- 适合:写入量大、数据分布均匀的场景
3. 区域分片(Zone Sharding / Tag Aware Sharding)
- 将数据按照地理区域或业务规则分布
- 可以指定某些数据存储在特定分片上
- 适合:多地域部署、数据合规要求
分片键选择原则:
- 基数高(高区分度)
- 写入分布均匀
- 查询模式匹配(大多数查询包含分片键)
- 不要使用单调递增的字段(如自增ID)作为分片键// 范围分片
sh.shardCollection("mydb.orders", { customer_id: 1 })
// 哈希分片
sh.shardCollection("mydb.logs", { user_id: "hashed" })
// 查看分片状态
sh.status()
// 查看数据分布
db.orders.getShardDistribution()
// 手动迁移Chunk(通常不需要,Balancer自动处理)
sh.moveChunk("mydb.orders", { customer_id: 1001 }, "shard2")14、MongoDB中为何需要水平分片?
1)减少单机请求数,将单机负载,提高总负载。
2)减少单机的存储空间,提高总存空间。
深入理解水平分片的必要性:
垂直扩展 vs 水平扩展:
垂直扩展(Scale Up):
- 增加单台服务器的硬件配置(CPU、内存、磁盘)
- 优点:简单,不需要修改应用
- 缺点:有硬件上限,成本非线性增长
- 单台服务器成本 = 基础成本 * 性能倍数的平方
水平扩展(Scale Out / Sharding):
- 增加更多服务器节点
- 优点:理论上无限扩展,使用普通硬件
- 缺点:系统复杂度增加,需要考虑数据分布和路由
何时需要分片:
1. 数据量超过单台服务器的存储容量(通常 > 2TB)
2. 写入吞吐量超过单台服务器的处理能力
3. 工作集(Working Set)超过单台服务器的内存容量
4. 需要地理分布的数据存储(合规性要求)
不推荐分片的情况:
1. 数据量小(< 100GB)
2. 写入量不大
3. 团队缺乏分片运维经验
4. 单机性能还有优化空间15、MongoDB中分片键的意义何在?
1、一个好的片键对分片至关重要。片键必须是一个索引,通过 sh.shardCollection 会自动创建索引。一个自增的片键对写入和数据均匀分布就不是很好,因为自增的片键总会在一个分片上写入,后续达到某个阀值可能会写到别的分片。但是按照片键查询会非常高效。随机片键对数据的均匀分布效果很好。注意尽量避免在多个分片上进行查询。
分片键选择的最佳实践:
// 好的分片键示例:
// 1. 高基数 + 随机分布
// 用户ID哈希 — 写入均匀分布
sh.shardCollection("mydb.orders", { user_id: "hashed" })
// 2. 复合分片键 — 兼顾均匀分布和查询效率
// { customer_id: 1, order_date: 1 }
// customer_id保证数据分布
// order_date支持时间范围查询
sh.shardCollection("mydb.orders", { customer_id: "hashed", order_date: 1 })
// 差的分片键示例:
// 1. 低基数 — 只有少量不同值
// { status: 1 } — 只有 pending, paid, shipped 等少数值
// 数据无法均匀分布
// 2. 单调递增 — 所有新写入集中在同一个分片
// { _id: 1 } — ObjectId是递增的
// 写入热点问题
// 3. 不支持常用查询模式
// 如果大部分查询是按 customer_id 查找
// 但分片键是 { created_at: 1 }
// 查询需要路由到所有分片(scatter-gather)分片键不可更改(MongoDB 4.2之前):
- 一旦选择了分片键,无法修改
- MongoDB 4.2+ 支持通过 refineCollectionShardKey 修改
- 修改分片键是一个昂贵的操作
分片键对查询的影响:
- 查询包含分片键:mongos直接路由到目标分片(定向查询)
- 查询不包含分片键:mongos需要向所有分片广播查询(scatter-gather)
- scatter-gather查询性能差,应尽量避免16、什么情况下需要用到MongoDB的分片?
1)机器的磁盘不够用了。使用分片解决磁盘空间的问题。
2)单个mongod已经不能满足写数据的性能要求。通过分片让写压力分散到各个分片上面,使用分片服务器自身的资源。
3)想把大量数据放到内存里提高性能。和上面一样,通过分片使用分片服务器自身的资源。
分片前的检查清单:
在决定分片之前,先检查以下几点:
1. 是否已经优化了单机性能?
- 索引是否合理?
- 查询是否高效?
- 工作集是否可以放入内存?
- 读写分离是否已经配置?
2. 数据量是否真的超过了单机限制?
- 存储空间 > 单机磁盘容量的 70%
- 活跃数据集 > 可用内存的 80%
3. 写入量是否超过单机处理能力?
- 单台服务器无法处理峰值写入
4. 是否有运维能力支撑分片?
- 是否有监控和告警?
- 是否有备份策略?
- 是否有回滚方案?17、构建一个分片集群需要用的那些角色?分别是什么?
1)分片服务器(Shard Server)mongod 实例,用于存储实际的数据块,实际生产环境中一个 shard server 角色可由几台机器组个一个 replica set 承担,防止主机单点故障这是一个独立普通的mongod进程,保存数据信息。可以是一个副本集也可以是单独的一台服务器。
2)配置服务器(Config Server)mongod 实例,存储了整个 Cluster Metadata,其中包括 chunk 信息。这是一个独立的mongod进程,保存集群和分片的元数据,即各分片包含了哪些数据的信息。最先开始建立,启用日志功能。像启动普通的 mongod 一样启动配置服务器,指定configsvr 选项。不需要太多的空间和资源,配置服务器的 1KB 空间相当于真实数据的 200MB。保存的只是数据的分布表。
3)路由服务器(Route Server)mongos实例,前端路由,客户端由此接入,且让整个集群看上去像单一数据库,前端应用起到一个路由的功能,供程序连接。本身不保存数据,在启动时从配置服务器加载集群信息,开启 mongos进程需要知道配置服务器的地址,指定configdb选项。
完整的分片集群架构图:
┌──────────────┐
│ Client │
└──────┬───────┘
│
┌──────┴───────┐
│ mongos │ ← 路由服务器(可部署多个)
│ (Query │
│ Router) │
└──┬───────┬───┘
│ │
┌────────┘ └────────┐
│ │
┌────────┴────────┐ ┌────────┴────────┐
│ Config Server │ │ Config Server │ ← 配置服务器(3个副本集)
│ (Replica Set) │ │ (Replica Set) │
└─────────────────┘ └─────────────────┘
mongos 从 Config Server 获取路由信息
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Shard 1 │ │ Shard 2 │ │ Shard 3 │ ← 分片服务器
│ (Replica Set) │ │ (Replica Set) │ │ (Replica Set) │
│ Primary │ │ Primary │ │ Primary │
│ Secondary │ │ Secondary │ │ Secondary │
│ Secondary │ │ Secondary │ │ Secondary │
└────────────────┘ └────────────────┘ └────────────────┘18、如何执行事务/加锁?
MongoDB没有使用传统的锁或者复杂的带回滚的事务,因为它设计的宗旨是轻量,快速以及可预计的高性能。可以把它类比成MySQL MyISAM的自动提交模式。通过精简对事务的支持,性能得到了提升,特别是在一个可能会穿过多个服务器的系统里。
MongoDB 4.0+ 的事务支持:
// 单文档原子操作(所有版本都支持)
db.accounts.updateOne(
{ _id: 1 },
{ $inc: { balance: -100 } } // 原子递减
)
// 多文档事务(MongoDB 4.0+ 副本集,4.2+ 分片集群)
session = db.getMongo().startSession()
try {
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
})
// 转账操作
session.getDatabase("bank").accounts.updateOne(
{ account_id: "A" },
{ $inc: { balance: -500 } }
)
session.getDatabase("bank").accounts.updateOne(
{ account_id: "B" },
{ $inc: { balance: 500 } }
)
session.commitTransaction()
} catch (error) {
session.abortTransaction()
throw error
} finally {
session.endSession()
}// .NET 中使用 MongoDB 事务
using (var session = await client.StartSessionAsync())
{
session.StartTransaction();
try
{
var accounts = database.GetCollection<Account>("accounts");
await accounts.UpdateOneAsync(
session,
Builders<Account>.Filter.Eq(x => x.AccountId, "A"),
Builders<Account>.Update.Inc(x => x.Balance, -500));
await accounts.UpdateOneAsync(
session,
Builders<Account>.Filter.Eq(x => x.AccountId, "B"),
Builders<Account>.Update.Inc(x => x.Balance, 500));
await session.CommitTransactionAsync();
}
catch (Exception)
{
await session.AbortTransactionAsync();
throw;
}
}MongoDB的并发控制机制:
WiredTiger存储引擎的锁机制:
- 文档级锁(Document-level Locking)
- 不同文档的写操作可以并发执行
- 同一文档的写操作串行执行
- 读操作默认使用快照隔离(Snapshot Isolation)
隔离级别:
- Read Uncommitted: 可能读到未提交的数据
- Read Committed: 只读到已提交的数据(默认)
- Snapshot: 事务开始时的数据快照
- Serializable: 最高隔离级别(MongoDB 4.0+)
性能影响:
- 多文档事务比单文档操作慢约30-50%
- 长事务会占用大量资源(锁、oplog空间)
- 建议事务尽量短,每次操作的数据量尽量小19、副本集角色有那些?做是什么?
- 主节点(Primary)接收所有的写请求,然后把修改同步到所有Secondary。一个Replica Set只能有一个Primary节点,当Primary挂掉后,其他Secondary或者Arbiter节点会重新选举出来一个主节点。默认读请求也是发到Primary节点处理的,可以通过修改客户端连接配置以支持读取Secondary节点。
- 副本节点(Secondary)与主节点保持同样的数据集。当主节点挂掉的时候,参与选主。
- 仲裁者(Arbiter)不保有数据,不参与选主,只进行选主投票。使用Arbiter可以减轻数据存储的硬件需求,Arbiter几乎没什么大的硬件资源需求,但重要的一点是,在生产环境下它和其他数据节点不要部署在同一台机器上。
副本集的高级特性:
读写分离配置:
- 默认所有读请求发到Primary
- 可以配置读偏好(Read Preference):
primary: 只从Primary读(默认,一致性最强)
primaryPreferred: 优先Primary,不可用则Secondary
secondary: 只从Secondary读
secondaryPreferred: 优先Secondary
nearest: 从延迟最低的节点读
数据同步机制:
- Primary将写操作记录到oplog(操作日志)
- Secondary异步拉取oplog并重放
- oplog是一个固定大小的capped collection
- 默认oplog大小为磁盘空间的5%
同步延迟监控:
- rs.status() 可以查看每个节点的同步状态
- 关注 optimeDate 差异(Primary vs Secondary)
- 延迟过大的Secondary不能参与选举
标签(Tags)配置:
- 可以给节点打标签,实现定向读写
- 例如:按数据中心、机房打标签
- 读请求可以指定只从特定标签的节点读取// 配置副本集标签
cfg = rs.conf()
cfg.members[0].tags = { dc: "east", use: "production" }
cfg.members[1].tags = { dc: "west", use: "production" }
cfg.members[2].tags = { dc: "east", use: "reporting" }
cfg.settings = {
tagSet: [
{ dc: "east", use: "production" },
{ dc: "west", use: "production" }
]
}
rs.reconfig(cfg)20、非关系型数据库有哪些类型?
1、Key-Value 存储,例如:Redis、Amazon DynamoDB、Memcached。特点是读写极快,适合缓存、会话管理、计数器等场景。
2、文档存储,例如:MongoDB、CouchDB、Amazon DocumentDB。特点是数据结构灵活,适合内容管理、用户画像、物联网数据等场景。
3、列存储,例如:Cassandra、HBase、Amazon Redshift。特点是适合大规模数据写入和分析查询,适合时序数据、日志分析等场景。
4、图数据库,例如:Neo4J、Amazon Neptune、TigerGraph。特点是擅长处理复杂关系和网络分析,适合社交网络、推荐系统、知识图谱等场景。
5、搜索引擎,例如:Elasticsearch、Solr。特点是全文搜索和聚合分析能力强,适合日志搜索、电商商品搜索等场景。
21、数据在什么时候才会扩展到多个分片(shard)里?
MongoDB 分片是基于区域(range)的。所以一个集合(collection)中的所有的对象都被存放到一个块(chunk)中。只有当存在多余一个块的时候,才会有多个分片获取数据的选项。现在,每个默认块的大小是64Mb,所以你需要至少 64 Mb 空间才可以实施一个迁移。
数据迁移的详细机制:
Chunk分裂(Split):
- 当Chunk大小超过阈值(默认64MB)时自动分裂
- 分裂基于分片键的值范围
- 分裂是原地操作,不移动数据
Chunk迁移(Migration):
- Balancer进程负责在分片之间迁移Chunk
- 默认每3秒检查一次是否需要迁移
- 迁移过程中对应用透明
- 可以设置迁移窗口(避免在业务高峰期迁移)
Balancer配置:
- 可以关闭Balancer:sh.stopBalancer()
- 可以设置迁移窗口
- 可以手动移动Chunk
性能影响:
- 迁移期间目标分片的IO负载会增加
- 迁移期间源分片和目标分片的网络带宽消耗增加
- 建议在业务低峰期进行大规模数据导入22、如果在一个分片(shard)停止或者很慢的时候,我发起一个查询会怎样?
如果一个分片(shard)停止了,除非查询设置了"Partial"选项,否则查询会返回一个错误。如果一个分片(shard)响应很慢,MongoDB则会等待它的响应。
分片故障处理机制:
// 查询选项
// allowPartialResults: 当部分分片不可用时,返回可用分片的结果
db.orders.find({}).allowPartialResults()
// 生产环境建议:
// 1. 每个分片都是副本集,避免单点故障
// 2. 设置合理的连接超时时间
// 3. 监控分片健康状态
// 4. 配置Write Concern为majority,保证数据可靠性// .NET 中设置分片集群的容错策略
var settings = new MongoClientSettings
{
Servers = new List<MongoServerAddress>
{
new("mongos1", 27017),
new("mongos2", 27017),
new("mongos3", 27017)
},
// 连接超时
ConnectTimeout = TimeSpan.FromSeconds(10),
// 服务器选择超时
ServerSelectionTimeout = TimeSpan.FromSeconds(30),
// 重试写入
RetryWrites = true,
// 读偏好
ReadPreference = ReadPreference.Nearest
};23、如何理解MongoDB中的GridFS机制,MongoDB为何使用GridFS来存储文件?
GridFS是一种将大型文件存储在MongoDB中的文件规范。使用GridFS可以将大文件分隔成多个小文档存放,这样我们能够有效的保存大文档,而且解决了BSON对象有限制的问题。
GridFS的设计动机和优势:
为什么需要GridFS:
1. BSON文档大小限制为16MB,大文件无法直接存储
2. GridFS将大文件分割为多个chunk(默认255KB),突破16MB限制
3. 支持范围请求(Range Request),可以分段下载
4. 支持断点续传
5. 可以存储任意类型的元数据
6. 可以利用MongoDB的副本集和分片机制
GridFS vs 对象存储(如S3):
GridFS优势:
- 与MongoDB生态集成,使用相同的驱动和连接
- 事务一致性(文件元数据和文件数据在同一数据库)
- 查询能力(可以按元数据搜索文件)
对象存储优势:
- 专门为文件存储设计,性能更优
- 更好的CDN集成
- 成本更低
- 支持更大的文件
建议:
- 小文件(< 16MB):直接存储在文档中
- 中等文件(16MB - 100MB):GridFS
- 大文件(> 100MB):对象存储(S3/OSS)24、我应该启动一个集群分片(sharded)还是一个非集群分片的 MongoDB 环境?
为开发便捷起见,我们建议以非集群分片(unsharded)方式开始一个 MongoDB 环境,除非一台服务器不足以存放你的初始数据集。从非集群分片升级到集群分片(sharding)是无缝的,所以在你的数据集还不是很大的时候没必要考虑集群分片(sharding)。
架构演进路线建议:
阶段 1:单机 MongoDB
- 数据量 < 100GB
- 并发请求 < 1000 QPS
- 适合:开发环境、小型应用
阶段 2:副本集(Replica Set)
- 数据量 100GB - 1TB
- 需要高可用性和读写分离
- 适合:生产环境的标准配置
- 推荐配置:1 Primary + 2 Secondary
阶段 3:分片集群(Sharded Cluster)
- 数据量 > 1TB
- 写入吞吐量超过单机处理能力
- 需要水平扩展
- 推荐配置:2+ Shard(每个都是副本集)+ 3 Config Server + 2+ mongos
阶段 4:多地域分片
- 需要数据就近访问
- 数据合规要求(数据不能出境)
- 使用Zone Sharding按地域分布数据25、MongoDB适合应用在那些场景?
从目前阿里云 MongoDB 云数据库上的用户看,MongoDB 的应用已经渗透到各个领域,比如游戏、物流、电商、内容管理、社交、物联网、视频直播等,以下是几个实际的应用案例。
游戏场景:
- 存储游戏用户信息
- 用户的装备、积分等直接以内嵌文档的形式存储
- 方便查询、更新
- 高并发读写需求
- 示例:玩家背包系统
物流场景:
- 存储订单信息
- 订单状态在运送过程中会不断更新
- 以 MongoDB 内嵌数组的形式来存储物流轨迹
- 一次查询就能将订单所有的变更读取出来
- 示例:快递追踪系统
社交场景:
- 存储用户信息、朋友圈信息
- 通过地理位置索引实现"附近的人"功能
- 好友关系图谱
- 消息推送记录
- 示例:社交Feed流
物联网场景:
- 存储所有接入的智能设备信息
- 设备汇报的日志信息(时序数据)
- 多维度分析设备数据
- 示例:设备监控平台
内容管理:
- 文章、评论、标签
- Schema灵活,不同类型的内容有不同的字段
- 全文搜索
- 示例:CMS系统26、"ObjectID"有哪些部分组成
一共有四部分组成:时间戳、客户端ID、客户进程ID、三个字节的增量计数器。_id是一个 12 字节长的十六进制数,它保证了每一个文档的唯一性。在插入文档时,需要提供_id。如果你不提供,那么 MongoDB 就会为每一文档提供一个唯一的 id。_id的头 4 个字节代表的是当前的时间戳,接着的后 3 个字节表示的是机器 id 号,接着的 2 个字节表示 MongoDB 服务器进程 id,最后的 3 个字节代表递增值。
ObjectId详细结构:
ObjectId (12 bytes = 24 hex characters):
┌────────────────┬─────────────────┬──────────────────┬─────────────────┐
│ Timestamp (4B) │ Machine ID (3B) │ Process ID (2B) │ Counter (3B) │
│ 秒级Unix时间戳 │ 机器唯一标识 │ 进程ID │ 自增计数器 │
│ 前8个hex字符 │ 接下来6个hex字符 │ 接下来4个hex字符 │ 最后6个hex字符 │
└────────────────┴─────────────────┴──────────────────┴─────────────────┘
示例:660f1a3b a1b2c3 01e4 000001
│ │ │ │
时间戳 机器ID PID 计数器
ObjectId的特点:
1. 时间有序:同一秒内生成的ObjectId按计数器递增
2. 全局唯一:无需中心化ID生成器
3. 可排序:按生成时间自然排序
4. 包含时间信息:可以从ObjectId中提取生成时间
从ObjectId提取时间:
new Date(parseInt(ObjectId.valueOf().substring(0, 8), 16) * 1000)
适用场景:
- 文档ID
- 嵌入文档的关联ID
- 排序键(按创建时间排序)27、如何使用"AND"或"OR"条件循环查询集合中的文档
在 find() 方法中,如果传入多个键,并用逗号( , )分隔它们,那么 MongoDB 会把它看成是AND条件。
>db.mycol.find({key1:value1, key2:value2}).pretty()
若基于OR条件来查询文档,可以使用关键字$or。
>db.mycol.find(
{
$or: [
{key1: value1}, {key2:value2}
]
}
).pretty()更多查询操作符:
// AND + OR 组合查询
db.orders.find({
status: "paid", // AND条件
$or: [
{ amount: { $gt: 1000 } },
{ priority: "high" }
]
})
// 常用查询操作符
// 比较操作符
db.orders.find({ amount: { $gt: 100, $lte: 500 } }) // 100 < amount <= 500
// 元素操作符
db.orders.find({ tags: { $exists: true } }) // 字段存在
db.orders.find({ remark: { $ne: null } }) // 字段不为null
// 数组操作符
db.orders.find({ tags: { $in: ["urgent", "vip"] } }) // 数组包含
db.orders.find({ tags: { $all: ["a", "b"] } }) // 数组同时包含
db.orders.find({ tags: { $size: 3 } }) // 数组长度
// 正则表达式
db.users.find({ name: { $regex: "^张", $options: "i" } })
// 逻辑操作符
db.orders.find({ $nor: [{ status: "cancelled" }, { status: "refunded" }] })
// 子文档查询
db.users.find({ "address.city": "北京" })
db.users.find({ "address.city": "北京", "address.district": "朝阳" })28、在MongoDB中如何排序?
MongoDB 中的文档排序是通过 sort() 方法来实现的。 sort() 方法可以通过一些参数来指定要进行排序的字段,并使用 1 和 -1 来指定排序方式,其中 1 表示升序,而 -1 表示降序。
>db.connectionName.find({key:value}).sort({columnName:1})排序注意事项:
// 复合排序
db.orders.find({ status: "paid" })
.sort({ created_at: -1, amount: -1 }) // 先按时间降序,时间相同按金额降序
// 内存排序限制
// 默认排序超过32MB会报错
// 解决方案1:创建索引
db.orders.createIndex({ status: 1, created_at: -1 })
// 解决方案2:使用 allowDiskUse
db.orders.find({ status: "paid" })
.sort({ created_at: -1 })
.allowDiskUse(true)
// 排序与分页配合
db.orders.find({ status: "paid" })
.sort({ created_at: -1 })
.skip(20)
.limit(10)
// 注意:大数据量分页时 skip 性能差
// 替代方案:基于上一页最后一条记录的游标分页
db.orders.find({
status: "paid",
_id: { $lt: lastId } // 上一页最后一条的_id
}).sort({ _id: -1 }).limit(10)29、什么是聚合?
聚合操作能够处理数据记录并返回计算结果。聚合操作能将多个文档中的值组合起来,对成组数据执行各种操作,返回单一的结果。它相当于 SQL 中的 count(*) 组合 group by。对于 MongoDB 中的聚合操作,应该使用 aggregate() 方法。
>db.COLLECTION_NAME.aggregate(AGGREGATE_OPERATION)聚合管道详细示例:
// 电商销售分析聚合管道
db.orders.aggregate([
// 阶段1:过滤
{ $match: {
status: "completed",
created_at: { $gte: ISODate("2024-01-01") }
}},
// 阶段2:关联用户信息
{ $lookup: {
from: "users",
localField: "user_id",
foreignField: "_id",
as: "user"
}},
// 阶段3:展开数组
{ $unwind: "$user" },
// 阶段4:分组统计
{ $group: {
_id: {
year: { $year: "$created_at" },
month: { $month: "$created_at" },
city: "$user.city"
},
totalRevenue: { $sum: "$amount" },
orderCount: { $sum: 1 },
avgOrderValue: { $avg: "$amount" }
}},
// 阶段5:排序
{ $sort: { totalRevenue: -1 } },
// 阶段6:限制结果
{ $limit: 10 }
], { allowDiskUse: true })30、raft选举过程,投票规则?
选举过程:
当系统启动好之后,初始选举后系统由1个Leader和若干个Follower角色组成。然后突然由于某个异常原因,Leader服务出现了异常,导致Follower角色检测到和Leader的上次RPC更新时间超过给定阈值时间时。此时Follower会认为Leader服务已出现异常,然后它将会发起一次新的Leader选举行为,同时将自身的状态从Follower切换为Candidate身份。随后请求其它Follower投票选择自己。
Raft协议详细流程:
Raft角色:
1. Leader — 处理所有客户端请求,复制日志到Follower
2. Candidate — 选举期间的候选角色
3. Follower — 被动接收Leader的日志复制
选举详细过程:
1. Follower的election timeout到期(150-300ms随机)
2. Follower转换为Candidate,增加currentTerm
3. Candidate为自己投票
4. Candidate向所有其他节点发送RequestVote RPC
5. 其他节点检查投票条件:
(a) Candidate的term >= 自己的term
(b) Candidate的日志至少和自己一样新
(c) 自己在本term内没有投过票
6. 如果Candidate获得"大多数"选票,成为Leader
7. Leader开始发送心跳(AppendEntries RPC)
安全性保证:
- 每个term最多一个Leader
- Leader必须包含所有已提交的日志条目
- Follower会拒绝日志比自己旧的Candidate
日志复制:
- Leader收到客户端写请求
- Leader将条目追加到自己的日志
- Leader并行发送AppendEntries到所有Follower
- 大多数Follower确认后,Leader提交条目
- Leader通知Follower提交这组题真正考什么
- 面试官往往不只是考定义,而是在看你能否把基础概念放回真实 .NET 场景。
- 这类题经常沿着语言基础、框架设计、性能和工程实践往下追问。
- 高分答案通常有三层:结论、原因、项目中的例子。
- MongoDB面试题往往与分布式系统概念交叉,如副本集、分片、选举等。
60 秒答题模板
- 先用一句话给结论。
- 再补关键原理或底层机制。
- 最后说适用边界、常见坑或项目中的使用经验。
容易失分的点
- 只会背术语,不会举例。
- 回答太散,没有结构。
- 忽略版本差异和工程背景。
- 不了解MongoDB的事务演进(4.0之前和之后差异大)。
- 不了解WiredTiger存储引擎的特性。
刷题建议
- 把答案拆成"定义、适用场景、风险点、实战例子"四段来复述。
- 遇到 .NET 基础题时,尽量补一个框架级别的落地场景,而不是只背术语。
- 高频概念题建议自己再追问一层:底层原理、常见坑、性能代价分别是什么。
- MongoDB面试题要结合实际运维经验回答,例如分片策略选择、副本集故障处理。
高频追问
- 如果面试官继续追问底层实现,你能否解释运行机制或源码层面的关键点?
- 如果题目放到 ASP.NET Core、消息队列或数据库场景里,这个结论是否还成立?
- 是否存在版本差异、框架差异或特殊边界条件需要主动说明?
- MongoDB的WiredTiger存储引擎有哪些核心特性?
- MongoDB 4.0/4.2/5.0分别引入了哪些重要特性?
复习重点
- 把每道题的关键词整理成自己的知识树,而不是只背原句。
- 对容易混淆的概念要做横向比较,例如机制差异、适用边界和性能代价。
- 复习时优先补"为什么",其次才是"怎么用"和"记住什么术语"。
- 重点掌握副本集选举、分片策略、索引优化和聚合管道。
面试作答提醒
- 先给结论,再补原因和例子。
- 回答基础题时不要只说"能用",最好补一句为什么这样选。
- 如果记不清细节,优先说出适用边界和排查思路。
- MongoDB的面试回答要体现出运维经验和实际项目经验。
