在上篇《筑基篇》中,我们从零搭建了MongoDB环境,吃透了核心概念、BSON数据类型、文档CRUD全操作、索引基础与聚合管道入门,相信你已经能独立完成90%业务场景下的基础开发。但随着业务规模增长,你一定会遇到这些瓶颈:
- 做LBS业务时,需要查询"附近3公里的商家",却不知道MongoDB原生支持地理空间查询?
- 做内容社区、商品搜索时,需要全文检索功能,却不知道MongoDB自带全文索引,无需额外引入Elasticsearch?
- 做复杂数据统计时,需要关联多个集合、一次查询多个维度的统计结果,却不知道聚合管道的高阶用法?
- 高并发场景下,需要保证"库存扣减+订单生成"的原子性,却不知道MongoDB 4.0+已经支持完整的事务?
- 单库单表扛不住海量数据和高并发读写,却不知道MongoDB原生支持副本集高可用和分片集群水平扩展?
- 慢查询堆积,却不知道怎么深度分析执行计划、怎么优化索引、怎么排查性能瓶颈?
别慌,这些问题正是本篇要彻底解决的核心。作为系列的第二篇------进阶精通篇,我们将完全承接上篇的基础,从高级查询、聚合管道高阶用法,到数据模型设计进阶、事务原理与实战,再到索引进阶、副本集、分片集群架构,最后到生产环境运维,带你彻底打通MongoDB的"任督二脉",从会用MongoDB变成真正懂MongoDB的企业级实战高手。
系列整体回顾与本篇核心规划
系列进度回顾
- 上篇(筑基篇):MongoDB核心认知、全场景环境搭建、核心数据模型与术语、文档CRUD全解、索引基础、聚合管道入门
- 下篇(进阶精通篇,本篇):高级查询操作、聚合管道高阶用法、数据模型设计进阶、事务原理与实战、索引进阶与性能优化、副本集架构、分片集群架构、生产环境运维与最佳实践
- 附加篇(八股文全集):从基础到高阶全覆盖的MongoDB面试题,附标准答案与答题思路,适配校招、社招全场景
本篇核心学习目标
学完本篇,你将达到中高级开发的MongoDB水平,具备生产环境实战能力:
- 熟练掌握MongoDB的高级查询特性:地理空间查询、全文索引、正则查询、游标操作,搞定LBS、全文搜索等特色业务场景
- 完全吃透聚合管道的高阶用法:lookup关联查询、lookup关联查询、lookup关联查询、unwind数组拆分、$facet多面聚合、窗口函数,用简洁的管道实现复杂的数据统计
- 掌握MongoDB数据模型设计进阶:嵌入式vs引用式模型的选择、分桶设计、海量数据模型设计,从源头优化性能
- 深入理解MongoDB事务原理:副本集事务、分片集群分布式事务、事务隔离级别,能在高并发场景下保证数据一致性
- 精通索引进阶与性能优化:索引底层原理、索引失效场景、慢查询分析与优化、执行计划深度解析,搞定千万级数据下的查询优化
- 掌握MongoDB高可用与水平扩展:副本集架构搭建、故障自动转移、读写分离;分片集群架构搭建、分片策略、水平扩展,支撑PB级海量数据
- 具备生产环境运维能力:监控、安全配置、性能调优、故障排查,能独立处理生产环境的常见问题
第一章 高级查询操作:搞定特色业务场景
在上篇中,我们学习了基础的条件查询、嵌套文档查询、数组查询,这一章我们讲解MongoDB的高级查询特性,这些是MongoDB的核心优势,能帮你搞定很多关系型数据库很难实现的业务场景。
前置说明
后续所有示例,均基于上篇的user_db数据库,我们新增几个业务集合,用于演示高级查询:
javascript
// 1. 商家集合:用于地理空间查询演示
db.createCollection("merchants", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name", "location", "category", "create_time"],
properties: {
name: { bsonType: "string", description: "商家名称" },
location: { bsonType: "object", description: "地理位置,GeoJSON格式" },
category: { bsonType: "string", description: "商家分类" },
rating: { bsonType: "double", minimum: 0, maximum: 5, description: "评分" },
create_time: { bsonType: "date" }
}
}
}
});
// 插入商家测试数据,location为GeoJSON格式的Point类型
db.merchants.insertMany([
{
name: "肯德基(天河城店)",
location: { type: "Point", coordinates: [113.327, 23.125] }, // 经度,纬度
category: "快餐",
rating: 4.2,
create_time: new Date()
},
{
name: "麦当劳(正佳广场店)",
location: { type: "Point", coordinates: [113.332, 23.128] },
category: "快餐",
rating: 4.5,
create_time: new Date()
},
{
name: "海底捞(太古汇店)",
location: { type: "Point", coordinates: [113.335, 23.130] },
category: "火锅",
rating: 4.8,
create_time: new Date()
},
{
name: "太二酸菜鱼(万菱汇店)",
location: { type: "Point", coordinates: [113.330, 23.126] },
category: "川菜",
rating: 4.6,
create_time: new Date()
}
]);
// 2. 商品集合:用于全文索引演示
db.createCollection("goods", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name", "description", "price", "category", "create_time"],
properties: {
name: { bsonType: "string", description: "商品名称" },
description: { bsonType: "string", description: "商品描述" },
price: { bsonType: "decimal", description: "商品价格" },
category: { bsonType: "string", description: "商品分类" },
tags: { bsonType: "array", description: "商品标签" },
create_time: { bsonType: "date" }
}
}
}
});
// 插入商品测试数据
db.goods.insertMany([
{
name: "iPhone 15 Pro Max 256GB 钛金属",
description: "苹果最新款旗舰手机,A17 Pro芯片,钛金属边框,专业级摄影系统",
price: NumberDecimal("9999.00"),
category: "手机",
tags: ["苹果", "iPhone", "旗舰", "5G"],
create_time: new Date()
},
{
name: "华为Mate 60 Pro 512GB 雅丹黑",
description: "华为最新旗舰手机,麒麟9000S芯片,卫星通话,超可靠玄武架构",
price: NumberDecimal("7999.00"),
category: "手机",
tags: ["华为", "Mate", "旗舰", "5G", "卫星通话"],
create_time: new Date()
},
{
name: "MacBook Pro 14英寸 M3 Pro 18GB/512GB",
description: "苹果专业笔记本电脑,M3 Pro芯片,Liquid Retina XDR显示屏,超长续航",
price: NumberDecimal("14999.00"),
category: "笔记本电脑",
tags: ["苹果", "MacBook", "M3", "专业"],
create_time: new Date()
}
]);
1.1 地理空间查询:LBS业务的核心利器
地理空间查询是MongoDB的核心特色功能,原生支持地理空间索引和查询,完美适配外卖、网约车、同城社交、运动轨迹等LBS(基于位置的服务)业务场景,无需额外引入Redis GEO或其他组件。
1.1.1 核心前置:GeoJSON格式
MongoDB的地理空间数据,必须使用GeoJSON格式存储,这是国际通用的地理空间数据格式,核心类型如下:
| GeoJSON类型 | 含义 | 业务场景 |
|---|---|---|
| Point | 点,单个地理位置坐标 | 商家位置、用户位置、车辆位置 |
| LineString | 线,多个点组成的线段 | 运动轨迹、道路、路线 |
| Polygon | 多边形,多个点组成的闭合区域 | 商圈范围、配送范围、行政区域 |
| MultiPoint | 多个点 | 多个商家位置、多个用户位置 |
Point类型的标准格式:
javascript
{
type: "Point",
coordinates: [经度, 纬度] // 注意:先经度,后纬度,顺序不能错!
}
高频避坑点 :GeoJSON的coordinates数组,第一个元素是经度,第二个是纬度,顺序绝对不能搞反,否则查询结果会完全错误!
1.1.2 创建地理空间索引
要使用地理空间查询,必须先创建地理空间索引,MongoDB支持两种地理空间索引:
- 2dsphere索引:用于地球表面的球面几何查询,支持所有GeoJSON类型,是生产环境的首选,适合真实的地理位置查询。
- 2d索引:用于平面几何查询,仅支持Point类型,适合游戏地图、平面坐标系等场景,生产环境极少使用。
javascript
// 创建2dsphere地理空间索引(推荐,生产环境首选)
db.merchants.createIndex({ location: "2dsphere" });
1.1.3 常用地理空间查询操作符
MongoDB提供了丰富的地理空间查询操作符,这里讲解企业开发中最常用的4种:
| 操作符 | 含义 | 业务场景 |
|---|---|---|
| $near | 查询距离指定点最近的文档,按距离升序排序 | 查找附近的商家、附近的用户 |
| $geoWithin | 查询完全在指定多边形区域内的文档 | 查找某个商圈内的商家、某个行政区域内的用户 |
| $geoIntersects | 查询与指定区域相交的文档 | 查找配送范围覆盖用户位置的商家 |
| $nearSphere | 与near类似,用于球面查询,2dsphere索引下near类似,用于球面查询,2dsphere索引下near类似,用于球面查询,2dsphere索引下near等价于$nearSphere | - |
业务示例1:$near 查询附近的商家
查询"用户当前位置(113.330, 23.127)附近3公里内的快餐商家,按距离从近到远排序,最多返回10个":
javascript
db.merchants.find({
location: {
$near: {
$geometry: {
type: "Point",
coordinates: [113.330, 23.127] // 用户当前位置:经度,纬度
},
$maxDistance: 3000, // 最大距离:3000米(3公里)
$minDistance: 0 // 最小距离:0米
}
},
category: "快餐" // 过滤条件:只查快餐商家
}).limit(10);
$maxDistance:单位是米,2dsphere索引下默认单位是米$near会自动按距离升序排序,无需额外调用sort()
业务示例2:$geoWithin 查询指定区域内的商家
查询"天河路商圈(多边形区域)内的所有商家":
javascript
db.merchants.find({
location: {
$geoWithin: {
$geometry: {
type: "Polygon",
coordinates: [
[ // 多边形的顶点坐标数组,必须闭合,第一个点和最后一个点相同
[113.320, 23.120],
[113.340, 23.120],
[113.340, 23.140],
[113.320, 23.140],
[113.320, 23.120] // 闭合多边形
]
]
}
}
}
});
业务示例3:计算两点之间的距离
使用聚合管道的$geoNear阶段,不仅能查询附近的文档,还能计算并返回文档到指定点的距离:
javascript
db.merchants.aggregate([
{
$geoNear: {
near: { type: "Point", coordinates: [113.330, 23.127] }, // 用户位置
distanceField: "distance", // 计算出的距离存入distance字段
maxDistance: 5000, // 最大5公里
query: { category: "快餐" }, // 过滤条件
spherical: true, // 球面计算
distanceMultiplier: 0.001 // 距离单位转换:米转公里
}
},
{
$project: {
_id: 0,
name: 1,
category: 1,
rating: 1,
distance: { $round: ["$distance", 2] } // 距离保留2位小数
}
}
]);
distanceField:指定存储距离的字段名distanceMultiplier:距离单位转换系数,0.001表示把米转换成公里
1.2 全文索引:轻量级全文检索方案
全文索引是MongoDB用于全文搜索的核心特性,支持对字符串字段进行全文检索,能实现关键词搜索、相关性排序、停用词过滤等功能,适合内容社区、商品搜索、文档搜索等轻量级全文搜索场景,无需额外引入Elasticsearch,降低系统复杂度。
1.2.1 创建全文索引
MongoDB支持两种全文索引:
- 单字段全文索引:给单个字符串字段创建全文索引
- 多字段全文索引:给多个字符串字段创建全文索引,一次查询可以搜索多个字段
javascript
// 创建多字段全文索引:同时给name、description、tags字段创建全文索引
db.goods.createIndex({ name: "text", description: "text", tags: "text" });
// 创建通配符全文索引:给集合中所有字符串字段创建全文索引(不推荐生产环境使用,索引过大)
db.goods.createIndex({ "$**": "text" });
1.2.2 全文搜索查询:text 与 search
使用$text操作符配合$search进行全文搜索,MongoDB会自动计算文档的相关性分数,按相关性降序排序。
业务示例1:基础全文搜索
搜索"包含'苹果'或'华为'关键词的商品":
javascript
db.goods.find(
{ $text: { $search: "苹果 华为" } }, // 空格表示OR关系,搜索包含苹果或华为的文档
{ score: { $meta: "textScore" } } // 投影:返回相关性分数
).sort({ score: { $meta: "textScore" } }); // 按相关性分数降序排序
$search中的关键词,空格表示OR关系,逗号也表示OR关系$meta: "textScore":获取文档的相关性分数,分数越高,相关性越强- 全文搜索会自动按相关性降序排序,也可以显式调用sort()
业务示例2:精确短语搜索
搜索"包含'iPhone 15 Pro'精确短语的商品":
javascript
db.goods.find(
{ $text: { $search: "\"iPhone 15 Pro\"" } }, // 双引号包裹表示精确短语搜索
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } });
- 用双引号
""包裹关键词,表示精确短语搜索,必须完全匹配短语的顺序和内容
业务示例3:排除关键词搜索
搜索"包含'手机'但不包含'苹果'的商品":
javascript
db.goods.find(
{ $text: { $search: "手机 -苹果" } }, // 减号-表示排除该关键词
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } });
- 减号
-开头的关键词,表示排除包含该关键词的文档
1.2.3 全文索引的注意事项与避坑指南
- 全文索引是轻量级方案:适合数据量在千万级以内、搜索需求不复杂的场景,如果数据量过亿、搜索需求复杂(比如分词、同义词、高亮),建议使用Elasticsearch
- 一个集合只能有一个全文索引:MongoDB限制一个集合最多只能创建一个全文索引,需要搜索多个字段时,创建一个多字段全文索引即可
- 停用词过滤:MongoDB会自动过滤停用词(比如"的"、"了"、"a"、"the"等无意义的词),搜索这些词不会返回结果
- 分词规则:MongoDB的全文索引默认使用空格、标点符号分词,对中文的分词支持不好(中文没有空格分隔),如果需要中文分词,建议使用Elasticsearch+IK分词器,或者MongoDB Atlas Search(MongoDB云服务的高级全文搜索功能)
1.3 正则查询:灵活的字符串匹配
MongoDB支持使用正则表达式进行字符串匹配查询,适合模糊搜索、前缀匹配、后缀匹配等场景,使用$regex操作符实现。
1.3.1 基础正则查询
javascript
// 1. 前缀匹配:查询用户名以"zhang"开头的用户(不区分大小写)
db.users.find({ username: { $regex: /^zhang/, $options: "i" } });
// 2. 后缀匹配:查询邮箱以"@example.com"结尾的用户
db.users.find({ email: { $regex: /@example\.com$/ } });
// 3. 包含匹配:查询商品名称包含"Pro"的商品(区分大小写)
db.goods.find({ name: { $regex: /Pro/ } });
// 4. 简写语法:可以直接用正则表达式,无需$regex
db.users.find({ username: /^zhang/i });
1.3.2 正则查询的索引命中规则
正则查询可以命中索引,但有严格的限制:
- 前缀匹配正则 :
/^xxx/或/^xxx/i,可以命中索引,性能最优 - 非前缀匹配正则 :
/xxx/、/xxx$/、/^.*xxx/,无法命中索引,会触发全集合扫描,性能极差,大表禁止使用
高频避坑点:
- 大表禁止使用非前缀匹配的正则查询,会导致全集合扫描,性能雪崩
- 如果需要复杂的模糊搜索,优先使用全文索引,不要用正则查询
$options: "i"表示不区分大小写,会导致索引失效,即使是前缀匹配也无法命中索引,尽量避免使用
1.4 游标操作:大数据量查询的优化
当查询结果集很大时(比如超过10万条),直接用find()会一次性返回所有结果,占用大量内存,甚至导致内存溢出。MongoDB使用游标(Cursor) 来处理大数据量查询,游标会分批返回结果,默认每批返回20条文档,避免一次性加载所有数据。
1.4.1 游标基础操作
javascript
// 1. 获取游标:find()方法返回的就是游标对象
let cursor = db.users.find();
// 2. 遍历游标:使用hasNext()和next()方法
while (cursor.hasNext()) {
let doc = cursor.next();
printjson(doc); // 打印文档
}
// 3. 简写遍历:使用forEach()方法
db.users.find().forEach(function(doc) {
printjson(doc);
});
// 4. 把游标转换为数组:仅适用于小结果集,大结果集会占用大量内存
let docs = db.users.find().toArray();
1.4.2 游标批量大小设置
可以通过batchSize()方法设置游标每批返回的文档数量,优化查询性能:
javascript
// 设置每批返回100条文档
db.users.find().batchSize(100).forEach(function(doc) {
printjson(doc);
});
- batchSize设置过大:会占用大量内存,甚至内存溢出
- batchSize设置过小:会增加网络IO次数,影响查询性能
- 建议根据文档大小调整,一般设置为100-1000之间
1.4.3 游标超时设置
MongoDB的游标默认10分钟后会自动超时关闭,避免游标长期占用资源。可以通过noCursorTimeout()方法禁用超时,但要注意手动关闭游标,避免资源泄漏:
javascript
// 禁用游标超时
let cursor = db.users.find().noCursorTimeout();
// 遍历完成后,手动关闭游标
cursor.close();
1.5 批量操作进阶:高效处理大量数据
上篇我们讲了insertMany()、updateMany()、deleteMany()批量操作,这一章我们讲解更高级的批量操作:bulkWrite(),它可以在一次请求中执行多种类型的操作(插入、更新、删除),性能更高,减少网络IO次数。
1.5.1 bulkWrite() 基础用法
bulkWrite()支持6种操作类型:
insertOne:插入单条文档updateOne:更新单条文档updateMany:批量更新文档deleteOne:删除单条文档deleteMany:批量删除文档replaceOne:替换单条文档
javascript
// 一次请求中执行多种操作:插入2个用户,更新1个用户,删除1个用户
db.users.bulkWrite([
// 操作1:插入用户
{
insertOne: {
document: {
username: "sunqi",
phone: "13800138005",
age: NumberInt(27),
is_vip: true,
create_time: new Date(),
update_time: new Date()
}
}
},
// 操作2:插入用户
{
insertOne: {
document: {
username: "zhouba",
phone: "13800138006",
age: NumberInt(24),
is_vip: false,
create_time: new Date(),
update_time: new Date()
}
}
},
// 操作3:更新用户
{
updateOne: {
filter: { username: "zhangsan" },
update: { $set: { age: NumberInt(27), update_time: new Date() } }
}
},
// 操作4:删除用户
{
deleteOne: {
filter: { username: "zhaoliu" }
}
}
], {
ordered: true // 有序执行,某条操作失败则停止后续操作,默认true
});
1.5.2 bulkWrite() 的优势
- 性能更高:一次网络请求执行多种操作,减少网络IO次数,性能比多次单条操作高10倍以上
- 原子性:ordered=true时,操作是有序的,某条操作失败会回滚之前的操作(注意:不是所有操作的原子性,是有序执行的回滚)
- 灵活性强:可以在一次请求中混合插入、更新、删除操作,适合复杂的批量数据处理场景
第二章 聚合管道高阶用法:实现复杂数据统计
在上篇中,我们学习了聚合管道的基础阶段:match、match、match、project、group、group、group、sort、limit、limit、limit、skip,这一章我们讲解聚合管道的高阶阶段,这些阶段能帮你实现非常复杂的数据统计、转换、关联查询,是MongoDB数据分析的核心利器。
前置说明
后续聚合管道示例,我们新增orders(订单集合)和order_items(订单明细集合),用于演示关联查询、数组拆分等高阶用法:
javascript
// 订单集合
db.createCollection("orders", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["order_no", "user_id", "total_amount", "status", "create_time"],
properties: {
order_no: { bsonType: "string" },
user_id: { bsonType: "objectId" },
total_amount: { bsonType: "decimal" },
status: { bsonType: "string", enum: ["pending", "paid", "shipped", "completed", "cancelled"] },
create_time: { bsonType: "date" }
}
}
}
});
// 订单明细集合
db.createCollection("order_items", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["order_id", "goods_id", "goods_name", "price", "num", "total_price"],
properties: {
order_id: { bsonType: "objectId" },
goods_id: { bsonType: "objectId" },
goods_name: { bsonType: "string" },
price: { bsonType: "decimal" },
num: { bsonType: "int" },
total_price: { bsonType: "decimal" }
}
}
}
});
// 插入测试数据(先获取之前插入的用户和商品的_id)
let user1 = db.users.findOne({ username: "zhangsan" })._id;
let user2 = db.users.findOne({ username: "lisi" })._id;
let goods1 = db.goods.findOne({ name: /iPhone 15 Pro/ })._id;
let goods2 = db.goods.findOne({ name: /华为Mate 60 Pro/ })._id;
let goods3 = db.goods.findOne({ name: /MacBook Pro/ })._id;
// 插入订单
let order1 = ObjectId();
let order2 = ObjectId();
let order3 = ObjectId();
db.orders.insertMany([
{
_id: order1,
order_no: "ORD20240417001",
user_id: user1,
total_amount: NumberDecimal("9999.00"),
status: "completed",
create_time: ISODate("2024-04-01T10:00:00Z")
},
{
_id: order2,
order_no: "ORD20240417002",
user_id: user1,
total_amount: NumberDecimal("7999.00"),
status: "paid",
create_time: ISODate("2024-04-10T14:00:00Z")
},
{
_id: order3,
order_no: "ORD20240417003",
user_id: user2,
total_amount: NumberDecimal("14999.00"),
status: "completed",
create_time: ISODate("2024-04-15T09:00:00Z")
}
]);
// 插入订单明细
db.order_items.insertMany([
{
order_id: order1,
goods_id: goods1,
goods_name: "iPhone 15 Pro Max 256GB 钛金属",
price: NumberDecimal("9999.00"),
num: NumberInt(1),
total_price: NumberDecimal("9999.00")
},
{
order_id: order2,
goods_id: goods2,
goods_name: "华为Mate 60 Pro 512GB 雅丹黑",
price: NumberDecimal("7999.00"),
num: NumberInt(1),
total_price: NumberDecimal("7999.00")
},
{
order_id: order3,
goods_id: goods3,
goods_name: "MacBook Pro 14英寸 M3 Pro",
price: NumberDecimal("14999.00"),
num: NumberInt(1),
total_price: NumberDecimal("14999.00")
}
]);
2.1 $lookup:集合关联查询,替代MySQL的JOIN
$lookup是MongoDB用于集合关联查询的核心阶段,等价于MySQL的LEFT JOIN,可以把两个集合关联起来,把右集合的匹配文档嵌入到左集合的文档中,实现一对多、多对一的关联查询。
2.1.1 基础语法
javascript
{
$lookup: {
from: "右集合名", // 要关联的右集合
localField: "左集合的关联字段", // 左集合中用于关联的字段
foreignField: "右集合的关联字段", // 右集合中用于关联的字段
as: "嵌入结果的字段名" // 关联结果嵌入到左集合文档的字段名,是一个数组
}
}
2.1.2 业务示例1:一对多关联(订单关联订单明细)
查询"所有订单,同时关联查询对应的订单明细":
javascript
db.orders.aggregate([
{
$lookup: {
from: "order_items", // 右集合:订单明细
localField: "_id", // 左集合关联字段:订单_id
foreignField: "order_id", // 右集合关联字段:订单明细的order_id
as: "items" // 嵌入结果的字段名:items,是一个数组
}
},
{
$project: {
_id: 0,
order_no: 1,
total_amount: 1,
status: 1,
create_time: 1,
"items.goods_name": 1,
"items.price": 1,
"items.num": 1
}
}
]);
as字段是一个数组,即使右集合只有一条匹配文档,也是数组- 如果右集合没有匹配文档,
as字段是空数组[]
2.1.3 业务示例2:多对一关联(订单关联用户)
查询"所有已完成的订单,同时关联查询下单用户的信息":
javascript
db.orders.aggregate([
{
$match: { status: "completed" } // 先过滤:只查已完成的订单
},
{
$lookup: {
from: "users", // 右集合:用户
localField: "user_id", // 左集合关联字段:订单的user_id
foreignField: "_id", // 右集合关联字段:用户的_id
as: "user_info" // 嵌入结果的字段名
}
},
{
$unwind: "$user_info" // 因为是多对一,user_info数组只有一个元素,用$unwind拆分为对象
},
{
$project: {
_id: 0,
order_no: 1,
total_amount: 1,
status: 1,
"user_info.username": 1,
"user_info.phone": 1
}
}
]);
- 这里用到了
$unwind阶段,我们下面会详细讲解
2.1.4 $lookup 的注意事项与避坑指南
-
$lookup 是 LEFT JOIN:等价于MySQL的LEFT JOIN,会返回左集合的所有文档,右集合没有匹配的话,as字段是空数组
-
关联字段类型必须一致:localField和foreignField的类型必须完全一致,比如ObjectId和ObjectId关联,String和String关联,类型不一致会导致关联失败,无法匹配到文档
-
关联字段建议加索引:右集合的foreignField字段建议加索引,否则$lookup会触发右集合的全表扫描,性能极差
-
避免大集合关联:两个大集合关联会占用大量内存,性能极差,尽量通过数据模型设计(嵌套文档)避免关联查询
-
**MongoDB 5.0+ 支持更强大的 lookup∗∗:MongoDB5.0+新增了'lookup**:MongoDB 5.0+新增了`lookup∗∗:MongoDB5.0+新增了'lookup
的pipeline`选项,可以在关联右集合时先对右集合进行过滤、投影,减少关联的数据量,提升性能:javascriptdb.orders.aggregate([ { $lookup: { from: "order_items", localField: "_id", foreignField: "order_id", as: "items", pipeline: [ // 对右集合先进行过滤和投影 { $match: { num: { $gte: 1 } } }, { $project: { goods_name: 1, price: 1, num: 1, _id: 0 } } ] } } ]);
2.2 $unwind:数组拆分
$unwind阶段用于把文档中的数组字段拆分成多个文档,每个数组元素对应一个文档,适合数组统计、数组元素过滤等场景。
2.2.1 基础语法
javascript
{
$unwind: {
path: "$数组字段名", // 要拆分的数组字段,必须以$开头
includeArrayIndex: "索引字段名", // 可选,把数组元素的索引存入指定字段
preserveNullAndEmptyArrays: true // 可选,是否保留数组为空、null、不存在的文档,默认false
}
}
// 简写语法:只指定path
{ $unwind: "$数组字段名" }
2.2.2 业务示例1:基础数组拆分
把用户的hobbies数组拆分成多个文档:
javascript
db.users.aggregate([
{ $match: { username: "zhangsan" } },
{ $unwind: "$hobbies" },
{ $project: { _id: 0, username: 1, hobbies: 1 } }
]);
执行结果:
javascript
{ "username" : "zhangsan", "hobbies" : "篮球" }
{ "username" : "zhangsan", "hobbies" : "读书" }
{ "username" : "zhangsan", "hobbies" : "编程" }
2.2.3 业务示例2:数组统计
统计"所有用户的爱好中,出现频率最高的3个爱好":
javascript
db.users.aggregate([
{ $unwind: "$hobbies" }, // 拆分hobbies数组
{
$group: {
_id: "$hobbies", // 按hobby分组
count: { $count: {} } // 统计每个hobby的出现次数
}
},
{ $sort: { count: -1 } }, // 按出现次数降序排序
{ $limit: 3 } // 取前3个
]);
2.2.4 $unwind 的注意事项
- 默认不保留空数组文档 :默认情况下,如果数组字段是空数组
[]、null、不存在,$unwind会过滤掉该文档,不会输出。如果需要保留,设置preserveNullAndEmptyArrays: true - 大数组拆分注意性能:如果数组元素很多(比如超过1000个),拆分后会生成大量文档,占用大量内存,性能极差,要注意控制数组长度
2.3 $bucket:分桶统计
$bucket阶段用于把文档按指定字段的值范围分成多个桶(bucket),每个桶对应一个范围,统计每个桶内的文档数量,适合价格区间统计、年龄区间统计、时间区间统计等场景。
2.3.1 基础语法
javascript
{
$bucket: {
groupBy: "$分组字段", // 用于分桶的字段
boundaries: [边界值1, 边界值2, 边界值3, ...], // 桶的边界值,必须是升序排列的数组
default: "默认桶名", // 可选,不在任何边界范围内的文档,放入默认桶
output: { // 可选,每个桶的输出字段,类似$group的输出
统计字段1: { 聚合函数: 字段 },
统计字段2: { 聚合函数: 字段 }
}
}
}
boundaries数组是左闭右开区间,比如[0, 20, 30, 50],对应的桶是[0,20)、[20,30)、[30,50)
2.3.2 业务示例:价格区间统计
统计"商品价格在0-5000、5000-10000、10000-20000区间的商品数量和平均价格":
javascript
db.goods.aggregate([
{
$bucket: {
groupBy: "$price", // 按价格分桶
boundaries: [NumberDecimal("0"), NumberDecimal("5000"), NumberDecimal("10000"), NumberDecimal("20000")], // 桶的边界
default: "其他", // 不在边界内的放入其他桶
output: {
count: { $count: {} }, // 统计每个桶的商品数量
avg_price: { $avg: "$price" } // 统计每个桶的平均价格
}
}
}
]);
2.4 $facet:多面聚合,一次查询多个统计结果
$facet是MongoDB聚合管道的"瑞士军刀",可以在一次聚合管道中,同时执行多个独立的子管道,一次查询返回多个维度的统计结果,大幅减少网络IO次数,提升性能,适合电商首页统计、仪表盘统计等需要多个统计结果的场景。
2.4.1 基础语法
javascript
{
$facet: {
统计结果名1: [子管道1的阶段数组],
统计结果名2: [子管道2的阶段数组],
统计结果名3: [子管道3的阶段数组],
...
}
}
2.4.2 业务示例:电商首页多维度统计
一次查询返回"商品总数、订单总数、已完成订单总金额、用户总数、最近30天的订单趋势":
javascript
db.orders.aggregate([
{
$facet: {
// 统计1:商品总数(从goods集合统计,这里用$lookup关联)
goods_count: [
{ $count: "total" } // 这里简化,实际可以关联goods集合
],
// 统计2:订单总数
order_count: [
{ $count: "total" }
],
// 统计3:已完成订单总金额
completed_total_amount: [
{ $match: { status: "completed" } },
{ $group: { _id: null, total: { $sum: "$total_amount" } } }
],
// 统计4:用户总数(关联users集合)
user_count: [
{ $group: { _id: "$user_id" } },
{ $count: "total" }
],
// 统计5:最近30天的订单趋势(按天分组)
last_30_days_trend: [
{
$match: {
create_time: {
$gte: new Date(new Date() - 30 * 24 * 60 * 60 * 1000)
}
}
},
{
$group: {
_id: { $dateToString: { format: "%Y-%m-%d", date: "$create_time" } },
order_count: { $count: {} },
total_amount: { $sum: "$total_amount" }
}
},
{ $sort: { _id: 1 } }
]
}
}
]);
$facet的每个子管道是独立的,互不影响- 一次聚合查询返回所有统计结果,性能比多次单独查询高很多
2.5 窗口函数:MongoDB 5.0+ 高级统计特性
窗口函数是MongoDB 5.0+推出的高级特性,等价于MySQL的窗口函数,可以在不分组的情况下,对一组相关的文档进行计算,比如排名、累计求和、移动平均、同比环比等,是数据分析的利器。
窗口函数通过$setWindowFields阶段实现,常用的窗口函数如下:
| 窗口函数 | 含义 |
|---|---|
| $rank | 跳跃排名,值相同排名相同,后续排名跳跃 |
| $denseRank | 连续排名,值相同排名相同,后续排名连续 |
| $rowNumber | 连续排名,即使值相同,排名也不重复 |
| $sum | 累计求和 |
| $avg | 移动平均 |
| $max | 窗口内最大值 |
| $min | 窗口内最小值 |
业务示例:商品价格排名
使用$rowNumber给商品按价格降序排名:
javascript
db.goods.aggregate([
{
$setWindowFields: {
sortBy: { price: -1 }, // 按价格降序排序
output: {
price_rank: { $rowNumber: {} } // 排名存入price_rank字段
}
}
},
{
$project: {
_id: 0,
name: 1,
price: 1,
category: 1,
price_rank: 1
}
}
]);
业务示例:分组排名
按商品分类分组,每个分类内按价格降序排名:
javascript
db.goods.aggregate([
{
$setWindowFields: {
partitionBy: "$category", // 按category分组
sortBy: { price: -1 },
output: {
category_price_rank: { $denseRank: {} }
}
}
},
{
$project: {
_id: 0,
name: 1,
price: 1,
category: 1,
category_price_rank: 1
}
}
]);
partitionBy:指定分组字段,类似GROUP BY,但不会合并文档,每个文档都会保留
第三章 数据模型设计进阶:从源头优化性能
在上篇中,我们学习了数据模型设计的基础规范,这一章我们讲解进阶内容:嵌入式模型vs引用式模型的选择、分桶设计、海量数据模型设计、反范式设计的最佳实践。数据模型设计是MongoDB性能优化的源头,好的模型设计能让性能提升10倍以上,远胜于后期的索引优化和SQL优化。
3.1 嵌入式模型 vs 引用式模型:核心选择原则
MongoDB的数据模型设计,核心就是选择嵌入式模型(Embedded Model) 还是引用式模型(Reference Model),这是MongoDB和关系型数据库最大的区别之一。
3.1.1 嵌入式模型
嵌入式模型,就是把关联数据直接嵌套在主文档中,用嵌套文档或数组存储,是MongoDB推荐的优先选择。
示例:订单+订单明细的嵌入式模型
javascript
{
_id: ObjectId("xxx"),
order_no: "ORD20240417001",
user_id: ObjectId("yyy"),
total_amount: NumberDecimal("9999.00"),
status: "completed",
// 订单明细直接嵌套在订单文档中,用数组存储
items: [
{
goods_id: ObjectId("zzz"),
goods_name: "iPhone 15 Pro Max",
price: NumberDecimal("9999.00"),
num: NumberInt(1),
total_price: NumberDecimal("9999.00")
}
],
create_time: new Date()
}
嵌入式模型的优势:
- 查询性能极高:一次查询就能获取主文档和所有关联数据,无需关联查询,避免了$lookup的性能开销
- 原子性更新:更新主文档和关联数据是原子性的,无需事务就能保证数据一致性
- 数据局部性好:关联数据存储在一起,磁盘IO次数少,缓存命中率高
嵌入式模型的劣势:
- 文档体积可能过大:如果嵌套数组的元素过多,会导致单文档体积超过16MB的限制,MongoDB会报错
- 嵌套数据更新不便:如果嵌套数据需要频繁单独更新,嵌入式模型会比较麻烦
- 嵌套数据查询不便:如果需要单独查询嵌套数据,嵌入式模型不如引用式模型灵活
3.1.2 引用式模型
引用式模型,就是把关联数据存储在单独的集合中,主文档只存储关联数据的_id,类似关系型数据库的外键,需要关联查询时用$lookup。
示例:订单+订单明细的引用式模型
javascript
// 订单文档
{
_id: ObjectId("xxx"),
order_no: "ORD20240417001",
user_id: ObjectId("yyy"),
total_amount: NumberDecimal("9999.00"),
status: "completed",
create_time: new Date()
}
// 订单明细文档,单独存储在order_items集合中
{
_id: ObjectId("aaa"),
order_id: ObjectId("xxx"), // 引用订单的_id
goods_id: ObjectId("zzz"),
goods_name: "iPhone 15 Pro Max",
price: NumberDecimal("9999.00"),
num: NumberInt(1),
total_price: NumberDecimal("9999.00")
}
引用式模型的优势:
- 文档体积小:关联数据单独存储,主文档体积小,不会超过16MB限制
- 关联数据更新灵活:关联数据可以单独更新,不影响主文档
- 关联数据查询灵活:可以单独查询关联数据,无需查询主文档
引用式模型的劣势:
- 查询性能低:需要关联查询,使用$lookup,性能不如嵌入式模型
- 无原子性保证:更新主文档和关联数据不是原子性的,需要事务保证数据一致性
- 多次查询:如果需要获取主文档和关联数据,需要多次查询或使用$lookup,增加网络IO
3.1.3 核心选择原则
选择嵌入式模型还是引用式模型,核心看三个维度:数据关系类型、数据更新频率、数据查询模式,具体原则如下:
| 场景 | 推荐模型 |
|---|---|
| 一对一关系(用户-用户详情) | 嵌入式模型,直接嵌套 |
| 一对多关系,"多"的数量少(<100),且经常一起查询(订单-订单明细) | 嵌入式模型,用数组嵌套 |
| 一对多关系,"多"的数量多(>100),或很少一起查询(用户-订单) | 引用式模型,单独存储,用$lookup关联 |
| 多对多关系(学生-课程) | 引用式模型,用数组存储关联_id,或中间集合 |
| 关联数据频繁单独更新 | 引用式模型 |
| 关联数据经常和主数据一起查询 | 嵌入式模型 |
高频避坑点:
- 不要盲目模仿关系型数据库的三范式,MongoDB优先推荐嵌入式模型,减少关联查询
- 嵌套数组的元素数量不要超过1000个,单文档体积不要超过1MB,否则会影响查询性能
- 如果不确定用哪种模型,优先用嵌入式模型,后续如果遇到问题再考虑引用式模型
3.2 分桶设计:海量时序数据的优化方案
分桶设计(Bucket Pattern)是MongoDB针对海量时序数据(比如日志、监控数据、物联网设备数据)的优化方案,核心思想是把一段时间内的数据(比如1小时、1天)打包存储在一个"桶"文档中,而不是每条数据一个文档,大幅减少文档数量,提升查询性能,减少索引大小。
3.2.1 传统时序数据模型的问题
传统的时序数据模型,每条数据一个文档:
javascript
// 传统模型:每条监控数据一个文档
{
_id: ObjectId("xxx"),
device_id: "device_001",
temperature: 25.5,
humidity: 60.2,
create_time: ISODate("2024-04-17T10:00:00Z")
},
{
_id: ObjectId("yyy"),
device_id: "device_001",
temperature: 25.6,
humidity: 60.3,
create_time: ISODate("2024-04-17T10:00:01Z")
}
问题:
- 文档数量极多:如果设备每秒上报一次数据,一天就有86400条数据,一个月就是259万条,一年就是3亿条
- 索引大小极大:_id索引、device_id索引、create_time索引都会非常大,占用大量内存和磁盘
- 查询性能差:查询一天的数据,需要扫描86400条文档,性能极差
3.2.2 分桶设计模型
分桶设计,把一个小时的数据打包在一个桶文档中:
javascript
// 分桶模型:一个小时的数据一个桶文档
{
_id: ObjectId("xxx"),
device_id: "device_001",
hour: ISODate("2024-04-17T10:00:00Z"), // 桶的时间范围:10点-11点
count: 120, // 桶内的数据条数
measurements: [ // 数据数组,存储这个小时的所有监控数据
{ time: ISODate("2024-04-17T10:00:00Z"), temperature: 25.5, humidity: 60.2 },
{ time: ISODate("2024-04-17T10:00:01Z"), temperature: 25.6, humidity: 60.3 },
// ... 省略其他数据
],
start_time: ISODate("2024-04-17T10:00:00Z"), // 桶内第一条数据的时间
end_time: ISODate("2024-04-17T10:01:59Z") // 桶内最后一条数据的时间
}
分桶设计的优势:
- 文档数量大幅减少:一个小时的数据一个文档,一天只有24个文档,一个月720个,一年8760个,文档数量减少了3万倍以上
- 索引大小大幅减少:索引只需要索引桶文档,索引大小极小,内存占用低
- 查询性能大幅提升:查询一个小时的数据,只需要查询1个文档,查询一天的数据,只需要查询24个文档,性能提升了上万倍
- 写入性能提升:写入时只需要更新桶文档的measurements数组,无需插入新文档,减少索引更新次数
3.2.3 分桶设计的最佳实践
- 桶的时间范围选择:根据数据上报频率选择,一般选择1分钟、5分钟、1小时、1天,原则是桶内的measurements数组元素数量不超过1000个
- 预分配桶:提前创建未来的桶文档,避免写入时创建桶文档的开销,提升写入性能
- 桶内数据压缩:如果数据量很大,可以对桶内的measurements数组进行压缩,减少文档体积
- 冷热数据分离:历史数据(冷数据)可以归档到单独的集合或存储引擎,当前数据(热数据)保留在主集合中
3.3 反范式设计:合理冗余,提升性能
反范式设计(Denormalization),就是通过合理冗余数据,减少关联查询,提升查询性能,是MongoDB数据模型设计的常用手段。关系型数据库强调三范式,避免数据冗余,而MongoDB优先推荐反范式设计,因为MongoDB的写入性能很高,冗余数据的更新成本远低于关联查询的性能成本。
3.3.1 反范式设计的常用手段
-
冗余字段 :把关联集合的字段冗余到主集合中,比如订单文档冗余用户名、商品名称,避免关联查询用户表和商品表
javascript// 反范式设计:订单文档冗余用户名和商品名称 { _id: ObjectId("xxx"), order_no: "ORD20240417001", user_id: ObjectId("yyy"), username: "zhangsan", // 冗余用户名,避免关联用户表 total_amount: NumberDecimal("9999.00"), items: [ { goods_id: ObjectId("zzz"), goods_name: "iPhone 15 Pro Max", // 冗余商品名称,避免关联商品表 price: NumberDecimal("9999.00"), num: NumberInt(1) } ] } -
预计算字段:把需要频繁计算的结果冗余到文档中,比如订单文档冗余商品数量、订单明细的小计金额,避免每次查询都计算
-
嵌套历史数据:把历史数据嵌套在主文档中,比如用户文档嵌套最近10个订单,避免查询订单表
3.3.2 反范式设计的注意事项
- 冗余字段必须是很少更新的静态数据:比如用户名、商品名称,这些字段很少更新,冗余的更新成本低;如果冗余字段频繁更新,会导致大量的更新操作,反而降低性能
- 必须保证冗余数据的一致性:更新主数据时,必须同步更新冗余数据,可以通过事务、MongoDB Change Streams(变更流)来保证一致性
- 不要过度冗余:只冗余查询频率高的字段,不要冗余所有字段,否则会导致文档体积过大,反而影响性能
- 权衡利弊:反范式设计是"以空间换时间",要权衡存储空间和查询性能,选择最优方案
第四章 MongoDB事务原理与实战:高并发数据一致性的保障
在MongoDB 4.0之前,MongoDB不支持多文档事务,只能保证单文档的原子性,这是很多企业不选择MongoDB的核心原因。MongoDB 4.0版本之后,支持了副本集多文档事务 ,4.2版本之后支持了分片集群分布式事务,补齐了MongoDB的最后一块短板,现在MongoDB已经可以用于核心金融、支付等需要强事务的场景。
4.1 事务的核心概念
MongoDB的事务和关系型数据库的事务类似,遵循ACID特性:
- 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败回滚
- 一致性(Consistency):事务执行前后,数据的完整性约束不会被破坏
- 隔离性(Isolation):多个事务并发执行时,互相之间是隔离的
- 持久性(Durability):事务一旦提交成功,对数据的修改就会永久生效
MongoDB的事务分为两类:
- 单文档事务:MongoDB天生支持单文档的原子性,更新单个文档(包括更新嵌套文档、数组)是原子性的,无需显式开启事务
- 多文档事务:MongoDB 4.0+支持副本集多文档事务,4.2+支持分片集群分布式事务,需要显式开启、提交、回滚
4.2 副本集多文档事务实战
副本集多文档事务,是指在副本集环境下,对多个文档、多个集合的操作,组成一个原子性的事务,要么全部成功,要么全部回滚。
4.2.1 前置条件
使用副本集多文档事务,必须满足以下条件:
- MongoDB版本 >= 4.0
- 必须是副本集环境(单机MongoDB不支持事务,生产环境也必须用副本集)
- 事务中的操作必须在同一个数据库中(MongoDB 4.2+支持跨数据库事务)
- 事务中的集合必须存在(不能在事务中创建集合)
- 事务中的操作不能是DDL操作(创建集合、创建索引等)
4.2.2 核心语法
MongoDB的事务语法和关系型数据库类似,核心步骤是:开启会话 → 开启事务 → 执行操作 → 提交事务/回滚事务。
javascript
// 1. 开启会话(Session)
let session = db.getMongo().startSession();
try {
// 2. 开启事务
session.startTransaction();
// 3. 在会话中执行事务操作
// 注意:所有操作必须通过session.getDatabase()获取数据库,通过session.getCollection()获取集合
let sessionDB = session.getDatabase("user_db");
// 操作1:扣减商品库存(假设goods集合有stock字段)
sessionDB.goods.updateOne(
{ _id: goods1, stock: { $gte: 1 } },
{ $inc: { stock: -1 } }
);
// 操作2:创建订单
let newOrder = {
order_no: "ORD20240417004",
user_id: user1,
total_amount: NumberDecimal("9999.00"),
status: "paid",
create_time: new Date()
};
sessionDB.orders.insertOne(newOrder);
// 操作3:创建订单明细
sessionDB.order_items.insertOne({
order_id: newOrder._id,
goods_id: goods1,
goods_name: "iPhone 15 Pro Max",
price: NumberDecimal("9999.00"),
num: NumberInt(1),
total_price: NumberDecimal("9999.00")
});
// 4. 提交事务
session.commitTransaction();
print("事务提交成功!");
} catch (error) {
// 5. 发生异常,回滚事务
print("事务执行失败,回滚事务:" + error);
session.abortTransaction();
} finally {
// 6. 关闭会话
session.endSession();
}
4.2.3 事务的隔离级别
MongoDB的事务支持两种隔离级别:
- 读已提交(Read Committed):默认隔离级别,一个事务只能读到其他事务已经提交的数据
- 快照读(Snapshot):MongoDB 5.0+支持,事务中的所有读操作都读取事务开始时的快照,避免不可重复读和幻读
设置隔离级别:
javascript
session.startTransaction({
readConcern: { level: "snapshot" }, // 读关注:快照读
writeConcern: { w: "majority" }, // 写关注:大多数节点确认
readPreference: "primary" // 读偏好:只从主节点读
});
4.3 分片集群分布式事务实战
MongoDB 4.2+支持分片集群分布式事务,可以在分片集群环境下,对跨分片的多个文档、多个集合的操作,组成一个原子性的事务,底层基于两阶段提交(2PC)实现。
分片集群分布式事务的语法和副本集事务完全一致,只需要连接到mongos路由节点即可,无需修改代码,MongoDB会自动处理跨分片的事务协调。
4.4 事务的最佳实践与避坑指南
- 事务粒度尽可能小:事务中的操作要尽可能少,执行时间要尽可能短,避免长事务。长事务会占用大量资源,导致锁等待,影响并发性能
- 事务中的操作尽可能命中索引:事务中的查询、更新操作必须命中索引,否则会触发全表扫描,导致事务执行时间过长
- 避免在事务中执行耗时操作:禁止在事务中调用外部接口、等待用户输入、执行大查询,避免长事务
- 合理设置超时时间 :事务默认超时时间是60秒,可以通过
maxCommitTimeMS设置,避免事务长时间占用资源 - 优先使用单文档原子性:如果能通过单文档更新实现业务需求,优先使用单文档原子性,不要使用多文档事务,单文档原子性性能更高,没有事务开销
- 写关注设置为majority :生产环境中,事务的writeConcern必须设置为
w: "majority",保证事务提交后,大多数副本节点都已写入,避免数据丢失 - 读偏好设置为primary :事务中的读操作必须从主节点读,设置
readPreference: "primary",避免从从节点读导致的数据不一致
第五章 索引进阶与性能优化:千万级数据下的查询优化
在上篇中,我们学习了索引的基础,这一章我们讲解索引进阶内容:索引底层原理、索引失效场景、慢查询分析与优化、执行计划深度解析、索引维护最佳实践。索引优化是MongoDB性能优化最核心、性价比最高的手段,正确的索引能让查询性能提升上千倍。
5.1 索引底层原理:B+树
MongoDB的索引底层数据结构和MySQL一样,都是B+树,原理完全一致,我们在MySQL系列中已经详细讲解过B+树的原理,这里简单回顾一下核心特性:
- B+树是多路平衡搜索树,树高极低,千万级数据树高仅3-4层,查询数据最多只需要3次磁盘IO
- 非叶子节点只存储键值和指针,不存储数据,每个节点可以存储上千个键值,树高极低
- 叶子节点存储完整的索引数据,所有叶子节点通过双向链表连接,范围查询性能极高
- MongoDB的_id索引是聚簇索引吗?不是,MongoDB的所有索引都是二级索引,数据存储在WiredTiger存储引擎的B树中,索引和数据是分开存储的,这一点和MySQL InnoDB不同
5.2 索引失效的十大高频场景
和MySQL一样,MongoDB也有索引失效的场景,这里总结生产环境中最常见的10大索引失效场景,必须牢记。
我们基于users集合的复合索引{ is_vip: 1, age: -1, username: 1 }讲解:
-
违背复合索引前缀匹配原则
javascript// 失效:不包含第一个前缀字段is_vip db.users.find({ age: 25, username: "zhangsan" }); -
索引字段使用函数运算/类型转换
javascript// 失效:对age字段使用$add函数 db.users.find({ $expr: { $eq: [{ $add: ["$age", 1] }, 26] } }); // 失效:类型不匹配,age是Int,查询用String db.users.find({ age: "25" }); -
使用ne、ne、ne、not、$nin反向查询
javascript// 大概率失效:$ne反向查询 db.users.find({ age: { $ne: 25 } }); -
使用$exists查询字段不存在的文档
javascript// 大概率失效:$exists: false db.users.find({ email: { $exists: false } }); -
使用$regex非前缀匹配正则
javascript// 失效:非前缀匹配正则 db.users.find({ username: { $regex: /san/ } }); // 失效:不区分大小写的正则,即使是前缀匹配也失效 db.users.find({ username: { $regex: /^zhang/, $options: "i" } }); -
使用$or连接非索引字段
javascript// 失效:$or连接的字段有一个没有索引 db.users.find({ $or: [{ username: "zhangsan" }, { address: "北京" }] }); -
复合索引中,范围查询字段后面的字段无法命中索引
javascript// 只有is_vip和age字段命中索引,username字段无法命中 db.users.find({ is_vip: true, age: { $gt: 25 }, username: "zhangsan" }); -
查询的字段选择性极低
javascript// 大概率失效:is_vip字段只有true/false两个值,选择性极低 db.users.find({ is_vip: true }); -
使用$where操作符
javascript// 失效:$where操作符会触发全表扫描,性能极差,禁止使用 db.users.find({ $where: "this.age > 25" }); -
索引碎片化严重
- 集合经过大量的插入、更新、删除后,索引会出现碎片化,导致索引效率降低,需要重建索引
5.3 慢查询分析与优化
慢查询是生产环境最常见的性能问题,这一节我们讲解如何开启慢查询日志、分析慢查询、优化慢查询。
5.3.1 开启慢查询日志
MongoDB的慢查询日志默认是关闭的,需要手动开启:
javascript
// 1. 查看当前慢查询配置
db.getProfilingStatus();
// 2. 开启慢查询日志,设置慢查询阈值为100ms
// profiling级别:0-关闭,1-记录慢查询,2-记录所有操作
db.setProfilingLevel(1, { slowms: 100 });
// 3. 查看慢查询日志
db.system.profile.find().sort({ ts: -1 }).limit(10);
5.3.2 分析慢查询:executionStats
使用explain("executionStats")分析慢查询的执行计划,查看查询是否命中索引、扫描的文档数、执行时间等:
javascript
// 分析慢查询的执行计划
db.users.find({ is_vip: true, age: { $gt: 25 } }).explain("executionStats");
核心分析字段:
- executionStats.executionSuccess:查询是否执行成功
- executionStats.nReturned:查询返回的文档数
- executionStats.totalKeysExamined:索引扫描的条目数
- executionStats.totalDocsExamined:文档扫描的条目数
- executionStats.executionTimeMillis:查询执行时间(毫秒)
- queryPlanner.winningPlan.inputStage.stage :查询阶段,核心判断指标
IXSCAN:命中索引,索引扫描,性能最优COLLSCAN:全集合扫描,没有命中索引,性能极差,必须优化
优化目标:
winningPlan.inputStage.stage必须是IXSCAN,绝对不能出现COLLSCANnReturned≈totalKeysExamined≈totalDocsExamined,三者越接近,索引效率越高executionTimeMillis越小越好,生产环境建议控制在100ms以内
5.3.3 慢查询优化案例
案例:查询"广东省的VIP用户,按年龄降序排序",执行时间500ms,全集合扫描。
javascript
// 慢查询SQL
db.users.find({ "address.province": "广东", is_vip: true }).sort({ age: -1 });
// 分析执行计划:COLLSCAN,全集合扫描
db.users.find({ "address.province": "广东", is_vip: true }).sort({ age: -1 }).explain("executionStats");
优化方案 :创建复合索引{ "address.province": 1, is_vip: 1, age: -1 },覆盖查询条件和排序字段。
javascript
// 创建复合索引
db.users.createIndex({ "address.province": 1, is_vip: 1, age: -1 });
// 再次查询,执行时间降到10ms以内
db.users.find({ "address.province": "广东", is_vip: true }).sort({ age: -1 });
// 分析执行计划:IXSCAN,命中索引
db.users.find({ "address.province": "广东", is_vip: true }).sort({ age: -1 }).explain("executionStats");
5.4 索引维护最佳实践
-
定期查看索引使用情况 :使用
$indexStats查看索引的使用情况,删除长期未使用的索引,减少写入开销javascript// 查看索引使用情况 db.users.aggregate([{ $indexStats: {} }]); -
定期重建碎片化严重的索引 :集合经过大量插入、更新、删除后,索引会出现碎片化,需要重建索引
javascript// 重建索引 db.users.reIndex(); // 重建所有索引 db.users.dropIndex("index_name"); // 删除索引 db.users.createIndex({ ... }); // 重新创建索引 -
避免在业务高峰期创建/删除索引:创建/删除索引会占用大量资源,影响业务性能,应该在业务低峰期执行
-
索引数量控制在5个以内:索引越多,写入性能越差,单表索引数量控制在5个以内,避免冗余、重复索引
-
复合索引遵循前缀匹配原则:高频等值查询字段放在最前面,范围查询字段放在后面,查询和排序字段尽量加入同一个复合索引
第六章 副本集架构:MongoDB高可用的基础
副本集(Replica Set)是MongoDB的高可用架构,由一组MongoDB节点组成,包含一个主节点(Primary)和多个从节点(Secondary),主节点负责写操作,从节点负责读操作,主节点故障时,从节点会自动选举出新的主节点,实现故障自动转移,保证业务高可用。生产环境中,MongoDB必须部署为副本集,绝对不能使用单机MongoDB。
6.1 副本集的核心概念
6.1.1 副本集的节点类型
副本集包含三种节点类型:
- 主节点(Primary) :
- 副本集中只有一个主节点
- 负责处理所有的写操作(INSERT、UPDATE、DELETE)
- 负责处理读操作(默认读偏好是primary)
- 把写操作记录到oplog(操作日志)中
- 从节点(Secondary) :
- 副本集中可以有多个从节点,推荐至少2个从节点,形成一主两从架构
- 从主节点同步oplog,重放oplog,保持和主节点数据一致
- 可以处理读操作,实现读写分离
- 主节点故障时,参与选举,投票选出新的主节点
- 仲裁节点(Arbiter) :
- 仲裁节点不存储数据,不参与读写操作,只参与选举投票
- 用于保证副本集节点数为奇数,避免脑裂(Split Brain)
- 推荐在节点数为偶数时添加仲裁节点,比如一主一从,添加一个仲裁节点,形成一主一从一仲裁架构
6.1.2 副本集的核心特性
- 高可用:主节点故障时,从节点会自动选举出新的主节点,故障转移时间一般在10-30秒,业务几乎无感知
- 数据冗余:从节点同步主节点的数据,保证数据有多份副本,避免数据丢失
- 读写分离:主节点负责写,从节点负责读,分散读压力,提升读并发能力
- 数据备份:可以从从节点备份数据,不影响主节点的业务性能
6.2 副本集的搭建步骤(一主两从)
这里我们讲解在Linux环境下,搭建一主两从副本集的详细步骤,生产环境推荐使用一主两从架构。
6.2.1 前置准备
-
准备3台服务器(或3个虚拟机),IP地址分别为:
- node1: 192.168.1.10(主节点)
- node2: 192.168.1.11(从节点)
- node3: 192.168.1.12(从节点)
-
在3台服务器上都安装MongoDB 7.0(安装步骤参考上篇)
-
关闭3台服务器的防火墙,或开放27017端口
-
在3台服务器上创建数据目录和日志目录:
bashmkdir -p /data/mongodb/data mkdir -p /data/mongodb/log chown -R mongod:mongod /data/mongodb
6.2.2 修改配置文件
在3台服务器上修改MongoDB配置文件/etc/mongod.conf:
yaml
# 数据目录
storage:
dbPath: /data/mongodb/data
journal:
enabled: true
# 日志配置
systemLog:
destination: file
logAppend: true
path: /data/mongodb/log/mongod.log
# 网络配置
net:
port: 27017
bindIp: 0.0.0.0 # 允许所有IP访问,生产环境建议限制为内网IP
# 副本集配置
replication:
replSetName: "rs0" # 副本集名称,所有节点必须一致
# 安全配置(生产环境必须开启)
security:
authorization: enabled # 开启身份验证
keyFile: /data/mongodb/keyfile # 副本集节点间认证的keyfile
6.2.3 创建keyfile(节点间认证)
在node1上创建keyfile,然后复制到node2和node3:
bash
# 在node1上创建keyfile
openssl rand -base64 756 > /data/mongodb/keyfile
chmod 400 /data/mongodb/keyfile
chown mongod:mongod /data/mongodb/keyfile
# 复制keyfile到node2和node3
scp /data/mongodb/keyfile root@192.168.1.11:/data/mongodb/
scp /data/mongodb/keyfile root@192.168.1.12:/data/mongodb/
# 在node2和node3上修改keyfile权限
chmod 400 /data/mongodb/keyfile
chown mongod:mongod /data/mongodb/keyfile
6.2.4 启动所有节点
在3台服务器上启动MongoDB:
bash
systemctl start mongod
systemctl enable mongod
6.2.5 初始化副本集
连接到node1,初始化副本集:
javascript
// 连接到node1
mongosh --host 192.168.1.10 --port 27017
// 初始化副本集
rs.initiate({
_id: "rs0", // 副本集名称,和配置文件一致
members: [
{ _id: 0, host: "192.168.1.10:27017" }, // node1
{ _id: 1, host: "192.168.1.11:27017" }, // node2
{ _id: 2, host: "192.168.1.12:27017" } // node3
]
});
// 查看副本集状态
rs.status();
- 初始化后,node1会成为主节点,node2和node3会成为从节点
rs.status()可以查看副本集的状态,stateStr字段显示节点的角色
6.2.6 创建管理员用户
在主节点上创建管理员用户:
javascript
// 切换到admin数据库
use admin;
// 创建管理员用户
db.createUser({
user: "root",
pwd: "Root@123456",
roles: [{ role: "root", db: "admin" }]
});
// 认证
db.auth("root", "Root@123456");
6.3 副本集的读写分离
副本集可以实现读写分离,主节点负责写,从节点负责读,分散读压力,提升读并发能力。通过设置读偏好(Read Preference) 来实现。
6.3.1 读偏好类型
MongoDB支持5种读偏好:
- primary:默认读偏好,所有读操作都从主节点读,保证数据一致性
- primaryPreferred:优先从主节点读,主节点不可用时,从从节点读
- secondary:所有读操作都从从节点读,数据可能有延迟(主从延迟)
- secondaryPreferred:优先从从节点读,从节点不可用时,从主节点读
- nearest:从网络延迟最低的节点读,不管是主节点还是从节点
6.3.2 设置读偏好
在代码中设置读偏好,以Node.js为例:
javascript
const { MongoClient, ReadPreference } = require('mongodb');
// 连接副本集,设置读偏好为secondaryPreferred
const client = new MongoClient('mongodb://root:Root@123456@192.168.1.10:27017,192.168.1.11:27017,192.168.1.12:27017/user_db?replicaSet=rs0&readPreference=secondaryPreferred');
// 或者在查询时设置读偏好
const db = client.db('user_db');
const users = await db.collection('users')
.find({ is_vip: true })
.readPreference(ReadPreference.SECONDARY_PREFERRED)
.toArray();
6.3.3 读写分离的注意事项
- 主从延迟问题:从节点同步主节点的数据有延迟,从从节点读可能读到旧数据,对数据一致性要求高的场景,必须从主节点读
- 读偏好选择 :
- 对数据一致性要求极高的场景(比如支付、订单查询):使用primary
- 对数据一致性要求不高的场景(比如商品列表、内容社区):使用secondaryPreferred
- 监控主从延迟:定期监控主从延迟,延迟过高时,及时从主节点读
6.4 副本集的故障自动转移
副本集的核心优势就是故障自动转移,当主节点故障时,从节点会自动选举出新的主节点,业务几乎无感知。
6.4.1 故障转移的流程
- 故障检测:从节点通过心跳检测,发现主节点不可达
- 选举触发:从节点发起选举,投票给自己
- 投票选举:所有节点参与投票,获得大多数节点(>n/2)投票的节点成为新的主节点
- 角色切换:新的主节点升级为主,其他从节点从新的主节点同步数据
- 恢复服务:新的主节点开始处理写操作,业务恢复
6.4.2 测试故障转移
我们可以手动模拟主节点故障,测试故障转移:
javascript
// 1. 查看副本集状态,确认node1是主节点
rs.status();
// 2. 关闭node1的MongoDB服务(模拟主节点故障)
systemctl stop mongod
// 3. 连接到node2或node3,查看副本集状态
rs.status();
// 可以看到,node2或node3已经成为新的主节点
// 4. 重启node1的MongoDB服务
systemctl start mongod
// 5. 再次查看副本集状态,node1会成为从节点,从新的主节点同步数据
rs.status();
6.5 副本集的数据备份与恢复
生产环境中,必须定期备份数据,避免数据丢失。副本集环境下,推荐从从节点备份数据,不影响主节点的业务性能。
6.5.1 使用mongodump备份
bash
# 从从节点备份数据
mongodump --host rs0/192.168.1.11:27017 --username root --password Root@123456 --authenticationDatabase admin --out /data/backup/$(date +%Y%m%d)
6.5.2 使用mongorestore恢复
bash
# 恢复数据
mongorestore --host rs0/192.168.1.10:27017 --username root --password Root@123456 --authenticationDatabase admin /data/backup/20240417
第七章 分片集群架构:MongoDB水平扩展的核心
当单副本集的数据量和读写压力达到瓶颈时(一般单副本集数据量超过5TB,或写压力超过1万QPS),就需要使用分片集群(Sharded Cluster)。分片集群是MongoDB的水平扩展架构,把数据分散存储在多个分片(Shard)中,每个分片是一个副本集,实现数据的水平扩展,轻松支撑PB级海量数据和百万级QPS。
7.1 分片集群的核心概念
7.1.1 分片集群的组成
分片集群由三个核心组件组成:
- 分片(Shard) :
- 每个分片是一个副本集,负责存储一部分数据
- 推荐至少3个分片,生产环境根据数据量和压力扩展
- mongos路由节点 :
- mongos是分片集群的路由节点,不存储数据
- 负责接收客户端的请求,根据分片键(Shard Key)把请求路由到对应的分片
- 合并多个分片的返回结果,返回给客户端
- 可以部署多个mongos节点,实现负载均衡
- 配置服务器(Config Server) :
- 配置服务器是一个副本集,存储分片集群的元数据(比如分片信息、数据分布信息、索引信息)
- mongos从配置服务器获取元数据,进行请求路由
- 必须是3节点的副本集,保证高可用
7.1.2 分片键(Shard Key)
分片键是分片集群的核心,是用来把数据分散到不同分片的字段,分片键的选择直接决定了分片集群的性能和扩展性,必须慎重选择。
分片键的选择原则:
- 高基数(High Cardinality):分片键的取值必须很多,比如用户ID、订单ID,不能是性别、状态这种只有几个值的字段
- 写分布均匀:分片键必须能让写操作均匀分布到所有分片,避免某个分片的写压力过大(热点分片)
- 查询隔离:分片键必须是高频查询的字段,查询时尽量带上分片键,mongos可以直接路由到对应的分片,避免广播查询(查询所有分片)
- 不可变:分片键的值不能修改,一旦插入就不能更改
7.1.3 分片策略
MongoDB支持三种分片策略:
- 范围分片(Range Sharding) :
- 根据分片键的范围进行分片,比如用户ID在1-100万的在分片1,100万-200万的在分片2
- 优点:范围查询性能好
- 缺点:容易出现热点分片(比如最新的数据都在一个分片)
- 哈希分片(Hash Sharding) :
- 对分片键进行哈希计算,根据哈希值进行分片,数据均匀分布到所有分片
- 优点:数据分布均匀,不会出现热点分片
- 缺点:范围查询性能差,需要广播查询
- 区域分片(Zone Sharding) :
- 根据分片键的区域进行分片,比如把中国的数据放在分片1,美国的数据放在分片2
- 优点:可以根据地理位置、业务规则分片,数据就近访问
- 缺点:配置复杂
推荐:生产环境优先使用哈希分片,数据分布均匀,不会出现热点分片;如果有大量范围查询,再考虑范围分片。
7.2 分片集群的搭建步骤
分片集群的搭建比较复杂,这里我们讲解最小化分片集群的搭建步骤:1个配置服务器副本集(3节点)、2个分片副本集(每个分片1主1从1仲裁)、1个mongos节点。
7.2.1 前置准备
准备9台服务器(或虚拟机):
- 配置服务器副本集:config1(192.168.1.20)、config2(192.168.1.21)、config3(192.168.1.22)
- 分片1副本集:shard1-primary(192.168.1.30)、shard1-secondary(192.168.1.31)、shard1-arbiter(192.168.1.32)
- 分片2副本集:shard2-primary(192.168.1.40)、shard2-secondary(192.168.1.41)、shard2-arbiter(192.168.1.42)
- mongos节点:mongos1(192.168.1.50)
所有服务器都安装MongoDB 7.0,关闭防火墙,创建数据目录和日志目录。
7.2.2 搭建配置服务器副本集
配置服务器必须是3节点的副本集,搭建步骤和普通副本集一致,配置文件中添加sharding: { clusterRole: "configsvr" }。
7.2.3 搭建分片副本集
搭建2个分片副本集,每个分片是1主1从1仲裁的副本集,配置文件中添加sharding: { clusterRole: "shardsvr" }。
7.2.4 启动mongos节点
mongos节点的配置文件不需要storage.dbPath,添加sharding: { configDB: "configRS/192.168.1.20:27017,192.168.1.21:27017,192.168.1.22:27017" }。
7.2.5 配置分片集群
连接到mongos节点,配置分片集群:
javascript
// 连接到mongos节点
mongosh --host 192.168.1.50 --port 27017
// 1. 添加分片
sh.addShard("shard1RS/192.168.1.30:27017,192.168.1.31:27017,192.168.1.32:27017");
sh.addShard("shard2RS/192.168.1.40:27017,192.168.1.41:27017,192.168.1.42:27017");
// 2. 开启数据库分片
sh.enableSharding("user_db");
// 3. 开启集合分片,选择分片键,使用哈希分片
sh.shardCollection("user_db.users", { _id: "hashed" });
// 4. 查看分片集群状态
sh.status();
7.3 分片集群的最佳实践
- 分片键选择至关重要:分片键必须高基数、写分布均匀、查询隔离、不可变,优先使用哈希分片
- 避免热点分片:热点分片会导致整个分片集群的性能瓶颈,通过合理选择分片键避免
- 查询尽量带上分片键:查询时带上分片键,mongos可以直接路由到对应的分片,避免广播查询(查询所有分片)
- 避免跨分片事务:跨分片事务性能极差,尽量通过数据模型设计避免
- 监控分片集群状态:定期监控分片集群的状态、数据分布、分片延迟,及时发现问题
- 合理规划分片数量:提前规划分片数量,避免后期频繁扩容,扩容会导致数据迁移,影响性能
第八章 生产环境运维与最佳实践
生产环境运维是MongoDB实战的最后一环,也是最重要的一环,这一章我们讲解监控、安全配置、性能调优、故障排查的核心内容。
8.1 监控
生产环境中,必须对MongoDB进行全面监控,及时发现问题,避免故障。
8.1.1 官方监控工具
-
MongoDB Compass:官方图形化客户端,自带监控功能,可以查看实时性能、慢查询、索引使用情况
-
mongostat :官方命令行工具,实时监控MongoDB的性能指标(QPS、连接数、内存使用等)
bashmongostat --host rs0/192.168.1.10:27017 --username root --password Root@123456 --authenticationDatabase admin -
mongotop :官方命令行工具,监控每个集合的读写时间
bashmongotop --host rs0/192.168.1.10:27017 --username root --password Root@123456 --authenticationDatabase admin
8.1.2 第三方监控工具
生产环境推荐使用第三方监控工具,比如:
- Prometheus + Grafana:最常用的监控方案,Prometheus采集指标,Grafana展示监控面板
- Zabbix:老牌监控工具,功能全面
- MongoDB Atlas:MongoDB云服务,自带完善的监控功能
8.1.3 核心监控指标
- 性能指标:QPS(读写请求数)、响应时间、慢查询数量
- 资源指标:CPU使用率、内存使用率、磁盘IO使用率、磁盘空间使用率
- 副本集指标:主从延迟、副本集状态、节点角色
- 分片集群指标:数据分布、分片延迟、mongos状态
8.2 安全配置
生产环境中,必须做好安全配置,避免数据泄露和未授权访问。
8.2.1 身份验证
-
开启身份验证 :配置文件中设置
security.authorization: enabled -
创建最小权限用户 :不要使用root用户连接业务,创建业务专用用户,只授予必要的权限
javascript// 创建业务用户,只授予user_db数据库的读写权限 use admin; db.createUser({ user: "app_user", pwd: "AppUser@123456", roles: [{ role: "readWrite", db: "user_db" }] }); -
定期更换密码:定期更换数据库用户的密码,避免密码泄露
8.2.2 网络安全
- 限制访问IP :配置文件中设置
net.bindIp为内网IP,不要绑定0.0.0.0 - 使用防火墙:使用防火墙限制只有业务服务器能访问MongoDB的27017端口
- 使用TLS/SSL加密:配置文件中开启TLS/SSL,加密客户端和MongoDB之间的通信
8.2.3 审计日志
开启审计日志,记录所有的数据库操作,便于事后追溯:
yaml
# 配置文件中开启审计日志
auditLog:
destination: file
format: JSON
path: /data/mongodb/log/audit.log
8.3 性能调优
生产环境中,必须做好性能调优,提升MongoDB的性能。
8.3.1 内存配置
MongoDB的WiredTiger存储引擎会尽量使用内存缓存数据,内存配置是性能调优的核心。
- WiredTiger缓存大小 :配置文件中设置
storage.wiredTiger.engineConfig.cacheSizeGB,建议设置为物理内存的50%-70% - 不要超过物理内存:缓存大小不要超过物理内存,否则会导致swap,性能急剧下降
8.3.2 连接数配置
配置文件中设置net.maxIncomingConnections,限制最大连接数,避免连接数过多导致内存溢出:
yaml
net:
maxIncomingConnections: 2000 # 最大连接数,根据服务器配置调整
8.3.3 WiredTiger存储引擎优化
yaml
storage:
wiredTiger:
engineConfig:
cacheSizeGB: 16 # 缓存大小
journalCompressor: snappy # 日志压缩算法
collectionConfig:
blockCompressor: snappy # 集合压缩算法
indexConfig:
prefixCompression: true # 索引前缀压缩
8.4 故障排查
生产环境中,遇到故障时,要冷静排查,快速定位问题。
8.4.1 常见故障与排查步骤
- 慢查询堆积 :
- 查看慢查询日志:
db.system.profile.find().sort({ ts: -1 }).limit(10) - 分析执行计划:
explain("executionStats") - 优化索引或SQL
- 查看慢查询日志:
- 连接数打满 :
- 查看连接数:
db.serverStatus().connections - 查看活跃连接:
db.currentOp() - kill异常连接:
db.killOp(opid) - 检查业务代码,是否有连接泄漏
- 查看连接数:
- 副本集主从延迟过高 :
- 查看主从延迟:
rs.status().members[n].optimeDate - 查看从节点的负载:
mongostat - 优化从节点的配置,或减少从节点的读压力
- 查看主从延迟:
- 内存使用率过高 :
- 查看内存使用:
db.serverStatus().mem - 调整WiredTiger缓存大小
- 检查是否有大查询占用内存
- 查看内存使用:
第九章 本篇最佳实践与避坑指南
9.1 数据模型设计最佳实践
- 优先使用嵌入式模型:一对多关系,"多"的数量少且经常一起查询,优先使用嵌入式模型,减少关联查询
- 合理使用引用式模型:一对多关系,"多"的数量多或很少一起查询,使用引用式模型
- 分桶设计优化时序数据:海量时序数据使用分桶设计,大幅减少文档数量,提升性能
- 合理冗余数据:反范式设计,冗余很少更新的静态数据,减少关联查询,提升性能
- 控制文档体积:单文档体积不要超过1MB,嵌套数组元素数量不要超过1000个
9.2 CRUD操作最佳实践
- 查询必须指定返回字段:禁止不写投影字段,只查询需要的字段,避免返回敏感字段和多余数据
- **更新必须用set∗∗:禁止直接替换文档,必须用set**:禁止直接替换文档,必须用set∗∗:禁止直接替换文档,必须用set更新指定字段
- 先查后改、先查后删:执行更新、删除前,先执行find()确认查询条件
- 优先使用逻辑删除:生产环境禁止物理删除,使用is_deleted字段实现逻辑删除
- 批量操作优先使用bulkWrite():批量操作使用bulkWrite(),性能更高,减少网络IO
- **高并发计数用inc∗∗:计数场景必须用inc**:计数场景必须用inc∗∗:计数场景必须用inc原子操作,避免并发安全问题
9.3 索引最佳实践
- 索引不是越多越好:单表索引控制在5个以内,只给高频查询字段建索引
- 复合索引遵循前缀匹配原则:高频等值查询字段放在最前面,范围查询字段放在后面
- 查询和排序字段加入同一个复合索引:避免内存排序
- 定期查看索引使用情况:删除长期未使用的索引
- 所有上线的查询必须用explain()分析:确认命中索引,避免全集合扫描
9.4 事务最佳实践
- 事务粒度尽可能小:事务中的操作要少,执行时间要短,避免长事务
- 优先使用单文档原子性:能通过单文档更新实现的,优先使用单文档原子性
- 写关注设置为majority:生产环境事务的writeConcern必须设置为majority
- 读偏好设置为primary:事务中的读操作必须从主节点读
- 合理设置超时时间:避免事务长时间占用资源
9.5 生产环境最佳实践
- 必须使用副本集:生产环境绝对不能使用单机MongoDB,必须部署副本集
- 必须开启身份验证:生产环境必须开启身份验证,创建最小权限用户
- 必须定期备份数据:从从节点备份数据,定期验证备份的有效性
- 必须全面监控:监控性能指标、资源指标、副本集状态、分片集群状态
- 禁止在业务高峰期执行重操作:创建/删除索引、备份数据、扩容分片等操作,必须在业务低峰期执行
本篇总结与附加篇预告
本篇总结
学完本篇,你已经完成了MongoDB从入门到精通的蜕变,达到了中高级开发的MongoDB水平:
- 熟练掌握了MongoDB的高级查询特性:地理空间查询、全文索引、正则查询、游标操作,搞定了LBS、全文搜索等特色业务场景
- 完全吃透了聚合管道的高阶用法:lookup关联查询、lookup关联查询、lookup关联查询、unwind数组拆分、$facet多面聚合、窗口函数,能用简洁的管道实现复杂的数据统计
- 掌握了MongoDB数据模型设计进阶:嵌入式vs引用式模型的选择、分桶设计、反范式设计,从源头优化了性能
- 深入理解了MongoDB事务原理:副本集事务、分片集群分布式事务,能在高并发场景下保证数据一致性
- 精通了索引进阶与性能优化:索引失效场景、慢查询分析与优化、执行计划深度解析,搞定了千万级数据下的查询优化
- 掌握了MongoDB高可用与水平扩展:副本集架构搭建、故障自动转移、读写分离;分片集群架构搭建、分片策略、水平扩展,支撑了PB级海量数据
- 具备了生产环境运维能力:监控、安全配置、性能调优、故障排查,能独立处理生产环境的常见问题
附加篇预告
《零基础从入门到精通MongoDB(附加篇):面试八股文全集》
在附加篇中,我们会把整个系列的核心知识点,整理成从基础到高阶全覆盖的MongoDB面试题,附标准答案与答题思路,适配校招、社招全场景,帮你搞定99%的MongoDB面试题,助力你拿到心仪的offer。
互动环节
如果你在学习过程中遇到任何问题,比如副本集搭建报错、慢查询优化、分片集群配置,都可以在评论区留言,我会一一回复解答。
如果本篇内容对你有帮助,欢迎点赞、收藏、转发,关注我,后续的八股文附加篇会第一时间更新,带你彻底吃透MongoDB,从零基础成长为NoSQL实战高手!