零基础从入门到精通MongoDB(下篇):进阶精通篇——吃透高级查询、事务、索引优化与集群架构,成为MongoDB实战高手

在上篇《筑基篇》中,我们从零搭建了MongoDB环境,吃透了核心概念、BSON数据类型、文档CRUD全操作、索引基础与聚合管道入门,相信你已经能独立完成90%业务场景下的基础开发。但随着业务规模增长,你一定会遇到这些瓶颈:

  • 做LBS业务时,需要查询"附近3公里的商家",却不知道MongoDB原生支持地理空间查询?
  • 做内容社区、商品搜索时,需要全文检索功能,却不知道MongoDB自带全文索引,无需额外引入Elasticsearch?
  • 做复杂数据统计时,需要关联多个集合、一次查询多个维度的统计结果,却不知道聚合管道的高阶用法?
  • 高并发场景下,需要保证"库存扣减+订单生成"的原子性,却不知道MongoDB 4.0+已经支持完整的事务?
  • 单库单表扛不住海量数据和高并发读写,却不知道MongoDB原生支持副本集高可用和分片集群水平扩展?
  • 慢查询堆积,却不知道怎么深度分析执行计划、怎么优化索引、怎么排查性能瓶颈?

别慌,这些问题正是本篇要彻底解决的核心。作为系列的第二篇------进阶精通篇,我们将完全承接上篇的基础,从高级查询、聚合管道高阶用法,到数据模型设计进阶、事务原理与实战,再到索引进阶、副本集、分片集群架构,最后到生产环境运维,带你彻底打通MongoDB的"任督二脉",从会用MongoDB变成真正懂MongoDB的企业级实战高手。


系列整体回顾与本篇核心规划

系列进度回顾

  1. 上篇(筑基篇):MongoDB核心认知、全场景环境搭建、核心数据模型与术语、文档CRUD全解、索引基础、聚合管道入门
  2. 下篇(进阶精通篇,本篇):高级查询操作、聚合管道高阶用法、数据模型设计进阶、事务原理与实战、索引进阶与性能优化、副本集架构、分片集群架构、生产环境运维与最佳实践
  3. 附加篇(八股文全集):从基础到高阶全覆盖的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支持两种地理空间索引:

  1. 2dsphere索引:用于地球表面的球面几何查询,支持所有GeoJSON类型,是生产环境的首选,适合真实的地理位置查询。
  2. 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支持两种全文索引:

  1. 单字段全文索引:给单个字符串字段创建全文索引
  2. 多字段全文索引:给多个字符串字段创建全文索引,一次查询可以搜索多个字段
javascript 复制代码
// 创建多字段全文索引:同时给name、description、tags字段创建全文索引
db.goods.createIndex({ name: "text", description: "text", tags: "text" });

// 创建通配符全文索引:给集合中所有字符串字段创建全文索引(不推荐生产环境使用,索引过大)
db.goods.createIndex({ "$**": "text" });

使用$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 全文索引的注意事项与避坑指南
  1. 全文索引是轻量级方案:适合数据量在千万级以内、搜索需求不复杂的场景,如果数据量过亿、搜索需求复杂(比如分词、同义词、高亮),建议使用Elasticsearch
  2. 一个集合只能有一个全文索引:MongoDB限制一个集合最多只能创建一个全文索引,需要搜索多个字段时,创建一个多字段全文索引即可
  3. 停用词过滤:MongoDB会自动过滤停用词(比如"的"、"了"、"a"、"the"等无意义的词),搜索这些词不会返回结果
  4. 分词规则: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种操作类型:

  1. insertOne:插入单条文档
  2. updateOne:更新单条文档
  3. updateMany:批量更新文档
  4. deleteOne:删除单条文档
  5. deleteMany:批量删除文档
  6. 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() 的优势
  1. 性能更高:一次网络请求执行多种操作,减少网络IO次数,性能比多次单条操作高10倍以上
  2. 原子性:ordered=true时,操作是有序的,某条操作失败会回滚之前的操作(注意:不是所有操作的原子性,是有序执行的回滚)
  3. 灵活性强:可以在一次请求中混合插入、更新、删除操作,适合复杂的批量数据处理场景

第二章 聚合管道高阶用法:实现复杂数据统计

在上篇中,我们学习了聚合管道的基础阶段: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 的注意事项与避坑指南
  1. $lookup 是 LEFT JOIN:等价于MySQL的LEFT JOIN,会返回左集合的所有文档,右集合没有匹配的话,as字段是空数组

  2. 关联字段类型必须一致:localField和foreignField的类型必须完全一致,比如ObjectId和ObjectId关联,String和String关联,类型不一致会导致关联失败,无法匹配到文档

  3. 关联字段建议加索引:右集合的foreignField字段建议加索引,否则$lookup会触发右集合的全表扫描,性能极差

  4. 避免大集合关联:两个大集合关联会占用大量内存,性能极差,尽量通过数据模型设计(嵌套文档)避免关联查询

  5. **MongoDB 5.0+ 支持更强大的 lookup∗∗:MongoDB5.0+新增了'lookup**:MongoDB 5.0+新增了`lookup∗∗:MongoDB5.0+新增了'lookuppipeline`选项,可以在关联右集合时先对右集合进行过滤、投影,减少关联的数据量,提升性能:

    javascript 复制代码
    db.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 的注意事项
  1. 默认不保留空数组文档 :默认情况下,如果数组字段是空数组[]、null、不存在,$unwind会过滤掉该文档,不会输出。如果需要保留,设置preserveNullAndEmptyArrays: true
  2. 大数组拆分注意性能:如果数组元素很多(比如超过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()
}

嵌入式模型的优势

  1. 查询性能极高:一次查询就能获取主文档和所有关联数据,无需关联查询,避免了$lookup的性能开销
  2. 原子性更新:更新主文档和关联数据是原子性的,无需事务就能保证数据一致性
  3. 数据局部性好:关联数据存储在一起,磁盘IO次数少,缓存命中率高

嵌入式模型的劣势

  1. 文档体积可能过大:如果嵌套数组的元素过多,会导致单文档体积超过16MB的限制,MongoDB会报错
  2. 嵌套数据更新不便:如果嵌套数据需要频繁单独更新,嵌入式模型会比较麻烦
  3. 嵌套数据查询不便:如果需要单独查询嵌套数据,嵌入式模型不如引用式模型灵活
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")
}

引用式模型的优势

  1. 文档体积小:关联数据单独存储,主文档体积小,不会超过16MB限制
  2. 关联数据更新灵活:关联数据可以单独更新,不影响主文档
  3. 关联数据查询灵活:可以单独查询关联数据,无需查询主文档

引用式模型的劣势

  1. 查询性能低:需要关联查询,使用$lookup,性能不如嵌入式模型
  2. 无原子性保证:更新主文档和关联数据不是原子性的,需要事务保证数据一致性
  3. 多次查询:如果需要获取主文档和关联数据,需要多次查询或使用$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") // 桶内最后一条数据的时间
}

分桶设计的优势

  1. 文档数量大幅减少:一个小时的数据一个文档,一天只有24个文档,一个月720个,一年8760个,文档数量减少了3万倍以上
  2. 索引大小大幅减少:索引只需要索引桶文档,索引大小极小,内存占用低
  3. 查询性能大幅提升:查询一个小时的数据,只需要查询1个文档,查询一天的数据,只需要查询24个文档,性能提升了上万倍
  4. 写入性能提升:写入时只需要更新桶文档的measurements数组,无需插入新文档,减少索引更新次数
3.2.3 分桶设计的最佳实践
  1. 桶的时间范围选择:根据数据上报频率选择,一般选择1分钟、5分钟、1小时、1天,原则是桶内的measurements数组元素数量不超过1000个
  2. 预分配桶:提前创建未来的桶文档,避免写入时创建桶文档的开销,提升写入性能
  3. 桶内数据压缩:如果数据量很大,可以对桶内的measurements数组进行压缩,减少文档体积
  4. 冷热数据分离:历史数据(冷数据)可以归档到单独的集合或存储引擎,当前数据(热数据)保留在主集合中

3.3 反范式设计:合理冗余,提升性能

反范式设计(Denormalization),就是通过合理冗余数据,减少关联查询,提升查询性能,是MongoDB数据模型设计的常用手段。关系型数据库强调三范式,避免数据冗余,而MongoDB优先推荐反范式设计,因为MongoDB的写入性能很高,冗余数据的更新成本远低于关联查询的性能成本。

3.3.1 反范式设计的常用手段
  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)
        }
      ]
    }
  2. 预计算字段:把需要频繁计算的结果冗余到文档中,比如订单文档冗余商品数量、订单明细的小计金额,避免每次查询都计算

  3. 嵌套历史数据:把历史数据嵌套在主文档中,比如用户文档嵌套最近10个订单,避免查询订单表

3.3.2 反范式设计的注意事项
  1. 冗余字段必须是很少更新的静态数据:比如用户名、商品名称,这些字段很少更新,冗余的更新成本低;如果冗余字段频繁更新,会导致大量的更新操作,反而降低性能
  2. 必须保证冗余数据的一致性:更新主数据时,必须同步更新冗余数据,可以通过事务、MongoDB Change Streams(变更流)来保证一致性
  3. 不要过度冗余:只冗余查询频率高的字段,不要冗余所有字段,否则会导致文档体积过大,反而影响性能
  4. 权衡利弊:反范式设计是"以空间换时间",要权衡存储空间和查询性能,选择最优方案

第四章 MongoDB事务原理与实战:高并发数据一致性的保障

在MongoDB 4.0之前,MongoDB不支持多文档事务,只能保证单文档的原子性,这是很多企业不选择MongoDB的核心原因。MongoDB 4.0版本之后,支持了副本集多文档事务 ,4.2版本之后支持了分片集群分布式事务,补齐了MongoDB的最后一块短板,现在MongoDB已经可以用于核心金融、支付等需要强事务的场景。


4.1 事务的核心概念

MongoDB的事务和关系型数据库的事务类似,遵循ACID特性:

  • 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败回滚
  • 一致性(Consistency):事务执行前后,数据的完整性约束不会被破坏
  • 隔离性(Isolation):多个事务并发执行时,互相之间是隔离的
  • 持久性(Durability):事务一旦提交成功,对数据的修改就会永久生效

MongoDB的事务分为两类:

  1. 单文档事务:MongoDB天生支持单文档的原子性,更新单个文档(包括更新嵌套文档、数组)是原子性的,无需显式开启事务
  2. 多文档事务:MongoDB 4.0+支持副本集多文档事务,4.2+支持分片集群分布式事务,需要显式开启、提交、回滚

4.2 副本集多文档事务实战

副本集多文档事务,是指在副本集环境下,对多个文档、多个集合的操作,组成一个原子性的事务,要么全部成功,要么全部回滚。

4.2.1 前置条件

使用副本集多文档事务,必须满足以下条件:

  1. MongoDB版本 >= 4.0
  2. 必须是副本集环境(单机MongoDB不支持事务,生产环境也必须用副本集)
  3. 事务中的操作必须在同一个数据库中(MongoDB 4.2+支持跨数据库事务)
  4. 事务中的集合必须存在(不能在事务中创建集合)
  5. 事务中的操作不能是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的事务支持两种隔离级别:

  1. 读已提交(Read Committed):默认隔离级别,一个事务只能读到其他事务已经提交的数据
  2. 快照读(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 事务的最佳实践与避坑指南

  1. 事务粒度尽可能小:事务中的操作要尽可能少,执行时间要尽可能短,避免长事务。长事务会占用大量资源,导致锁等待,影响并发性能
  2. 事务中的操作尽可能命中索引:事务中的查询、更新操作必须命中索引,否则会触发全表扫描,导致事务执行时间过长
  3. 避免在事务中执行耗时操作:禁止在事务中调用外部接口、等待用户输入、执行大查询,避免长事务
  4. 合理设置超时时间 :事务默认超时时间是60秒,可以通过maxCommitTimeMS设置,避免事务长时间占用资源
  5. 优先使用单文档原子性:如果能通过单文档更新实现业务需求,优先使用单文档原子性,不要使用多文档事务,单文档原子性性能更高,没有事务开销
  6. 写关注设置为majority :生产环境中,事务的writeConcern必须设置为w: "majority",保证事务提交后,大多数副本节点都已写入,避免数据丢失
  7. 读偏好设置为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 }讲解:

  1. 违背复合索引前缀匹配原则

    javascript 复制代码
    // 失效:不包含第一个前缀字段is_vip
    db.users.find({ age: 25, username: "zhangsan" });
  2. 索引字段使用函数运算/类型转换

    javascript 复制代码
    // 失效:对age字段使用$add函数
    db.users.find({ $expr: { $eq: [{ $add: ["$age", 1] }, 26] } });
    // 失效:类型不匹配,age是Int,查询用String
    db.users.find({ age: "25" });
  3. 使用ne、ne、ne、not、$nin反向查询

    javascript 复制代码
    // 大概率失效:$ne反向查询
    db.users.find({ age: { $ne: 25 } });
  4. 使用$exists查询字段不存在的文档

    javascript 复制代码
    // 大概率失效:$exists: false
    db.users.find({ email: { $exists: false } });
  5. 使用$regex非前缀匹配正则

    javascript 复制代码
    // 失效:非前缀匹配正则
    db.users.find({ username: { $regex: /san/ } });
    // 失效:不区分大小写的正则,即使是前缀匹配也失效
    db.users.find({ username: { $regex: /^zhang/, $options: "i" } });
  6. 使用$or连接非索引字段

    javascript 复制代码
    // 失效:$or连接的字段有一个没有索引
    db.users.find({ $or: [{ username: "zhangsan" }, { address: "北京" }] });
  7. 复合索引中,范围查询字段后面的字段无法命中索引

    javascript 复制代码
    // 只有is_vip和age字段命中索引,username字段无法命中
    db.users.find({ is_vip: true, age: { $gt: 25 }, username: "zhangsan" });
  8. 查询的字段选择性极低

    javascript 复制代码
    // 大概率失效:is_vip字段只有true/false两个值,选择性极低
    db.users.find({ is_vip: true });
  9. 使用$where操作符

    javascript 复制代码
    // 失效:$where操作符会触发全表扫描,性能极差,禁止使用
    db.users.find({ $where: "this.age > 25" });
  10. 索引碎片化严重

    • 集合经过大量的插入、更新、删除后,索引会出现碎片化,导致索引效率降低,需要重建索引

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");

核心分析字段

  1. executionStats.executionSuccess:查询是否执行成功
  2. executionStats.nReturned:查询返回的文档数
  3. executionStats.totalKeysExamined:索引扫描的条目数
  4. executionStats.totalDocsExamined:文档扫描的条目数
  5. executionStats.executionTimeMillis:查询执行时间(毫秒)
  6. queryPlanner.winningPlan.inputStage.stage :查询阶段,核心判断指标
    • IXSCAN:命中索引,索引扫描,性能最优
    • COLLSCAN:全集合扫描,没有命中索引,性能极差,必须优化

优化目标

  • winningPlan.inputStage.stage必须是IXSCAN,绝对不能出现COLLSCAN
  • nReturnedtotalKeysExaminedtotalDocsExamined,三者越接近,索引效率越高
  • 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 索引维护最佳实践

  1. 定期查看索引使用情况 :使用$indexStats查看索引的使用情况,删除长期未使用的索引,减少写入开销

    javascript 复制代码
    // 查看索引使用情况
    db.users.aggregate([{ $indexStats: {} }]);
  2. 定期重建碎片化严重的索引 :集合经过大量插入、更新、删除后,索引会出现碎片化,需要重建索引

    javascript 复制代码
    // 重建索引
    db.users.reIndex(); // 重建所有索引
    db.users.dropIndex("index_name"); // 删除索引
    db.users.createIndex({ ... }); // 重新创建索引
  3. 避免在业务高峰期创建/删除索引:创建/删除索引会占用大量资源,影响业务性能,应该在业务低峰期执行

  4. 索引数量控制在5个以内:索引越多,写入性能越差,单表索引数量控制在5个以内,避免冗余、重复索引

  5. 复合索引遵循前缀匹配原则:高频等值查询字段放在最前面,范围查询字段放在后面,查询和排序字段尽量加入同一个复合索引


第六章 副本集架构:MongoDB高可用的基础

副本集(Replica Set)是MongoDB的高可用架构,由一组MongoDB节点组成,包含一个主节点(Primary)和多个从节点(Secondary),主节点负责写操作,从节点负责读操作,主节点故障时,从节点会自动选举出新的主节点,实现故障自动转移,保证业务高可用。生产环境中,MongoDB必须部署为副本集,绝对不能使用单机MongoDB。


6.1 副本集的核心概念

6.1.1 副本集的节点类型

副本集包含三种节点类型:

  1. 主节点(Primary)
    • 副本集中只有一个主节点
    • 负责处理所有的写操作(INSERT、UPDATE、DELETE)
    • 负责处理读操作(默认读偏好是primary)
    • 把写操作记录到oplog(操作日志)中
  2. 从节点(Secondary)
    • 副本集中可以有多个从节点,推荐至少2个从节点,形成一主两从架构
    • 从主节点同步oplog,重放oplog,保持和主节点数据一致
    • 可以处理读操作,实现读写分离
    • 主节点故障时,参与选举,投票选出新的主节点
  3. 仲裁节点(Arbiter)
    • 仲裁节点不存储数据,不参与读写操作,只参与选举投票
    • 用于保证副本集节点数为奇数,避免脑裂(Split Brain)
    • 推荐在节点数为偶数时添加仲裁节点,比如一主一从,添加一个仲裁节点,形成一主一从一仲裁架构
6.1.2 副本集的核心特性
  1. 高可用:主节点故障时,从节点会自动选举出新的主节点,故障转移时间一般在10-30秒,业务几乎无感知
  2. 数据冗余:从节点同步主节点的数据,保证数据有多份副本,避免数据丢失
  3. 读写分离:主节点负责写,从节点负责读,分散读压力,提升读并发能力
  4. 数据备份:可以从从节点备份数据,不影响主节点的业务性能

6.2 副本集的搭建步骤(一主两从)

这里我们讲解在Linux环境下,搭建一主两从副本集的详细步骤,生产环境推荐使用一主两从架构。

6.2.1 前置准备
  1. 准备3台服务器(或3个虚拟机),IP地址分别为:

    • node1: 192.168.1.10(主节点)
    • node2: 192.168.1.11(从节点)
    • node3: 192.168.1.12(从节点)
  2. 在3台服务器上都安装MongoDB 7.0(安装步骤参考上篇)

  3. 关闭3台服务器的防火墙,或开放27017端口

  4. 在3台服务器上创建数据目录和日志目录:

    bash 复制代码
    mkdir -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种读偏好:

  1. primary:默认读偏好,所有读操作都从主节点读,保证数据一致性
  2. primaryPreferred:优先从主节点读,主节点不可用时,从从节点读
  3. secondary:所有读操作都从从节点读,数据可能有延迟(主从延迟)
  4. secondaryPreferred:优先从从节点读,从节点不可用时,从主节点读
  5. 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 读写分离的注意事项
  1. 主从延迟问题:从节点同步主节点的数据有延迟,从从节点读可能读到旧数据,对数据一致性要求高的场景,必须从主节点读
  2. 读偏好选择
    • 对数据一致性要求极高的场景(比如支付、订单查询):使用primary
    • 对数据一致性要求不高的场景(比如商品列表、内容社区):使用secondaryPreferred
  3. 监控主从延迟:定期监控主从延迟,延迟过高时,及时从主节点读

6.4 副本集的故障自动转移

副本集的核心优势就是故障自动转移,当主节点故障时,从节点会自动选举出新的主节点,业务几乎无感知。

6.4.1 故障转移的流程
  1. 故障检测:从节点通过心跳检测,发现主节点不可达
  2. 选举触发:从节点发起选举,投票给自己
  3. 投票选举:所有节点参与投票,获得大多数节点(>n/2)投票的节点成为新的主节点
  4. 角色切换:新的主节点升级为主,其他从节点从新的主节点同步数据
  5. 恢复服务:新的主节点开始处理写操作,业务恢复
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 分片集群的组成

分片集群由三个核心组件组成:

  1. 分片(Shard)
    • 每个分片是一个副本集,负责存储一部分数据
    • 推荐至少3个分片,生产环境根据数据量和压力扩展
  2. mongos路由节点
    • mongos是分片集群的路由节点,不存储数据
    • 负责接收客户端的请求,根据分片键(Shard Key)把请求路由到对应的分片
    • 合并多个分片的返回结果,返回给客户端
    • 可以部署多个mongos节点,实现负载均衡
  3. 配置服务器(Config Server)
    • 配置服务器是一个副本集,存储分片集群的元数据(比如分片信息、数据分布信息、索引信息)
    • mongos从配置服务器获取元数据,进行请求路由
    • 必须是3节点的副本集,保证高可用
7.1.2 分片键(Shard Key)

分片键是分片集群的核心,是用来把数据分散到不同分片的字段,分片键的选择直接决定了分片集群的性能和扩展性,必须慎重选择。

分片键的选择原则

  1. 高基数(High Cardinality):分片键的取值必须很多,比如用户ID、订单ID,不能是性别、状态这种只有几个值的字段
  2. 写分布均匀:分片键必须能让写操作均匀分布到所有分片,避免某个分片的写压力过大(热点分片)
  3. 查询隔离:分片键必须是高频查询的字段,查询时尽量带上分片键,mongos可以直接路由到对应的分片,避免广播查询(查询所有分片)
  4. 不可变:分片键的值不能修改,一旦插入就不能更改
7.1.3 分片策略

MongoDB支持三种分片策略:

  1. 范围分片(Range Sharding)
    • 根据分片键的范围进行分片,比如用户ID在1-100万的在分片1,100万-200万的在分片2
    • 优点:范围查询性能好
    • 缺点:容易出现热点分片(比如最新的数据都在一个分片)
  2. 哈希分片(Hash Sharding)
    • 对分片键进行哈希计算,根据哈希值进行分片,数据均匀分布到所有分片
    • 优点:数据分布均匀,不会出现热点分片
    • 缺点:范围查询性能差,需要广播查询
  3. 区域分片(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 分片集群的最佳实践

  1. 分片键选择至关重要:分片键必须高基数、写分布均匀、查询隔离、不可变,优先使用哈希分片
  2. 避免热点分片:热点分片会导致整个分片集群的性能瓶颈,通过合理选择分片键避免
  3. 查询尽量带上分片键:查询时带上分片键,mongos可以直接路由到对应的分片,避免广播查询(查询所有分片)
  4. 避免跨分片事务:跨分片事务性能极差,尽量通过数据模型设计避免
  5. 监控分片集群状态:定期监控分片集群的状态、数据分布、分片延迟,及时发现问题
  6. 合理规划分片数量:提前规划分片数量,避免后期频繁扩容,扩容会导致数据迁移,影响性能

第八章 生产环境运维与最佳实践

生产环境运维是MongoDB实战的最后一环,也是最重要的一环,这一章我们讲解监控、安全配置、性能调优、故障排查的核心内容。


8.1 监控

生产环境中,必须对MongoDB进行全面监控,及时发现问题,避免故障。

8.1.1 官方监控工具
  1. MongoDB Compass:官方图形化客户端,自带监控功能,可以查看实时性能、慢查询、索引使用情况

  2. mongostat :官方命令行工具,实时监控MongoDB的性能指标(QPS、连接数、内存使用等)

    bash 复制代码
    mongostat --host rs0/192.168.1.10:27017 --username root --password Root@123456 --authenticationDatabase admin
  3. mongotop :官方命令行工具,监控每个集合的读写时间

    bash 复制代码
    mongotop --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 核心监控指标
  1. 性能指标:QPS(读写请求数)、响应时间、慢查询数量
  2. 资源指标:CPU使用率、内存使用率、磁盘IO使用率、磁盘空间使用率
  3. 副本集指标:主从延迟、副本集状态、节点角色
  4. 分片集群指标:数据分布、分片延迟、mongos状态

8.2 安全配置

生产环境中,必须做好安全配置,避免数据泄露和未授权访问。

8.2.1 身份验证
  1. 开启身份验证 :配置文件中设置security.authorization: enabled

  2. 创建最小权限用户 :不要使用root用户连接业务,创建业务专用用户,只授予必要的权限

    javascript 复制代码
    // 创建业务用户,只授予user_db数据库的读写权限
    use admin;
    db.createUser({
      user: "app_user",
      pwd: "AppUser@123456",
      roles: [{ role: "readWrite", db: "user_db" }]
    });
  3. 定期更换密码:定期更换数据库用户的密码,避免密码泄露

8.2.2 网络安全
  1. 限制访问IP :配置文件中设置net.bindIp为内网IP,不要绑定0.0.0.0
  2. 使用防火墙:使用防火墙限制只有业务服务器能访问MongoDB的27017端口
  3. 使用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 常见故障与排查步骤
  1. 慢查询堆积
    • 查看慢查询日志:db.system.profile.find().sort({ ts: -1 }).limit(10)
    • 分析执行计划:explain("executionStats")
    • 优化索引或SQL
  2. 连接数打满
    • 查看连接数:db.serverStatus().connections
    • 查看活跃连接:db.currentOp()
    • kill异常连接:db.killOp(opid)
    • 检查业务代码,是否有连接泄漏
  3. 副本集主从延迟过高
    • 查看主从延迟:rs.status().members[n].optimeDate
    • 查看从节点的负载:mongostat
    • 优化从节点的配置,或减少从节点的读压力
  4. 内存使用率过高
    • 查看内存使用:db.serverStatus().mem
    • 调整WiredTiger缓存大小
    • 检查是否有大查询占用内存

第九章 本篇最佳实践与避坑指南

9.1 数据模型设计最佳实践

  1. 优先使用嵌入式模型:一对多关系,"多"的数量少且经常一起查询,优先使用嵌入式模型,减少关联查询
  2. 合理使用引用式模型:一对多关系,"多"的数量多或很少一起查询,使用引用式模型
  3. 分桶设计优化时序数据:海量时序数据使用分桶设计,大幅减少文档数量,提升性能
  4. 合理冗余数据:反范式设计,冗余很少更新的静态数据,减少关联查询,提升性能
  5. 控制文档体积:单文档体积不要超过1MB,嵌套数组元素数量不要超过1000个

9.2 CRUD操作最佳实践

  1. 查询必须指定返回字段:禁止不写投影字段,只查询需要的字段,避免返回敏感字段和多余数据
  2. **更新必须用set∗∗:禁止直接替换文档,必须用set**:禁止直接替换文档,必须用set∗∗:禁止直接替换文档,必须用set更新指定字段
  3. 先查后改、先查后删:执行更新、删除前,先执行find()确认查询条件
  4. 优先使用逻辑删除:生产环境禁止物理删除,使用is_deleted字段实现逻辑删除
  5. 批量操作优先使用bulkWrite():批量操作使用bulkWrite(),性能更高,减少网络IO
  6. **高并发计数用inc∗∗:计数场景必须用inc**:计数场景必须用inc∗∗:计数场景必须用inc原子操作,避免并发安全问题

9.3 索引最佳实践

  1. 索引不是越多越好:单表索引控制在5个以内,只给高频查询字段建索引
  2. 复合索引遵循前缀匹配原则:高频等值查询字段放在最前面,范围查询字段放在后面
  3. 查询和排序字段加入同一个复合索引:避免内存排序
  4. 定期查看索引使用情况:删除长期未使用的索引
  5. 所有上线的查询必须用explain()分析:确认命中索引,避免全集合扫描

9.4 事务最佳实践

  1. 事务粒度尽可能小:事务中的操作要少,执行时间要短,避免长事务
  2. 优先使用单文档原子性:能通过单文档更新实现的,优先使用单文档原子性
  3. 写关注设置为majority:生产环境事务的writeConcern必须设置为majority
  4. 读偏好设置为primary:事务中的读操作必须从主节点读
  5. 合理设置超时时间:避免事务长时间占用资源

9.5 生产环境最佳实践

  1. 必须使用副本集:生产环境绝对不能使用单机MongoDB,必须部署副本集
  2. 必须开启身份验证:生产环境必须开启身份验证,创建最小权限用户
  3. 必须定期备份数据:从从节点备份数据,定期验证备份的有效性
  4. 必须全面监控:监控性能指标、资源指标、副本集状态、分片集群状态
  5. 禁止在业务高峰期执行重操作:创建/删除索引、备份数据、扩容分片等操作,必须在业务低峰期执行

本篇总结与附加篇预告

本篇总结

学完本篇,你已经完成了MongoDB从入门到精通的蜕变,达到了中高级开发的MongoDB水平:

  • 熟练掌握了MongoDB的高级查询特性:地理空间查询、全文索引、正则查询、游标操作,搞定了LBS、全文搜索等特色业务场景
  • 完全吃透了聚合管道的高阶用法:lookup关联查询、lookup关联查询、lookup关联查询、unwind数组拆分、$facet多面聚合、窗口函数,能用简洁的管道实现复杂的数据统计
  • 掌握了MongoDB数据模型设计进阶:嵌入式vs引用式模型的选择、分桶设计、反范式设计,从源头优化了性能
  • 深入理解了MongoDB事务原理:副本集事务、分片集群分布式事务,能在高并发场景下保证数据一致性
  • 精通了索引进阶与性能优化:索引失效场景、慢查询分析与优化、执行计划深度解析,搞定了千万级数据下的查询优化
  • 掌握了MongoDB高可用与水平扩展:副本集架构搭建、故障自动转移、读写分离;分片集群架构搭建、分片策略、水平扩展,支撑了PB级海量数据
  • 具备了生产环境运维能力:监控、安全配置、性能调优、故障排查,能独立处理生产环境的常见问题

附加篇预告

《零基础从入门到精通MongoDB(附加篇):面试八股文全集》

在附加篇中,我们会把整个系列的核心知识点,整理成从基础到高阶全覆盖的MongoDB面试题,附标准答案与答题思路,适配校招、社招全场景,帮你搞定99%的MongoDB面试题,助力你拿到心仪的offer。


互动环节

如果你在学习过程中遇到任何问题,比如副本集搭建报错、慢查询优化、分片集群配置,都可以在评论区留言,我会一一回复解答。

如果本篇内容对你有帮助,欢迎点赞、收藏、转发,关注我,后续的八股文附加篇会第一时间更新,带你彻底吃透MongoDB,从零基础成长为NoSQL实战高手!

相关推荐
Ts-Drunk2 小时前
[特殊字符]深度解剖!Hermes-Agent 源码全解析(架构+核心流程+二次开发指南)
人工智能·架构·ai编程·hermes
sa100272 小时前
一键获取淘宝天猫商品评论:API 接口实战与多语言实现教程
数据库·oracle
不懂的浪漫2 小时前
mqtt-plus 架构解析(九):测试体系,为什么要同时有 MqttTestTemplate 和 EmbeddedBroker
spring boot·物联网·mqtt·架构
ofoxcoding2 小时前
OpenClaw Nanobot 架构拆解:从源码学会 AI Agent 的骨架设计(2026)
人工智能·ai·架构
huanmieyaoseng10032 小时前
Linux安装达梦数据库DM8
linux·运维·数据库
禅思院2 小时前
使用 VueUse 构建一个支持暂停/重置的 CountUp 组件
前端·vue.js·架构
香蕉鼠片2 小时前
Mysql进阶篇
数据库·mysql·oracle
数厘2 小时前
2.12 sql 数据插入(INSERT INTO)
数据库·sql·oracle
薛定e的猫咪2 小时前
2026 年 4 月实测:OpenAI Codex 保姆级教程,从安装到 MCP、Skills 与多智能体协作
前端·数据库·人工智能