索引与优化
1.1 索引类型
| 类型 | 说明 |
|---|
| 单字段索引 | 最基础,{field: 1} / {field: -1} |
| 复合索引 | 多字段组合,遵循最左前缀 |
| 多键索引(Multikey) | 字段是数组时自动创建,每个数组元素一条索引项 |
| 唯一索引 | {unique: true},_id 默认唯一且不可删 |
| 稀疏索引 | 只索引「存在该字段」的文档 |
| 部分索引 | 按 partialFilterExpression 只索引满足条件的文档,比稀疏更灵活 |
| TTL 索引 | 基于 Date 字段到期自动删除 |
| 文本索引 | {field: "text"},支持全文检索 |
| 地理空间索引 | 2dsphere / 2d |
| 哈希索引 | {field: "hashed"},用于哈希分片 |
每个集合默认在 _id 字段上有一个唯一索引。
db.user.createIndex({ name: 1 })
db.user.createIndex({ name: 1, age: -1 })
db.user.createIndex({ name: 1 }, { unique: true })
db.user.createIndex({ email: 1 }, { unique: true, sparse: true })
db.user.createIndex({ age: 1 }, { partialFilterExpression: { age: { $gt: 18 } } })
db.user.createIndex({ createTime: 1 }, { expireAfterSeconds: 3600 })
db.user.createIndex({ desc: "text" })
db.user.createIndex({ loc: "2dsphere" })
db.user.createIndex({ shardKey: "hashed" })
db.users.createIndex({ age: 1 }, { background: true })
db.user.getIndexes()
db.user.dropIndex("name_1")
1.2 索引核心规则
| 问题 | 答题要点 |
|---|
| 最左前缀原则 | 查询条件必须包含复合索引的前导字段,否则用不上 |
| ESR 规则 | 复合索引字段顺序按 Equality(等值查询) → Sort(排序查询) → Range(范围查询) 排列 |
| 覆盖索引 | 查询条件 + 返回字段全在索引里,不用回表;explain 结果里没有 FETCH 阶段;注意排除 _id(投影加 _id: 0) |
| 索引失效场景 | $ne / $nin / $not;$regex 非 ^ 前缀;类型不一致(用字符串查数字);跳过复合索引前导字段;$or 中有字段没索引 |
| 索引的代价 | 占空间 + 写放大(每次写都要维护索引),不是越多越好 |
1.3 与 MySQL 索引对比
| 维度 | MySQL(InnoDB) | MongoDB |
|---|
| 索引结构 | B+树 | B树 |
| 索引类型 | 主键、唯一、普通、联合、全文等 | 单字段、复合、多键、文本、地理、哈希、TTL |
| 最左前缀 | 支持 | 支持(复合索引) |
| 索引覆盖 | 支持(覆盖索引) | 支持(covered query) |
| 索引代价 | 占用空间,降低写入速度 | 同上 |
1.4 慢查询定位
db.setProfilingLevel(1, { slowms: 100 })
db.system.profile.find().sort({ ts: -1 }).limit(10)
db.user.find({ age: { $gt: 18 } }).explain("executionStats")
看三个关键值:
stage:COLLSCAN 全表扫描(要警惕) / IXSCAN 走索引(正常)totalDocsExamined 与 nReturned:比值应接近 1,差得越多说明索引选择性越差executionTimeMillis:实际耗时
辅助工具:mongostat(整体吞吐)、mongotop(各集合读写耗时)、db.currentOp()(当前操作)。
1.5 TTL / 稀疏 / 部分索引的坑
- TTL 索引基于 Date 字段 +
expireAfterSeconds;后台线程每 60 秒扫一次,删除时间不精确;被 TTL 删除的文档同样会产生 oplog - 稀疏索引只跳过「完全没有该字段」的文档,字段值为
null 仍会被索引 - 部分索引用
partialFilterExpression,表达力更强,优先选部分索引
1.6 性能优化
- 为高频查询字段建索引,尤其是
where、sort、join 用到的字段。 - 复合索引字段顺序:等值查询在前,范围查询在后,排序字段最后。
- 避免过多索引:每个索引都会降低写入速度,增加存储。
- 使用覆盖查询:尽量让查询字段和返回字段都包含在索引中。
- 使用部分索引:只索引常用子集,减少索引大小。
- 使用 TTL 索引:自动清理过期数据。
- 定期分析慢查询:开启
profiling,用 explain 诊断。 - 避免在索引字段上使用
$ne、$not、$regex(非前缀),这些通常导致全表扫描。
事务与一致性
一、事务基础概念
1.1 什么是事务
事务是一组操作的集合,这些操作要么全部成功,要么全部失败,保证数据的一致性。
1.2 MongoDB 事务的发展
| 版本 | 支持情况 |
|---|
| 4.0 | 副本集支持多文档事务 |
| 4.2 | 分片集群支持多文档事务 |
| 4.4 | 支持在事务中创建集合和索引 |
| 5.0 | 事务性能优化,支持更多场景 |
1.3 与关系型数据库的对比
| 维度 | MySQL | MongoDB |
|---|
| 事务粒度 | 行级、表级 | 文档级、多文档 |
| 隔离级别 | 读未提交、读已提交、可重复读(默认)、串行化 | 快照隔离(Snapshot Isolation) |
| 默认隔离级别 | 可重复读 | 快照隔离 |
| 跨分片事务 | 支持 | 4.2+ 支持 |
| 性能开销 | 较高 | 较高,但通常低于跨分片 |
| 使用复杂度 | 简单、SQL标准 | 需要Session |
| 适用场景 | 强事务、复杂关联 | 灵活文档、水平扩展、事务需求少 |
const session = db.getMongo().startSession();
session.startTransaction();
try {
session.getDatabase("app").orders.updateOne({ _id: 1 }, { $set: { status: "PAID" } });
session.getDatabase("app").stock.updateOne({ sku: "A" }, { $inc: { qty: -1 } });
session.commitTransaction();
} catch (e) {
session.abortTransaction();
}
二、单文档原子性
2.1 核心原则
单文档原子性:MongoDB 对单个文档的更新天然原子(含嵌套子文档、数组),很多「伪事务」场景其实一条 updateOne 就能解决,不必上事务。
2.3 与多文档事务的关系
- 单文档原子性不需要显式事务,MongoDB 自动保证。
- 多文档事务只在需要跨文档、跨集合、跨分片时使用。
三、多文档事务
3.1 使用场景
- 跨集合操作(如订单和库存)
- 跨文档操作(如转账)
- 跨分片操作(分片集群)
3.2 基本语法
const session = db.getMongo().startSession();
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
});
try {
const orders = session.getDatabase("shop").orders;
const inventory = session.getDatabase("shop").inventory;
orders.updateOne({ _id: 1 }, { $set: { status: "confirmed" } });
inventory.updateOne({ sku: "A001" }, { $inc: { stock: -1 } });
session.commitTransaction();
} catch (error) {
session.abortTransaction();
throw error;
} finally {
session.endSession();
}
3.3 读关注(readConcern)
决定读取操作能看到什么样的数据
控制读的时候,数据需要被多少个节点确认,或者是否需要一致性快照
| 级别 | 说明 |
|---|
| local | 默认,读取当前节点最新数据,可能回滚 |
| majority | 读取已确认多数节点的数据,不会回滚 |
| snapshot | 事务内使用,保证一致性快照 |
| linearizable | 线性一致,保证读到最新已提交数据 |
3.4 写关注(writeConcern)
写操作需要被确认到什么程度,才返回成功
控制写操作 要写到多少个节点、是否要落盘,才算成功
| 级别 | 说明 | 适用场景 |
|---|
| w: 1 | 主节点确认即可 | 一般写入,性能优先 |
| w: majority | 多数节点确认,推荐用于事务 | 重要数据,防止回滚 |
| w: 0 | 不等待确认,直接返回 | 日志、监控等可容忍丢失场景 |
| j: true | 写入 journal 后才确认 | 需持久化保证 |
| w: majority + j: true | 多数节点确认且落盘 | 最高持久性,性能最差 |
四、ACID 特性
| 特性 | MongoDB 支持情况 |
|---|
| 原子性(Atomicity) | 单文档天然原子;多文档事务保证全部成功或全部失败 |
| 一致性(Consistency) | 事务提交后数据满足约束;应用层需自行保证业务规则 |
| 隔离性(Isolation) | 快照隔离,事务之间互不干扰;写冲突时事务中止并重试 |
| 持久性(Durability) | 依赖 writeConcern,w:majority + j:true 可保证 |
五、使用限制与注意事项
5.1 限制
- 事务超时:默认 60 秒,可调整
transactionLifetimeLimitSeconds。 - 文档大小:事务中所有操作的总大小不能超过 16MB(oplog 限制)。
- 不支持的操作:
- 创建或删除集合(4.4 之前)
- 创建或删除索引(4.4 之前)
count 命令(可使用 countDocuments)distinct 命令- 某些管理命令
- 分片集群:跨分片事务性能开销大,需谨慎使用。
- 写冲突:事务内写冲突会导致事务中止,应用需重试。
5.2 性能开销
- 多文档事务比单文档操作慢,因为需要协调多个节点、维护快照、写 oplog。
- 事务越长,持有锁和资源越久,冲突概率越高。
- 建议事务尽量短小,避免长时间运行。
5.3 重试机制
function runTransactionWithRetry(session, txnFunc) {
while (true) {
try {
session.startTransaction();
txnFunc(session);
session.commitTransaction();
break;
} catch (error) {
session.abortTransaction();
if (error.hasErrorLabel("TransientTransactionError")) {
continue;
}
throw error;
}
}
}