MongoDB 完全指南:从文档模型到分片集群的生产实战

MongoDB 是文档数据库的代表 —— 你拿到的不是行而是 JSON-like 文档,schema 灵活,查询语法表达力强。但很多人用它只是"把 JSON 存数据库",没用上索引、Aggregation Pipeline、副本集、分片这些核心能力,结果性能差、可靠性差,然后归咎于"MongoDB 不行"。这篇文章把 MongoDB 从数据模型讲到生产部署,讲清楚它适合什么、不适合什么。

数据模型:Document、Collection、Database

// 一个文档(Document) - 类似 JSON
{
    "_id": ObjectId("..."),
    "username": "mores",
    "email": "m@x.com",
    "profile": {
        "age": 30,
        "city": "Beijing"
    },
    "tags": ["dev", "blogger"],
    "createdAt": ISODate("2026-05-15")
}

// Collection 类似关系数据库的表,但 schema 灵活
// Database 类似关系数据库的库

MongoDB 文档大小上限 16MB,字段命名规则、嵌套对象、数组都支持。无 schema 不代表无设计 —— 反而要在应用层精心设计文档结构,否则容易乱。

基础 CRUD

// 插入
db.users.insertOne({ username: "mores", email: "m@x.com" })
db.users.insertMany([{...}, {...}])

// 查询
db.users.find({ username: "mores" })
db.users.find({ age: { $gte: 18, $lt: 60 } })
db.users.find({ tags: { $in: ["dev", "designer"] } })
db.users.find({ "profile.city": "Beijing" })   // 嵌套字段

// 投影:只返回部分字段
db.users.find({ city: "Beijing" }, { username: 1, email: 1 })

// 更新
db.users.updateOne({ _id: ObjectId(...) }, { $set: { age: 31 } })
db.users.updateMany({ city: "Beijing" }, { $inc: { age: 1 } })

// 数组操作
db.users.updateOne({ _id: ... }, { $push: { tags: "blogger" } })
db.users.updateOne({ _id: ... }, { $pull: { tags: "old_tag" } })
db.users.updateOne({ _id: ... }, { $addToSet: { tags: "unique" } })

// 删除
db.users.deleteOne({ _id: ... })
db.users.deleteMany({ status: "inactive" })

索引

没有索引的 MongoDB 查询是全 collection 扫描。索引设计和 MySQL 类似:

// 单字段
db.users.createIndex({ username: 1 })          // 1 升序,-1 降序
db.users.createIndex({ email: 1 }, { unique: true })

// 复合索引:遵循"最左前缀"
db.orders.createIndex({ userId: 1, createdAt: -1 })
// 能用:WHERE userId = ?, WHERE userId = ? ORDER BY createdAt DESC
// 不能用:WHERE createdAt = ?  (跳过了 userId)

// 数组索引:数组每个元素都建索引
db.users.createIndex({ tags: 1 })
// 查 { tags: "dev" } 用得上

// 全文索引
db.articles.createIndex({ content: "text" })
db.articles.find({ $text: { $search: "kubernetes" } })

// 地理空间索引
db.bikes.createIndex({ location: "2dsphere" })
db.bikes.find({
    location: {
        $near: {
            $geometry: { type: "Point", coordinates: [116.4, 39.9] },
            $maxDistance: 1000   // 1 公里内
        }
    }
})

// TTL 索引:自动删除过期文档
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })

// 部分索引(只对部分文档建索引,省空间)
db.orders.createIndex(
    { createdAt: 1 },
    { partialFilterExpression: { status: "active" } }
)

// 查看索引
db.orders.getIndexes()
db.orders.find({ ... }).explain("executionStats")   // 看是否用了索引

Aggregation Pipeline:MongoDB 的"SQL"

聚合管道由多个 stage 组成,文档流过每个 stage 做变换:

// "上个月每个城市的订单数和总金额"
db.orders.aggregate([
    // 1. 过滤
    { $match: { createdAt: { $gte: ISODate("2026-04-01"), $lt: ISODate("2026-05-01") } } },

    // 2. join 用户表(类似 SQL JOIN)
    { $lookup: {
        from: "users",
        localField: "userId",
        foreignField: "_id",
        as: "user"
    }},
    { $unwind: "$user" },

    // 3. 分组聚合
    { $group: {
        _id: "$user.profile.city",
        count: { $sum: 1 },
        total: { $sum: "$amount" }
    }},

    // 4. 排序
    { $sort: { total: -1 } },

    // 5. 限制
    { $limit: 10 }
])

Aggregation 比 SQL 表达力更灵活(数组、嵌套对象、表达式),但学习曲线陡。复杂查询调试要熟练用 explain 看每个 stage 性能。

事务

MongoDB 4.0+ 支持多文档事务,API 像 SQL:

const session = client.startSession();
try {
    session.startTransaction();

    db.accounts.updateOne({ _id: "A" }, { $inc: { balance: -100 } }, { session });
    db.accounts.updateOne({ _id: "B" }, { $inc: { balance: 100 } }, { session });

    session.commitTransaction();
} catch (e) {
    session.abortTransaction();
} finally {
    session.endSession();
}

但 MongoDB 事务有限制:

  • 性能比单文档慢一个数量级 —— 用得多就该想是不是 schema 设计有问题。
  • 跨分片事务在 4.2+ 才支持,且性能更差。
  • 不能跨数据库(单 cluster 内可以)。

MongoDB 的设计哲学是"把相关数据嵌进同一个文档,避免多文档事务"。能嵌套就嵌套。

Schema 设计:嵌入 vs 引用

嵌入(Embedding)

{
    _id: ...,
    username: "mores",
    addresses: [
        { type: "home", city: "Beijing", street: "..." },
        { type: "office", city: "Shanghai", street: "..." }
    ]
}

适合:数据生命周期相同、读取通常一起、子文档相对小且数量有限。

引用(Referencing)

// users collection
{ _id: 1, username: "mores" }

// orders collection
{ _id: ..., userId: 1, items: [...] }

// 查询时手动或用 $lookup 关联

适合:子文档数量不可控、独立访问、生命周期不同。

实战经验

1. "一起访问的数据放一起" 是 MongoDB 设计第一原则。
2. "1 对少"嵌入,"1 对多"引用,"多对多"看情况
3. 避免无限增长的数组 —— 一个文档存几万条评论会变成大文档,操作慢。
4. 需要全局唯一约束的字段,要建唯一索引(MongoDB 不自动)。

副本集(Replica Set)

MongoDB 的高可用方案:一主 + N 从,主挂了自动选新主。

// 配置副本集
rs.initiate({
    _id: "rs0",
    members: [
        { _id: 0, host: "mongo1:27017" },
        { _id: 1, host: "mongo2:27017" },
        { _id: 2, host: "mongo3:27017", arbiterOnly: true }   // 仲裁节点
    ]
})

// 客户端连接(写主、读从)
mongo "mongodb://mongo1:27017,mongo2:27017,mongo3:27017/?replicaSet=rs0"

Read Preference / Write Concern

// 读偏好:从哪里读
db.users.find().readPref("secondary")   // 优先从从节点读

// 写关注:写多少节点才算成功
db.users.insertOne(
    { ... },
    { writeConcern: { w: "majority", j: true } }   // 多数节点写盘
)

这套机制让 MongoDB 在 CAP 上可调:strong write concern + linearizable read = 强一致;最低配置 = 性能优先 + 最终一致。

分片(Sharding)

数据量超过单机时分片,通常几十 TB 以上才用。

// 启用分片
sh.enableSharding("mydb")
sh.shardCollection("mydb.orders", { userId: "hashed" })
// 按 userId 哈希分到不同 shard

// 看分片状态
sh.status()

分片设计的关键是选好 shard key:

  • 分布均匀(避免热点)。
  • 查询能利用(否则每次查都要 scatter-gather)。
  • 一旦选定难以更改(MongoDB 5.0 之前不能改,5.0+ 才能 reshard)。

常见错误:用"自增 ID"做 shard key —— 所有新写都到一个分片,瞬间热点。

性能优化

  • 查询用 explain:executionStats 里 "totalDocsExamined" 应接近 "nReturned"。差距大说明扫了很多没用的。
  • 批量写:insertMany / bulkWrite 比循环 insertOne 快很多。
  • 避免大文档:文档接近 16MB 性能急剧下降。监控并拆分。
  • 合理的 working set:MongoDB 想性能好,常用数据要在内存里。指标:resident memory 至少能放下索引 + 热数据。

什么时候用 MongoDB

  • 数据结构灵活,schema 演化频繁(博客文章、CMS、产品目录)。
  • 需要嵌套结构(地址、配置、属性)。
  • 读多写少,且查询模式可预测。
  • 需要全文检索 + 地理空间 + 复杂聚合一站式。

什么时候不用

  • 需要强一致的复杂事务(转账、库存)—— 还是 RDBMS 更稳。
  • 大量 JOIN 关联查询 —— MongoDB 的 $lookup 性能差,关系数据用关系数据库。
  • 极高写吞吐 + 时序数据 —— InfluxDB / ClickHouse 更专业。
  • 需要 SQL 标准生态(BI 工具、报表)。

常见坑

坑 1:把 schema 不固定理解为"不用设计"。错。schema 灵活不代表无设计,反而要更精心。新字段加进去后,查询、索引、迁移都要考虑。

坑 2:用 _id 自动生成的 ObjectId 排序当时间序。ObjectId 前 4 字节是时间戳,大致按时间递增,但同一秒内多个节点生成不严格递增。需要严格时间序用专门的字段。

坑 3:跨 shard 查询。 查询不带 shard key,MongoDB 要 fan-out 到所有 shard,性能差。设计 shard key 时考虑查询模式。

坑 4:writeConcern 选 1。 默认 w:1 时,主节点写完返回。如果主立刻挂,从未必复制完,数据丢。重要数据 w:"majority"。

坑 5:聚合内存超限。Aggregation 默认 100MB 内存上限,超了报错。复杂聚合加 { allowDiskUse: true }

Change Streams:监听数据变化

MongoDB 3.6+ 提供 Change Streams,实时监听 collection 的变化:

// 监听 orders 集合的所有变化
const changeStream = db.orders.watch();
changeStream.on('change', (change) => {
    console.log(change.operationType, change.fullDocument);
});

// 带过滤
db.orders.watch([
    { $match: { 'fullDocument.status': 'paid' } }
]);

// 从某个 resumeToken 续传(支持断线恢复)
db.orders.watch([], { resumeAfter: lastToken });

Change Streams 让 MongoDB 也能做"事件驱动架构" —— 数据变了,推送给下游。底层依赖 oplog(副本集的操作日志),所以必须用副本集才能用。

聚合管道的高级用法

$facet:多个聚合在一个查询里

// "本月订单总数 + 按状态分布 + 每天数量"一次查询完
db.orders.aggregate([
    { $match: { createdAt: { $gte: ISODate("2026-05-01") } } },
    { $facet: {
        total: [ { $count: "count" } ],
        byStatus: [ { $group: { _id: "$status", count: { $sum: 1 } } } ],
        daily: [
            { $group: {
                _id: { $dateToString: { format: "%Y-%m-%d", date: "$createdAt" } },
                count: { $sum: 1 }
            }},
            { $sort: { _id: 1 } }
        ]
    }}
]);
// 返回 { total: [...], byStatus: [...], daily: [...] }

$bucket / $bucketAuto:数据分桶

// 用户年龄分布
db.users.aggregate([
    { $bucket: {
        groupBy: "$age",
        boundaries: [0, 18, 30, 50, 100],
        default: "unknown",
        output: { count: { $sum: 1 } }
    }}
]);
// [{ _id: 0, count: 100 }, { _id: 18, count: 5000 }, ...]

$graphLookup:图遍历

// 找出某个员工的全部上下级
db.employees.aggregate([
    { $match: { name: "Alice" } },
    { $graphLookup: {
        from: "employees",
        startWith: "$reportsTo",
        connectFromField: "reportsTo",
        connectToField: "_id",
        as: "managers",
        maxDepth: 5
    }}
]);
// 一次查询拿到 Alice 上面所有层级的 manager

索引的隐性消耗

索引太多会拖慢写入和增大存储。每加一个索引:

  • 插入 / 更新需要维护额外索引树。
  • 占空间(尤其 multikey 数组索引膨胀快)。
  • 查询优化器要考虑的候选索引变多,plan 选择慢。

定期检查未使用的索引:

// MongoDB 4.0+
db.orders.aggregate([{ $indexStats: {} }])
// 看每个索引的 accesses.ops,长期为 0 说明没用,可以删

事务的代价

MongoDB 4.0 的多文档事务设计上是"对必要时支持",而不是"取代单文档原子操作"。性能差距:

  • 单文档原子操作:几十万 QPS。
  • 多文档事务:几千 QPS。

所以"schema 设计避免多文档事务"是最佳实践 —— 把相关数据放一个文档里,改一个文档就是原子的。

WiredTiger 存储引擎

MongoDB 3.0 起默认 WiredTiger,核心特性:

  • 压缩:Snappy(快,默认)/ Zlib(更省空间,慢)/ Zstd(平衡)。
  • Document-level locking:行级锁,比之前的库级锁并发好得多。
  • Cache:用一半物理内存做 cache,装下"working set"性能最佳。
  • Journal:WAL 日志,默认每 100ms 刷盘,保证耐久。

实战:从 SQL 思维过渡到 MongoDB 思维

// SQL 风格(反模式)
db.users.find({ _id: 1 })
// 拿到 user 后,再 db.orders.find({ userId: 1 })
// 来回查多次

// MongoDB 风格(更好):嵌入
{
    _id: 1,
    name: "...",
    orders: [
        { id: 101, total: 100, ... },
        { id: 102, total: 200, ... }
    ]
}
// 一次查询拿全部
db.users.find({ _id: 1 })

注意:不是所有数据都嵌入。订单数量可控就嵌入,如果一个用户有几千个订单,嵌入会让文档膨胀超过 16MB。这种情况下用引用 + $lookup。

写在最后

MongoDB 不是"NoSQL"那么简单 —— 它是一套以"文档"为中心的完整数据库系统,有自己的查询语言、聚合体系、副本集、分片、事务。把它当"能存 JSON 的数据库"用的人感受到的痛苦,大多源于没真正学它的设计哲学。学透 MongoDB 后,你会发现它在某些场景下比关系数据库灵活得多;但也会清楚意识到,它不能替代关系数据库的所有用途。合适的场景用对工具,这才是数据库选型的根本

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

Redis 完全指南:从 5 种数据结构到 Cluster 部署实战

2026-5-15 16:26:44

技术教程

Elasticsearch 完全指南:从倒排索引到集群部署的实战

2026-5-15 17:25:45

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索