跳转至

MongoDB 文档存储与聚合管道

🧩 1. MongoDB 的文档模型和关系型数据库有什么区别?适合什么场景?

要理解 MongoDB,就不能用关系型数据库的思维去套。它们不是“谁比谁好”,而是在处理不同形状的数据时,各自有最适合的姿势。

image.png

四个本质区别:

① 数据模型:二维表 vs 嵌套文档 关系型数据库要求你把数据拆成多张表,通过外键关联。比如一个用户和他的订单,需要 users 表和 orders 表,查询时做 JOIN。而 MongoDB 可以把订单直接内嵌在用户文档里,一次查询就拿到完整数据。这种“一个文档就是一个聚合根”的设计,避免了复杂的跨表关联。

// MongoDB 一条用户文档,订单内嵌
{
  "_id": "u123",
  "name": "张三",
  "orders": [
    {"order_id": "o001", "amount": 99.9, "date": "2026-07-01"},
    {"order_id": "o002", "amount": 150.0, "date": "2026-07-05"}
  ]
}
// 一次查询就拿到用户和所有订单,无需 JOIN
db.users.findOne({"_id": "u123"})

② Schema 约束:强 Schema vs 弱 Schema 关系型数据库建表时必须定义好所有列和数据类型,后期加字段要 ALTER TABLE,这在数据量巨大时会锁表。MongoDB 的集合不需要预定义字段,每个文档可以有不同的结构。这在需求快速迭代、字段经常变化时非常灵活。但这个灵活性也有代价——应用层需要自己保证数据一致性,不能全依赖数据库的约束。

③ 关联方式:JOIN vs 内嵌/引用 关系型数据库用 JOIN 连接多张表,查询灵活但性能开销大,尤其多表 JOIN 时。MongoDB 推荐用内嵌来避免 JOIN,当数据天然“包含于”主文档时(如订单明细属于订单),直接嵌套进去。当数据需要跨文档共享时,就用引用(类似外键),然后在应用层做二次查询,或者用 $lookup 实现类似 LEFT JOIN 的效果。

④ 事务支持:强 ACID vs 最终一致性

传统观念是 MongoDB 不支持事务,但从 4.0 开始,MongoDB 已经支持多文档 ACID 事务,只是它的实现方式和关系型数据库不同(基于 WiredTiger 的 MVCC)。不过,在分布式环境下,过度依赖事务会拖累性能,MongoDB 的设计哲学依然是“尽可能用单文档原子操作代替多文档事务”。

场景选型决策树:

你的数据是高度结构化、互相之间关系复杂、需要频繁跨表查询?
  ├─ 是 → 关系型数据库。如金融账务、ERP 系统、报表系统。
  └─ 否 → 你的数据是半结构化、字段不固定、或是“聚合根”模式?
          ├─ 是 → MongoDB。
          │       · 内容管理(博客、商品详情)
          │       · 物联网时序数据(每个设备上报的 JSON 日志)
          │       · 用户画像(字段随业务频繁变化)
          │       · 实时分析(大数据写入,低延迟查询)
          └─ 否 → 你有大量 JOIN 需求且数据一致性要求极高?
                  └─ 关系型数据库。

同时也可以用混合架构:MongoDB 存业务主数据,MySQL 存订单和财务数据。

示例代码:灵活 Schema 的实际好处

# 同一集合中,不同结构的文档可以共存
db.products.insert_many([
    {"name": "iPhone", "price": 6999, "colors": ["black", "white"]},
    {"name": "MacBook", "price": 12999, "chip": "M4", "ram": "16GB"},
    {"name": "AirPods", "price": 1399}  # 缺少额外字段也完全合法
])

🔧 2. MongoDB 的聚合管道(Aggregation Pipeline)怎么用?有哪些常用阶段?

聚合管道是 MongoDB 的数据处理流水线,它借鉴了 Unix 命令行的管道思想:文档从一端流入,依次经过多个处理阶段,每个阶段对数据做一次变换,最终输出你想要的结果。

image.png

常用阶段速查表:

查看内嵌表格

实战示例:从一个用户订单集合中,统计每个用户的消费总额,并按金额降序排列。

db.orders.aggregate([
    // 阶段 1: 过滤,只看已完成订单
    { $match: { status: "completed" } },

    // 阶段 2: 分组,按 user_id 分组,计算总金额和订单数
    { $group: {
        _id: "$user_id",
        totalAmount: { $sum: "$amount" },
        orderCount: { $sum: 1 }
    }},

    // 阶段 3: 排序,按总金额降序
    { $sort: { totalAmount: -1 } },

    // 阶段 4: 限制,只取前 10 名
    { $limit: 10 },

    // 阶段 5: 投影,美化输出
    { $project: {
        _id: 0,
        userId: "$_id",
        totalAmount: 1,
        orderCount: 1,
        avgAmount: { $divide: ["$totalAmount", "$orderCount"] }
    }}
])

$lookup 示例:关联用户表,补全用户姓名

db.orders.aggregate([
    { $match: { status: "completed" } },
    { $group: {
        _id: "$user_id",
        totalAmount: { $sum: "$amount" }
    }},
    // 左连接 users 集合
    { $lookup: {
        from: "users",
        localField: "_id",
        foreignField: "_id",
        as: "user"
    }},
    // unwind 将数组展开为对象
    { $unwind: "$user" },
    { $project: {
        _id: 0,
        userId: "$_id",
        userName: "$user.name",
        totalAmount: 1
    }}
])

$facet 示例:同时做多个维度的统计,互不干扰

db.products.aggregate([
    { $facet: {
        "priceStats": [
            { $group: { _id: null, avgPrice: { $avg: "$price" } } }
        ],
        "categoryCount": [
            { $group: { _id: "$category", count: { $sum: 1 } } }
        ],
        "top5Expensive": [
            { $sort: { price: -1 } },
            { $limit: 5 }
        ]
    }}
])
// 一次查询同时返回三组结果,适合 Dashboard 场景

性能优化提示:

  • $match$sort 尽量放在管道最前面,利用索引快速削减数据量。

  • $group 前尽量用 $match 减少进入分组的数据。

  • 使用 allowDiskUse: true 允许 MongoDB 在处理超大聚合时使用磁盘临时空间,避免内存不足错误。


📝 3. MongoDB 的 CRUD 操作有哪些?updateOne 和 findAndModify 有什么区别?

MongoDB 的 CRUD 操作和 SQL 有清晰的对应关系,但它的原子性边界和返回机制更加灵活。

核心 CRUD 方法一览:

查看内嵌表格

updateOne 和 findAndModify 的本质区别:

这是两个经常会让人犹豫的方法。核心差异在于原子性边界和返回值。

updateOne(filter, update, options)
  · 操作:只更新,不返回原始文档
  · 返回值:{ acknowledged: true, matchedCount: 1, modifiedCount: 1 }
  · 原子性:更新操作本身是原子的,但“查+改”不是原子的(两个独立步骤)

findOneAndUpdate(filter, update, options)
  · 操作:原子地查找文档,更新它,然后返回文档
  · 返回值:根据选项,返回更新前或更新后的完整文档
  · 原子性:“查找并修改”整个过程在一个原子操作中完成

关键场景对比:

① 并发安全 当你需要先读一个值,再基于这个值做修改时,updateOne 存在竞态条件。典型场景:库存扣减。

// ❌ 不安全:两步操作,中间可能被其他请求修改
let product = db.products.findOne({ _id: "p001" });
if (product.stock > 0) {
    db.products.updateOne(
        { _id: "p001" },
        { $inc: { stock: -1 } }
    );
}

// ✅ 安全:用 updateOne + 条件更新,原子性由单文档保证
let result = db.products.updateOne(
    { _id: "p001", stock: { $gt: 0 } },  // 过滤条件同时检查库存
    { $inc: { stock: -1 } }
);
if (result.modifiedCount === 0) {
    print("库存不足或扣减失败");
}

// ✅ 更优:用 findOneAndUpdate,既原子扣减,又返回扣减后的文档
let updated = db.products.findOneAndUpdate(
    { _id: "p001", stock: { $gt: 0 } },
    { $inc: { stock: -1 } },
    { returnDocument: "after" }  // 返回更新后的文档
);
if (!updated) {
    print("库存不足");
} else {
    print(`扣减成功,剩余库存: ${updated.stock}`);
}

② 需要返回原文档的场景 当你需要“取出一个待处理的任务,同时把它标记为处理中”,findOneAndUpdate 是最佳选择,它能在一个原子操作里完成“查找并更新”,并返回修改前的任务信息。

// 原子地取出一个待处理任务,并将状态改为 processing
let task = db.tasks.findOneAndUpdate(
    { status: "pending" },
    { $set: { status: "processing", worker_id: "w001" } },
    { returnDocument: "before", sort: { created_at: 1 } }
);
// task 就是那个被抢到的任务,其他 worker 不会再抢到它

③ 更新操作的多样性 MongoDB 的更新操作符非常丰富,这些都可以在 updateOnefindOneAndUpdate 中使用:

查看内嵌表格

选项参数对比:

// updateOne 常用选项
db.collection.updateOne(filter, update, {
    upsert: true,       // 如果不存在则插入
    arrayFilters: [...] // 数组过滤条件
});

// findOneAndUpdate 额外选项
db.collection.findOneAndUpdate(filter, update, {
    returnDocument: "before" | "after",  // 返回更新前/后文档
    sort: { field: 1 },                  // 匹配多个时,选择第一个用于更新
    projection: { field: 1, _id: 0 },    // 返回哪些字段
    upsert: true
});

选择决策表:

查看内嵌表格

🔄 1. MongoDB 的事务支持(4.0+)是怎样的?和 MySQL 事务有什么区别?

从 4.0 版本开始,MongoDB 正式支持多文档 ACID 事务,打破了“NoSQL 无事务”的刻板印象。但它的实现方式和 MySQL 有显著不同,这些差异会直接影响你在 Agent 系统中如何设计数据一致性方案。

MongoDB 事务的核心特征:

image.png

代码示例:一个简单的转账事务

const session = client.startSession();

try {
    session.startTransaction({
        readConcern: { level: "snapshot" },
        writeConcern: { w: "majority" }
    });

    const accounts = session.getDatabase("bank").collection("accounts");

    // 扣减 A 账户
    const resultA = await accounts.updateOne(
        { _id: "A", balance: { $gte: 100 } },
        { $inc: { balance: -100 } },
        { session }
    );

    if (resultA.modifiedCount === 0) {
        throw new Error("A 账户余额不足");
    }

    // 增加 B 账户
    await accounts.updateOne(
        { _id: "B" },
        { $inc: { balance: 100 } },
        { session }
    );

    await session.commitTransaction();
    console.log("转账成功");
} catch (error) {
    await session.abortTransaction();
    console.log("转账失败,已回滚:", error.message);
} finally {
    session.endSession();
}

与 MySQL 事务的核心区别:

查看内嵌表格

选型建议:

  • MongoDB 事务适合低频但需要跨多文档原子性的场景,比如年终奖结算、复杂的库存转移。

  • 如果是对事务性能要求极高的 OLTP 系统(如高频订单),建议仍用 MySQL。

  • 在 AI Agent 场景中,事务常用于保证任务状态与执行记录的一致性,比如同时更新任务状态和写入操作日志。


⚖️ 2. MongoDB 的副本集机制是怎样的?如何实现高可用?

副本集是 MongoDB 高可用的基石。它通过一主多从 + 自动故障转移的架构,确保在单台服务器宕机时,系统仍能正常提供服务。

image.png

三大核心组件:

  • 主节点:唯一接受写入的节点。所有写操作记录到 oplog(操作日志)。

  • 从节点:异步从主节点拉取 oplog,重放操作以保持数据一致。默认不可写入,但可配置读取。

  • 仲裁者:不存储数据,只在选举时投票,帮助打破平局。资源消耗极小。

自动故障转移流程:

1. 心跳检测:所有节点每 2 秒互发心跳。如果从节点在 10 秒内未收到主节点心跳,
   则判定主节点失联。

2. 选举触发:满足选举条件的从节点发起选举,向其他节点请求投票。

3. 投票规则:
   · 节点的 priority 值(0-1000,0 表示永不成为主)
   · optime(数据最新程度):oplog 时间戳越新,越容易当选
   · 多数派原则:必须获得超过半数节点的投票

4. 新主诞生:得票最多的从节点晋升为新主,其他从节点自动指向它同步。

5. 旧主恢复:旧主重新加入后,发现自己已不是主,自动降级为从节点。

高可用的具体实现手段:

① Write Concern(写关注)

控制写入数据被复制到多少个节点后才返回成功。值越高,数据安全性越强,但写入延迟越大。

// w: 1 默认,只要主节点写入即返回
db.accounts.insertOne({ _id: "C", balance: 500 });

// w: "majority" 必须复制到多数节点才返回,强一致性
db.accounts.insertOne(
    { _id: "D", balance: 1000 },
    { writeConcern: { w: "majority", wtimeout: 5000 } }
);

② Read Concern(读关注) 控制读取数据的隔离级别。"majority" 保证只读已被多数节点确认的数据,防止读到之后可能被回滚的“脏数据”。

③ Read Preference(读偏好) 控制客户端从哪种节点读取数据。primary 默认只从主节点读;secondaryPreferred 优先从从节点读以分担负载。

④ 副本集配置示例

rs.initiate({
    _id: "rs0",
    members: [
        { _id: 0, host: "mongo1:27017", priority: 10 },
        { _id: 1, host: "mongo2:27017", priority: 5 },
        { _id: 2, host: "mongo3:27017", arbiterOnly: true }  // 仲裁者
    ]
});

⑤ 客户端高可用连接

from pymongo import MongoClient

# 连接字符串中列出所有副本集成员,驱动会自动发现主节点
client = MongoClient(
    "mongodb://mongo1:27017,mongo2:27017,mongo3:27017/?replicaSet=rs0",
    w="majority",
    readPreference="secondaryPreferred"
)
# 当主节点故障,驱动会自动重连到新选举的主节点

🔍 3. MongoDB 的索引类型有哪些?如何设计高效索引?

索引是任何数据库性能的核心。MongoDB 的索引体系非常丰富,远超关系型数据库的 B-Tree 单一模式。

常见索引类型:

查看内嵌表格

设计高效索引的 ESR 法则:

ESR 是 MongoDB 索引设计的黄金法则:Equality(等值查询) → Sort(排序) → Range(范围查询)。索引字段应按这个顺序排列。

// 查询:找出 userId=123 且 date 在最近 7 天的订单,按 date 降序
// ❌ 差索引:范围查询在排序前
db.orders.createIndex({ date: -1, userId: 1 });

// ✅ 好索引:等值优先,排序其次,范围最后
db.orders.createIndex({ userId: 1, date: -1 });

索引设计步骤:

  1. 分析查询模式:找出最频繁、最慢的查询。

  2. 使用 explain() 检查索引使用情况:

db.orders.find({ userId: 123, date: { $gte: startDate } })
        .sort({ date: -1 })
        .explain("executionStats");
// 查看 winningPlan 中的 stage:
//   IXSCAN 表示使用了索引扫描
//   COLLSCAN 表示全表扫描,需要加索引
// 查看 totalDocsExamined vs nReturned,比值越大索引效果越差
  1. 优先创建覆盖索引:如果查询需要的所有字段都在索引中,MongoDB 可以直接从索引返回结果,无需访问实际文档,称为“覆盖查询”。
// 查询只返回 userId, date, amount
db.orders.createIndex({ userId: 1, date: -1, amount: 1 });
db.orders.find(
    { userId: 123 },
    { _id: 0, userId: 1, date: 1, amount: 1 }
).explain("executionStats");
// 如果 winningPlan.stage === "PROJECTION_COVERED",则是覆盖查询
  1. 监控索引大小和性能:定期用 db.collection.stats() 查看索引大小,用 db.collection.aggregate([{$indexStats:{}}]) 查看哪些索引实际被使用。

在 Agent 场景中的索引实践:

  • 对话历史查询:{ session_id: 1, created_at: -1 } 支持快速拉取某个会话的最近消息。

  • 任务队列消费:{ status: 1, priority: -1, created_at: 1 } 让 Worker 能高效取到待处理任务。

  • 知识库检索元数据过滤:{ tenant_id: 1, doc_type: 1, embedding_score: -1 } 配合向量搜索的结果进行权限过滤和排序。


🧩 1. MongoDB 的分片集群是如何工作的?如何选择分片键?

当单台服务器的内存、磁盘或 CPU 无法承载数据量和吞吐量时,就需要将数据水平拆分到多台服务器上。MongoDB 的分片集群正是为此而生。

分片集群的三大组件

image.png

  • mongos:无状态的路由节点,负责接收客户端请求,根据分片键和配置信息将请求路由到对应的 Shard,并合并结果。可以部署多实例实现负载均衡。

  • Config Server:存储集群的元数据,包括数据如何分布(分片键范围与 Shard 的映射)。必须是副本集以保证高可用。

  • Shard:真正存储数据的单元。每个 Shard 是一个独立的副本集,拥有完整的数据副本和高可用能力。

数据分布与 Chunk 机制

数据根据分片键的值被切分成一个个 Chunk(默认大小 64MB)。当一个 Chunk 过大或数据分布不均时,MongoDB 会在 Shard 之间自动迁移 Chunk,这个过程叫做自动均衡。

分片键: user_id (范围分片)
═════════════════════════════════
Shard A: user_id [ minKey  ─── 1000 ]
Shard B: user_id [ 1001   ─── 2000 ]
Shard C: user_id [ 2001   ─── maxKey ]

如何选择分片键?选择分片键是分片集群设计中最关键的决策,一旦选定很难更改。 需要同时考虑两个目标:数据均匀分布和查询隔离。

查看内嵌表格

高效分片键的四个黄金法则

  1. 高基数:分片键的值应有足够多的不同取值。比如 country 只有几十个值,数据最多分布在几十个 Chunk,无法实现真正的水平扩展。

  2. 均匀分布:写入操作应尽可能均匀地分散到所有 Shard,避免某个 Shard 成为热点。哈希分片或复合分片键可以有效解决递增键的热点问题。

  3. 查询隔离:绝大多数查询应能根据分片键定位到单个 Shard(称为定向查询),而不是广播到所有 Shard。比如一个 SaaS 系统,查询总是带上 company_id,那么用 company_id 做分片键就很理想。

  4. 避免单调递增:ObjectId 和自增 ID 的前缀具有时间单调性,如果直接作为范围分片键,所有最新写入都会打到同一个 Shard。应改用哈希分片,或组合上另一个高基数前缀字段。

示例代码:创建分片集群并指定分片键

// 1. 连接到 mongos
mongos> sh.addShard("shard1/localhost:27018")
mongos> sh.addShard("shard2/localhost:27019")

// 2. 对数据库启用分片
mongos> sh.enableSharding("agent_db")

// 3. 选择哈希分片键 (user_id)
mongos> sh.shardCollection("agent_db.sessions", { "user_id": "hashed" })

// 4. 或选择复合范围分片键 (tenant_id + created_at)
mongos> sh.shardCollection("agent_db.tasks", { "tenant_id": 1, "created_at": 1 })

⚖️ 2. MongoDB 和 Elasticsearch 在搜索场景如何选型?各自适合什么场景?

很多团队会把 MongoDB 和 Elasticsearch 都部署起来,让 MongoDB 做主存储,Elasticsearch 做搜索引擎,并通过 Change Stream 或消息队列保持数据同步。理解它们各自的核心优势,就能在合适的场景下减掉不必要的组件。

核心区别:

查看内嵌表格

MongoDB 自己的全文索引 MongoDB 提供了 text 索引,支持分词、停用词、加权和相关性排序。对于简单的关键词搜索和产品内搜索,完全够用。

// 创建文本索引
db.articles.createIndex({ title: "text", content: "text" });

// 全文搜索
db.articles.find({ $text: { $search: "Agent 性能优化" } })
           .sort({ score: { $meta: "textScore" } })
           .limit(10);

选型决策树

你的搜索需求是什么?
├─ 主要是基于字段的精确筛选?(如 user_id=123 AND status="active")
│   └─ MongoDB 足够。复合索引 + 聚合管道高效且简单。
├─ 需要全文搜索,但不是核心功能?(如站内文章搜索,数据量 < 100万)
│   └─ MongoDB 的 text 索引。架构简单,避免数据同步的复杂性。
├─ 核心功能是强大的全文搜索?(如电商、文档检索、日志分析)
│   └─ Elasticsearch。它的倒排索引、分词器、相关性评分远强于 MongoDB。
├─ 需要复杂的聚合分析 + 可视化?
│   └─ Elasticsearch + Kibana,或者 MongoDB + Metabase/Grafana。
└─ 数据一致性要求极高,需要事务?
    └─ MongoDB 做主存储。如果需要强搜索,再通过 Change Stream 同步到 ES。

混合架构:MongoDB 做主存储 + Elasticsearch 做搜索引擎

# MongoDB Change Stream 监听数据变更,实时同步到 Elasticsearch
with mongo_client.watch([{'$match': {'operationType': {'$in': ['insert', 'update', 'replace']}}}]) as stream:
    for change in stream:
        doc = change['fullDocument']
        es_client.index(index='products', id=str(doc['_id']), body=doc)

这种架构适合既要事务和灵活数据模型,又要强大搜索体验的场景,比如电商、内容平台、Agent 的知识库搜索。代价是需要维护两套存储和数据同步链路。


🌊 3. MongoDB 的 Change Stream 是什么?在 Agent 场景如何应用?

Change Stream 就像数据库的“事件流”,它允许应用监听集合或数据库级别的实时数据变更,而无需轮询或解析 oplog。 它基于副本集的 oplog 实现,提供了安全、可恢复的变更数据捕获(CDC)能力。

image.png

在 Agent 场景下的典型应用:

① 实时任务状态驱动 在一个多 Agent 协作系统中,Orchestrator 将任务写入 MongoDB 的 tasks 集合,Worker Agent 通过 Change Stream 监听分配给自己的任务,实现事件驱动的任务调度,无需轮询。

import pymongo

client = pymongo.MongoClient("mongodb://localhost:27017/?replicaSet=rs0")
collection = client.agent_db.tasks

# 监听分配给当前 Worker 的新任务
pipeline = [
    {'$match': {
        'operationType': 'insert',
        'fullDocument.assigned_to': 'worker_1',
        'fullDocument.status': 'pending'
    }}
]

with collection.watch(pipeline) as stream:
    for change in stream:
        task = change['fullDocument']
        print(f"收到新任务: {task['_id']}")
        # 执行任务并更新状态
        collection.update_one(
            {'_id': task['_id']},
            {'$set': {'status': 'processing'}}
        )
        result = execute_task(task)
        collection.update_one(
            {'_id': task['_id']},
            {'$set': {'status': 'completed', 'result': result}}
        )

② 知识库文档变更与 Embedding 更新

当运营人员在知识库中新增或修改文档时,Agent 需要立即感知并重新生成 Embedding,以保证 RAG 系统的检索结果总是基于最新内容。

# 监听 knowledge 集合的插入和更新事件
pipeline = [{'$match': {'operationType': {'$in': ['insert', 'update', 'replace']}}}]

with client.agent_db.knowledge.watch(pipeline) as stream:
    for change in stream:
        doc = change['fullDocument']
        # 重新生成 Embedding
        embedding = generate_embedding(doc['content'])
        # 更新文档中的向量字段
        client.agent_db.knowledge.update_one(
            {'_id': doc['_id']},
            {'$set': {'embedding': embedding}}
        )

③ 审计与监控

Change Stream 可用于实时监控敏感操作的变更(如用户权限修改、系统配置变更),并将事件推送到告警系统或审计日志。

Change Stream 的可靠性保障:

  • 断点续传:每个 Change Event 都带有一个 _id,它基于 oplog 的时间戳和序号。如果连接断开,可以从上次记录的 resumeToken 继续监听,不会丢失事件。

  • 持久化消费点:将 resumeToken 保存在外部存储(如 Redis 或数据库)中,即使应用重启也能精确续接。

resume_token = load_resume_token_from_redis()  # 从 Redis 读取上次保存的位置
stream = collection.watch(resume_after=resume_token)

for change in stream:
    process(change)
    save_resume_token_to_redis(change['_id'])  # 每处理一条,更新游标

收束:

Change Stream 让 MongoDB 从一个被动响应的数据库,变成了一个能主动“推”消息的实时数据中枢。在 Agent 系统中,它扮演着事件总线的角色——把任务、文档、状态的变化实时分发给各个 AI Worker,让整个多 Agent 集群的协作变得丝滑且可追溯。


⚡ 4. MongoDB 的性能如何优化?如何分析慢查询?

MongoDB 的性能优化,通常遵循“先诊断,后下药”的原则。首先要抓住慢查询,然后根据执行计划和资源使用情况做针对性优化。

诊断工具链:

  • Profiler:开启数据库的慢查询日志,自动记录执行时间超过阈值的操作。

  • explain():查看一条查询的具体执行计划,判断是否走了索引、扫描了多少文档。

  • mongostat / mongotop:实时监控实例的吞吐量、锁等待、读写比例等。

  • currentOp() / killOp():查看当前正在执行的操作,紧急时可以杀掉导致雪崩的慢查询。

开启慢查询分析:

// 设置慢查询阈值为 100 毫秒
db.setProfilingLevel(1, { slowms: 100 })

// 查询最近的慢查询记录
db.system.profile.find().sort({ ts: -1 }).limit(5).pretty()

分析慢查询的四个关键指标: 使用 explain("executionStats") 可以得到详细的执行统计,重点关注:

db.orders.find({ user_id: 123, status: "completed" }).explain("executionStats")
  • executionStats.executionTimeMillis:总执行时间。

  • executionStats.totalDocsExamined:扫描的文档总数。

  • executionStats.nReturned:实际返回的文档数。

  • queryPlanner.winningPlan.stageIXSCAN 表示使用了索引,COLLSCAN 表示全表扫描。

优化方向一:索引优化

全表扫描是慢查询的头号杀手。优化索引的黄金法则是 ESR 规则:Equality (等值查询) → Sort (排序) → Range (范围查询)。

// 查询:user_id = 123 且 created_at > start,按 created_at 降序
// ❌ 差索引
db.orders.createIndex({ created_at: -1, user_id: 1 })
// ✅ 好索引:等值优先
db.orders.createIndex({ user_id: 1, created_at: -1 })

优化方向二:查询与写入优化

  • 使用 覆盖索引:让查询所需的所有字段都包含在索引中,MongoDB 可以直接从索引返回数据,无需访问文档(totalDocsExamined 为 0)。

  • 投影:只返回需要的字段,减少网络传输和内存占用。

  • 批量写入:用 insertManybulkWrite 替代逐条写入,减少网络往返。

优化方向三:硬件与架构

  • 使用 SSD:MongoDB 的 WiredTiger 引擎对随机 IO 很敏感,SSD 对写入和读取的提升非常明显。

  • 配置足够内存:WiredTiger 的缓存越大,热数据命中率越高,磁盘 IO 越少。

  • 读写分离:将报表类和分析类查询指向从节点,减轻主节点压力。

监控与告警脚本示例:

import pymongo
client = pymongo.MongoClient("mongodb://localhost:27017")
db = client.admin
# 获取当前操作,发现执行超过 5 秒的查询
ops = db.current_op({"active": True, "secs_running": {"$gt": 5}})
for op in ops['inprog']:
    print(f"慢查询: {op['op']} {op['ns']} 耗时 {op['secs_running']}秒")

MongoDB 存储引擎与底层原理

🧬 1. WiredTiger 的 B-Tree 存储结构、MVCC 实现、Checkpoint 机制、压缩算法

WiredTiger 是 MongoDB 3.2 之后的默认存储引擎,它的设计目标是在高并发写入的场景下,同时保证读写性能和数据一致性。

① B-Tree 存储结构

WiredTiger 使用 B+ Tree 变种 存储数据和索引。它将数据按键排序,叶子节点存储实际数据。与传统的 B-Tree 不同,WiredTiger 的树节点大小是 4KB~32KB,与操作系统页大小对齐,以便高效利用磁盘 IO。它不直接在原位置更新数据,而是采用 Copy-on-Write 的方式:修改一个页面时,会先复制一份,在副本上修改,然后原子地更新父节点的指针。这使得写操作不会直接污染原始的干净页面,天然支持 MVCC。

image.png

② MVCC(多版本并发控制)

WiredTiger 的 MVCC 是基于 事务 ID(Transaction ID) 和 时间戳 实现的。每个文档的每个版本都关联一个事务 ID。当一个写事务开始,它创建数据的新的版本,并标记上自己的事务 ID,而旧版本仍然保留。读事务根据自己开始时的时间戳,只能看到在该时间戳之前已经提交的版本。这样,读操作永远不会阻塞写操作,写操作也不会阻塞读操作。被淘汰的旧版本由后台的垃圾回收线程定期清理

MVCC 版本链:
  Key "name"  → [Version 3: txn=103] → [Version 2: txn=101] → [Version 1: txn=98]
                  读事务 txn=102 只能看到 Version 2

③ Checkpoint 机制

WiredTiger 采用 定期 Checkpoint 的方式将内存中的脏页刷写到磁盘,形成一致性的数据快照。Checkpoint 默认每 60 秒执行一次,也可以在日志文件达到 2GB 时触发。在 Checkpoint 期间,WiredTiger 会创建一个新的快照,然后将该快照中所有脏页写入磁盘。这个过程是增量的,即它只写入自上次 Checkpoint 以来发生变化的页面,而不是全量导出。在 MongoDB 崩溃恢复时,它会从最近的 Checkpoint 开始,并重放之后的 Journal 日志,恢复到崩溃前的一致性状态。

④ 压缩算法

WiredTiger 支持多种压缩算法,以 CPU 换空间。

  • snappy:默认的压缩算法。压缩率适中,压缩和解压速度快,适合大多数场景。

  • zlib:压缩率更高,但 CPU 开销更大,适合存储空间敏感或冷数据。

  • zstd(MongoDB 4.2+):在压缩率和速度之间提供了更好的平衡。

  • none:不压缩,适合对延迟极其敏感且磁盘空间充足的场景。

// 创建集合时指定压缩算法
db.createCollection("logs", {
    storageEngine: {
        wiredTiger: { configString: "block_compressor=zstd" }
    }
})

🚀 2. 为什么 MongoDB 写入性能好?内存配置如何影响性能?

MongoDB 的写入性能之所以出色,并非因为单次写操作有多快,而是因为它把随机写转化为了顺序写,并充分利用了内存来吸收写峰值。

写入性能好的秘密:

  1. WiredTiger 的写入模型 WiredTiger 不直接在磁盘上修改原数据。写入请求到达后,它先将修改记录在内存中的 Journal Buffer(日志缓冲区),然后以顺序追加的方式快速写入 Journal 文件(磁盘上的 WAL)。真正的数据页面修改只发生在内存的缓存中(脏页),由后台的 Checkpoint 线程定期刷盘。也就是说,写操作的延迟主要取决于一次内存更新和一次顺序日志写入,而不是磁盘的随机 IO。

  2. 文档模型的自然优势 MongoDB 的文档是自包含的,通常不需要像关系型数据库那样跨越多个表进行复杂的 JOIN 写入。很多时候,一个业务操作只需要更新一个 BSON 文档,MongoDB 原生保证单文档操作的原子性,无需分布式事务的开销。

  3. 高并发下的无锁争用 WiredTiger 使用 意向锁 和 MVCC,使得多个写操作可以并发地在不同的文档上执行,读操作则不会被写操作阻塞。只有对同一个文档的并发写才会发生锁竞争。

内存配置如何影响性能?

WiredTiger 内部缓存的默认大小是 (系统内存 - 1GB) * 50%。这个缓存用于存放未压缩的 B-Tree 页面(数据页和索引页)。内存越大,能缓存的热数据就越多,磁盘 IO 就越少,性能就越好。

  • 缓存不足:当工作集(经常访问的数据和索引)超过缓存大小,WiredTiger 需要频繁将不常用的页面驱逐出缓存,并从磁盘读取新的页面。这会导致 Page Fault 飙升,磁盘 IO 成为瓶颈,请求延迟显著增加。表现为 mongostat 中的 faults 字段升高。

  • 缓存过大:虽然性能可能更好,但会挤压操作系统和其他进程的内存,可能导致 OOM。

配置 WiredTiger 缓存大小:

# mongod.conf
storage:
  wiredTiger:
    engineConfig:
      cacheSizeGB: 8  # 固定大小,推荐为物理内存的 60% 左右

监控缓存使用情况:

// 查看缓存使用统计
db.serverStatus().wiredTiger.cache
/*
{
  "bytes currently in the cache": 1234567890,
  "maximum bytes configured": 8589934592,
  "pages evicted by application threads": 120,
  "pages read into cache": 50000,
  ...
}
*/

如果 pages evicted by application threads(应用线程被迫参与驱逐页面)持续很高,说明缓存不足,需要扩容内存或增加缓存配置。

一句话收束: MongoDB 的写入高性能来自“日志顺序写 + 内存缓存修改 + 后台异步刷盘”的组合拳。而 WiredTiger 缓存则是这套机制的发动机——它越大,数据在内存中待得越久,磁盘的负担就越轻,整个系统的吞吐量就越能稳定在峰值。在生产中,监控缓存驱逐率、命中率和脏页比例,是判断内存配置是否合理的关键。


Oplog 机制深挖

⚙️ 1. Oplog 是什么?存在哪里?有什么特点?

Oplog 是 Operation Log 的缩写,它是 MongoDB 副本集实现数据复制的核心。你可以把它理解为主库的 “命令回放清单”——主库上的每一次写入(增、删、改),都会被记录成一条幂等的操作日志,从库则不断地拉取并重放这些日志,以维持和主库的数据一致。

┌─────────────┐                    ┌─────────────┐
│   Primary   │                    │  Secondary   │
│             │   Oplog 复制       │             │
│  写入操作   │──────────────────→│  读取 Oplog  │
│   ↓        │                    │  重放操作    │
│  本地 Oplog │                    │  本地 Oplog  │
└─────────────┘                    └─────────────┘

物理存储位置: Oplog 存储在 local 数据库 下的一个固定集合(Capped Collection)中,名为 oplog.rs。因为是固定集合,它有严格的大小上限,当集合被写满时,最旧的记录会被自动覆盖删除,类似于一个环形缓冲区。

核心特点:

  • 固定集合:Oplog 是固定大小,由 oplogSizeMB 参数在初始化时指定,后续可动态修改。它在磁盘上占用连续空间,写入性能极高。

  • 记录的是操作,不是数据变更:Oplog 中记录的是类似 { op: "i", ns: "test.orders", o: { _id: 1, item: "apple" } } 这样的语句,而不是行级的变更前后数据。

  • 严格时间排序:Oplog 的每条记录都带有一个时间戳 ts(基于集群时间),从库正是根据这个时间戳来追踪自己复制到了哪个位置。

  • 幂等性设计:所有操作都被转换成一种“无论执行多少次,最终状态都一致”的形式,这是保证从库可以安全重放的基础。

一条典型的 Oplog 记录长什么样?

// 插入操作
{
  "ts": Timestamp(1690000000, 1),  // 时间戳,全局唯一
  "t": NumberLong(1),              // 事务 ID(单文档写作为 1)
  "h": NumberLong("1234567890"),   // 哈希值,用于校验
  "v": 2,                          // 版本
  "op": "i",                       // 操作类型: i=insert, u=update, d=delete
  "ns": "test.orders",             // 命名空间
  "o": { _id: ObjectId("..."), item: "apple", qty: 10 }  // 插入的完整文档
}

// 更新操作
{
  "ts": Timestamp(1690000005, 1),
  "op": "u",
  "ns": "test.orders",
  "o2": { _id: ObjectId("...") },   // 更新条件,通常是 _id
  "o": { "$set": { "qty": 5 } }     // 更新操作符
}

为什么用固定集合?

因为 MongoDB 的设计哲学是“复制不是为了永久存档,而是为了短期同步”。固定集合既省去了复杂的日志清理逻辑,又保证了写入速度。主从同步只要追得上这个“滑动窗口”,就不会出问题。


🔁 2. Oplog 的幂等性设计是怎样的?固定集合大小如何影响复制?Oplog 窗口与复制延迟有什么关系?

2.1 幂等性设计

幂等性意味着:同一条操作在从库上执行一次和执行多次,最终的数据结果完全相同。 这是保证从库安全重放 Oplog 的基石。MongoDB 通过以下设计保证幂等性:

  • insert 转换为 upsert:对于插入操作,从库在执行时会将其视为“如果 _id 已存在则不做任何事”。这样即使因为网络波动导致 Oplog 重复发送,也不会插入重复文档。

  • update 使用操作符,而非覆盖值:Oplog 中记录的 update 通常使用 $set$inc 等操作符(如上例中的 { "$set": { "qty": 5 } }),这些操作符执行多次的效果和执行一次一样,不会因为重复执行而把字段加成意外值。如果无法使用操作符(例如用户执行的更新是直接替换整个文档),Oplog 会记录替换后的完整文档,这样重放多次也只是反复写入同一个文档。

  • delete 变为条件删除:删除操作被记录为 { op: "d", o: { _id: ... } },从库只根据 _id 删除。即使文档已经被删过一次,再次执行也不会报错或产生副作用。

// 主库执行:db.orders.updateOne({ _id: 1 }, { $inc: { qty: 1 } })
// Oplog 记录为:{ op: "u", o2: { _id: 1 }, o: { "$inc": { "qty": 1 } } }
// 从库无论执行多少次,最终 qty 都只增加 1

2.2 固定集合大小与 Oplog 窗口

Oplog 固定集合的大小直接决定了 Oplog 窗口——即主库能“记住”多长时间的写操作历史。

Oplog 容量 (oplogSizeMB)
Oplog 窗口 (时间)
从库能容忍的最大离线时间

计算公式(经验值):

Oplog 窗口 (秒) ≈ Oplog 容量 (MB) / 平均写入速率 (MB/s)

例如,Oplog 容量为 5 GB,主库平均写入速率为 1 MB/s,那么 Oplog 窗口大约就是 5000 秒(约 83 分钟)。如果某个从库因为网络中断、维护重启等原因离线超过 83 分钟,它重新连接后会发现,自己上次同步到的那个 ts 时间戳对应的 Oplog 记录已经被新写入覆盖了。此时,从库就无法通过增量 Oplog 追上主库,而是会进入 RECOVERING 状态,需要执行一次全量同步(Initial Sync)来重建数据。

2.3 复制延迟与 Oplog 的关系

复制延迟(Replication Lag)是指从库重放 Oplog 的速度慢于主库写入速度的程度。Oplog 窗口决定了从库能承受的最大复制延迟,而不是复制延迟本身。

  • Oplog 窗口大:即使复制延迟偶尔飙高,只要不超出窗口,从库依然可以慢慢追上。

  • Oplog 窗口小:稍有网络抖动或从库负载升高,复制延迟就很容易超过窗口,导致从库被迫全量同步,这又会反过来加剧主库的 IO 和网络压力,形成恶性循环。

监控命令:

// 查看主库的 Oplog 状态
rs.printReplicationInfo()
// 输出示例:
// configured oplog size:   5120 MB
// log length start to end: 4500 secs (1.25 hrs)
// oplog first event time:  Mon Jul 05 2026 10:00:00 GMT+0800
// oplog last event time:   Mon Jul 05 2026 11:15:00 GMT+0800

// 查看从库的复制延迟
rs.printSecondaryReplicationInfo()
// 输出示例:
// source: mongo2:27017
//     syncedTo: Mon Jul 05 2026 11:14:00 GMT+0800
//     replLag: 60 secs

🔧 3. Oplog 窗口过小导致从节点脱离同步时如何排查和处理?

故障现象: 从库状态显示 RECOVERING,日志中频繁出现 "could not find member in sync source's oplog""we are too stale to catch up" 等错误。在主库上执行 rs.printReplicationInfo() 发现 Oplog 窗口只有几分钟,而复制延迟经常超过这个值。

排查流程:

第一步:确认 Oplog 窗口与写入压力

// 在主库上查看 Oplog 窗口时长
rs.printReplicationInfo()
// 关注 log length start to end,如果只有几分钟甚至几秒,说明窗口过小。

同时用 mongostat 监控主库的写入速率(insert/update/delete per second),估算所需的 Oplog 大小。

第二步:检查从库的复制延迟

// 在从库上执行
rs.printSecondaryReplicationInfo()

如果 replLag 经常超过 Oplog 窗口,说明从库要么性能跟不上,要么网络不稳定。

第三步:分析从库的瓶颈

  • 从库的磁盘 IO 是否打满?(iostat

  • 从库的 CPU 或内存是否不足?

  • 从库是否承载了过多的读请求?(如果是,考虑将读流量迁走或增加从库)

解决方案(按优先级排序):

方案一:动态调整 Oplog 大小(最直接、无需停服)

MongoDB 3.6+ 支持在线调整 Oplog 大小,无需重启。只需确保调整后的值大于当前值。

// 将 Oplog 调整为 20GB (20480MB)
db.adminCommand({ replSetResizeOplog: 1, size: 20480 })

调整后,Oplog 窗口会立即扩大,给从库更多时间追数据。但要注意,调整 Oplog 会占用磁盘空间,需提前确认磁盘余量。

方案二:增加 Oplog 大小并重启(早期版本或需要同步改配置) 在 mongod.conf 中增加 replication.oplogSizeMB 配置项,并重启实例。这需要滚动重启副本集成员,过程中要保证主库可用。

方案三:优化写入模型 如果写入速率过高是因为无效的逐条写入,改用 insertManybulkWrite 或聚合管道来减少 Oplog 记录数。同时,检查是否有什么计划外的全量更新或 TTL 索引导致频繁删除。

方案四:扩容从库硬件

如果从库的 IO 或内存成为重放 Oplog 的瓶颈,升级从库的磁盘到 SSD,或增加 WiredTiger 缓存,以提升重放速度。

方案五:重新同步(当从库已完全脱离时)

如果从库已经无法通过 Oplog 追上来,唯一的选择是重新做一次全量同步。

// 在从库上执行,强制重新同步
db.adminCommand({ resync: 1 })
// 或者更推荐的方式:停止从库,清空数据目录,重新加入副本集,让它自动 Initial Sync

重新同步期间,从库无法提供读服务,且会给主库带来较大的 IO 和网络压力,应在低峰期执行。

监控预警脚本示例(Python):

import pymongo

client = pymongo.MongoClient("mongodb://primary:27017")
admin_db = client.admin

# 获取 Oplog 信息
oplog_status = admin_db.command("replSetGetStatus")
for member in oplog_status['members']:
    if member['stateStr'] == 'PRIMARY':
        primary_optime = member['optime']
    if member['stateStr'] == 'SECONDARY':
        lag = primary_optime['ts'].time - member['optime']['ts'].time
        if lag > 600:  # 超过 10 分钟告警
            print(f"CRITICAL: {member['name']} 复制延迟 {lag} 秒")

从根本上预防: 在副本集初始化时,就根据业务规模和增长预期,为 Oplog 分配足够的大小。一个常见的实践是,对于中大型生产集群,Oplog 至少设置为 10GB ~ 50GB,而不是默认的 5% 磁盘空间。你可以用 mongod --oplogSize 20480 在启动时指定,或使用配置文件。


14、文档设计模式:嵌套 vs 引用

📌 1. 什么时候用嵌套文档,什么时候用引用?各有什么优缺点?

这是 MongoDB 文档设计中最根本的权衡。本质上是“数据在一起”和“数据分开”两种哲学的对决。

嵌套文档 (Embedding)                    引用 (Referencing)
┌─────────────────────────┐      ┌──────────┐    ┌──────────┐
│  用户文档               │      │  用户     │    │  订单     │
│  {                      │      │  _id: 1   │    │  user_id:1│
│    _id: 1,              │      │  name:"张"│◄───│  amount:99│
│    name: "张三",         │      └──────────┘    └──────────┘
│    orders: [            │
│      { amount: 99 },    │      数据逻辑独立,通过外键关联
│      { amount: 150 }    │
│    ]                    │
│  }                      │
│  一次查询取全部           │      需要两次查询或 $lookup
└─────────────────────────┘

嵌套文档 (Embedding)

何时使用?

  • 数据之间是“包含”或“一对一”的紧密关系:比如用户和他的收货地址。地址脱离了用户就毫无意义。

  • 数据总是一起被读取:取出用户信息时,几乎总是需要他的头像、昵称、简介,这些就应该嵌在一起。

  • 子数据量小且有限:一个博客文章的几条标签,一个产品的几个变体规格。如果子数组可能无限增长(比如用户的浏览历史),就不适合内嵌。

  • 需要原子性操作:同时更新用户信息和其订单状态,内嵌在一个文档里天然就是原子的。

优点:

  • 读性能极高:一次磁盘 I/O 就能拿到全部关联数据,无需 JOIN。

  • 原子性:单文档操作天然 ACID,更新用户信息的同时修改其内嵌文档,要么全成功,要么全失败。

缺点:

  • 文档可能过大:内嵌数据膨胀会导致超过 16MB 的 BSON 限制,且频繁 IO 大文档影响性能。

  • 数据冗余:同一份数据可能内嵌在多个文档里,更新它就要更新所有副本,容易产生不一致。

  • 查询内嵌细节不便:想查“所有购买了某商品的用户”时,内嵌在用户文档里的订单就非常难查了,这违背了“数据跟着查询走”的原则。

引用 (Referencing)

何时使用?

  • 多对多关系:学生和课程,一个学生选多门课,一门课被多个学生选。

  • 独立生命周期:订单和商品,订单可以独立于商品存在,删除商品不影响历史订单。

  • 子集合可能无限增长:用户的消息列表、操作日志。这些数据无限增长,必然要单独存一个集合。

优点:

  • 文档瘦身:用户文档保持轻量,更新用户信息不影响其成千上万的订单。

  • 数据一致性高:商品信息只存一份,修改后所有引用它的订单都能看到最新数据(在应用层处理)。

  • 灵活查询:可以独立对子集合做复杂的聚合、排序和分页。

缺点:

  • 读性能开销:通常需要多次查询或使用 $lookup 进行关联,消耗更多 IO 和 CPU。

  • 缺乏原子性:跨文档的更新需要事务支持,MongoDB 4.0+ 虽然支持多文档事务,但性能和易用性不如单文档操作。

选择决策树:

数据关系是"包含"吗?
├─ 是 → 子数据量固定且小吗?
│   ├─ 是 → 内嵌 (如用户地址)
│   └─ 否 → 引用 (如用户操作日志)
└─ 否 → 是"多对多"或独立生命周期吗?
    └─ 是 → 引用 (如订单与商品)

📚 2. MongoDB 经典文档设计模式解析

这六种模式是在大量实践中沉淀下来的“套路”,用来解决特定场景下的存储和性能问题。

查看内嵌表格

代码示例:Bucket Pattern 如何存储传感器数据

// 每分钟一条的温度数据,打包成每小时一个文档
{
  "_id": "sensor_01_20260705_10",
  "sensor_id": "sensor_01",
  "bucket_start": ISODate("2026-07-05T10:00:00Z"),
  "bucket_end": ISODate("2026-07-05T10:59:59Z"),
  "sample_count": 60,
  "avg_temp": 25.3,
  "min_temp": 23.1,
  "max_temp": 28.4,
  "temperatures": [
    { "ts": ISODate("..."), "val": 25.1 },
    { "ts": ISODate("..."), "val": 25.2 },
    // ... 60条
  ]
}

这样,一天 1440 条数据被压缩成 24 个文档。查询某小时的温度趋势只需一次磁盘 IO,而不是 60 次。avg_temp 等预计算字段让统计查询瞬间完成。


💬 3. Agent 会话消息如何用 Bucket Pattern 分桶存储?设计方案是什么?

Agent 的会话消息是典型的“海量、持续增长、写多读少”场景。一个用户和 Agent 聊一天可能产生数百条消息,全部内嵌在会话文档里,文档会迅速膨胀到几 MB,导致读写性能恶化。如果每条消息存一个独立文档,查询某次会话的全量历史又需要扫描无数小文档,效率极低。

Bucket Pattern 正是为此而生。

核心思路:将会话消息按会话 ID + 时间窗口分桶存储,每个桶是一个独立的文档,内部用数组存放具体的消息。

用户 Session: sess_abc
═════════════════════════════════════════

Bucket 1: 2026-07-05 10:00 ~ 10:59
┌─────────────────────────────────────┐
│  {                                  │
│    _id: "sess_abc_20260705_10",     │
│    session_id: "sess_abc",          │
│    bucket_start: ISODate("...10:00"),│
│    bucket_end: ISODate("...10:59"), │
│    message_count: 35,               │
│    messages: [                      │
│      { ts: "...", role: "user", content: "..." },│
│      { ts: "...", role: "assistant", content: "..." },│
│      ...                            │
│    ]                                │
│  }                                  │
└─────────────────────────────────────┘

Bucket 2: 2026-07-05 11:00 ~ 11:59
┌─────────────────────────────────────┐
│  ...类似结构...                     │
└─────────────────────────────────────┘

方案设计细节:

① 分桶的粒度选择

  • 按小时:适合消息频率中等的场景,每小时几十到几百条消息,每个桶 50KB~200KB 左右,大小适中。

  • 按天:适合低频交互,比如每天十几条消息。

  • 按消息数:固定每个桶最多存 200 条消息,满了就建新桶。适合消息密度不可预测的场景。

  • 建议优先按小时,因为时间窗口固定,查询历史时按时间段定位桶非常自然。

② 文档结构设计(含索引)

// 创建索引:支持高效查询某个会话的某个时间窗口内的桶
db.session_buckets.createIndex(
    { session_id: 1, bucket_start: 1 },
    { unique: true }
);
// unique 保证同一个会话同一个时间窗口不会有重复的桶

③ 写入逻辑:使用原子操作 $push$inc

async function addMessage(sessionId, message) {
    const now = new Date();
    // 计算当前小时对应的 bucket_start
    const bucketStart = new Date(Date.UTC(
        now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours()
    ));

    const result = await db.session_buckets.findOneAndUpdate(
        {
            session_id: sessionId,
            bucket_start: bucketStart
        },
        {
            $push: { messages: message },
            $inc: { message_count: 1 },
            $setOnInsert: {
                bucket_end: new Date(bucketStart.getTime() + 3600000 - 1)
            }
        },
        {
            upsert: true,
            returnDocument: "after"
        }
    );
    return result;
}

④ 读取逻辑:查询某次会话的完整历史

async function getSessionHistory(sessionId, limit = 100) {
    // 按 bucket_start 降序取最近的桶,直到拿够 limit 条消息
    const buckets = await db.session_buckets.find(
        { session_id: sessionId },
        { messages: 1, bucket_start: 1 }
    ).sort({ bucket_start: -1 }).limit(10).toArray();

    // 从桶里提取消息,按时间排序
    let messages = [];
    for (const bucket of buckets) {
        messages = bucket.messages.concat(messages);  // 正序拼接
    }
    return messages.slice(-limit);  // 返回最近的 N 条
}

⑤ 查询特定时间范围的消息(按时间段检索)

async function getMessagesInRange(sessionId, start, end) {
    const buckets = await db.session_buckets.find(
        {
            session_id: sessionId,
            bucket_start: { $gte: start, $lte: end }
        },
        { messages: 1 }
    ).sort({ bucket_start: 1 }).toArray();

    // 过滤桶内时间范围内的消息
    const result = [];
    for (const bucket of buckets) {
        for (const msg of bucket.messages) {
            if (msg.ts >= start && msg.ts <= end) {
                result.push(msg);
            }
        }
    }
    return result;
}

⑥ 过期清理(TTL 策略) 对于需要自动清理的会话数据,可以在 bucket_start 上建 TTL 索引:

db.session_buckets.createIndex(
    { bucket_start: 1 },
    { expireAfterSeconds: 2592000 }  // 30天后自动删除整个桶
);

Bucket Pattern 带来的好处:

  • 写入性能好:每次写入只是一个原子 $push,不像内嵌文档那样需要频繁搬运整个大文档。

  • 查询效率高:拉取会话历史只需要读几个大文档,而不是几百个小文档。

  • 存储节省:每个桶存多条消息,减少了文档头的开销(文档 ID、元数据等),整体存储成本更低。

  • 易于归档:按时间窗口分桶天然适合冷热分离和按时间清理。

一个常见的担忧:消息丢失或重复。 上述 $push 操作是原子的,但对于网络超时等情况,需要在应用层做幂等处理。可以在消息结构中加一个唯一的 message_id,然后使用 $addToSet 代替 $push 来确保同一 message_id 只被写入一次。

await db.session_buckets.updateOne(
    { session_id: sessionId, bucket_start: bucketStart },
    { $addToSet: { messages: message }, $setOnInsert: { ... } },
    { upsert: true }
);

通过这种设计,Agent 的会话存储系统既能扛住海量消息的写入,又能快速响应用户“往上翻聊天记录”的需求,在性能和空间之间找到了最佳的平衡点。


MongoDB 一致性与并发

15、写关注(WriteConcern)与读关注(ReadConcern)

难度级别:⭐⭐~⭐⭐⭐⭐(w:1/w:majority、local/majority/linearizable/snapshot)

基础题:WriteConcern w:1 和 w:majority 有什么区别?

w:1:写入主节点成功即返回,性能好但可能丢数据(主节点崩溃未复制到从节点) w:majority:写入大多数节点成功才返回,性能差但数据安全(至少一个从节点写入成功)

进阶题:ReadConcern 的 local/majority/linearizable/snapshot 四种级别分别是什么含义?与隔离性有什么关系?

1️⃣ Common Answer

ReadConcern 就是读一致性级别,local 读本地,majority 读大多数节点。具体细节记不清了。

2️⃣ Impressive Answer

ReadConcern 定义了读操作的隔离级别,与数据库事务隔离性类似:

  1. local(默认)
  2. 含义:从主节点或从节点读取最新数据,不保证已复制到大多数节点
  3. 隔离性:读未提交(Read Uncommitted)级别
  4. 场景:对一致性要求不高的场景,如日志、监控数据

  5. majority

  6. 含义:只读已复制到大多数节点的数据,保证数据不回滚
  7. 隔离性:读已提交(Read Committed)级别
  8. 场景:需要数据一致性的场景,如计费、订单

  9. linearizable

  10. 含义:读取最新写入的数据,保证线性一致性(类似串行化)
  11. 隔离性:可串行化(Serializable)级别
  12. 场景:需要强一致性的场景,如库存扣减、余额查询
  13. 代价:只能从主节点读,性能差

  14. snapshot

  15. 含义:读取事务开始时的快照数据,保证可重复读
  16. 隔离性:可重复读(Repeatable Read)级别
  17. 场景:多文档事务,需要读取一致的数据集

  18. Agent 场景应用

  19. 会话数据:writeConcern: w:1 + readConcern: local,性能优先
  20. 计费数据:writeConcern: w:majority + readConcern: majority,一致性优先
  21. 余额查询:readConcern: linearizable,保证读到最新数据

3️⃣ Key Differences

查看内嵌表格

场景题:Agent 计费数据如何配置强一致读写?会话数据如何配置?

1️⃣ Common Answer

计费数据用 w:majority 吧,会话数据用 w:1。具体配置不太清楚。

2️⃣ Impressive Answer

Agent 不同业务场景的一致性配置策略

  1. 计费数据(强一致)
  2. WriteConcern{w: "majority", j: true},确保写入大多数节点且落盘
  3. ReadConcernmajority,确保读取已提交数据
  4. ReadPreferenceprimary,只从主节点读
  5. 理由:计费数据不能丢,不能读脏数据,性能可以牺牲

  6. 会话数据(最终一致)

  7. WriteConcern{w: 1, j: false},写入主节点即返回
  8. ReadConcernlocal,读取最新数据
  9. ReadPreferencesecondaryPreferred,优先从从节点读
  10. 理由:会话数据量大,性能优先,短暂不一致可接受

  11. 用户余额(线性一致)

  12. WriteConcern{w: "majority", j: true}
  13. ReadConcernlinearizable,保证读到最新余额
  14. ReadPreferenceprimary,只能从主节点读
  15. 理由:余额查询需要强一致性,避免超卖

  16. 配置示例(Spring Boot) \``yamlspring:data:mongodb:uri: mongodb://localhost:27017/agent_dbwrite-concern: MAJORITYread-concern: MAJORITYread-preference: PRIMARY``

  17. 性能对比

  18. 强一致配置:TPS 约 1000,延迟 10-50ms
  19. 最终一致配置:TPS 约 5000,延迟 1-5ms

3️⃣ Key Differences

查看内嵌表格

容易一起考的题

查看内嵌表格


16、锁机制:从全局锁到文档级锁的演进

难度级别:⭐⭐~⭐⭐⭐⭐(意向锁、乐观并发控制、db.currentOp)

基础题:MongoDB 的锁粒度是什么级别?

MongoDB 3.0+ 使用 WiredTiger 存储引擎,锁粒度为文档级锁(Document-Level Locking)。

  • 3.0 之前:全局锁(Global Lock),整个数据库被锁定

  • 3.0-3.2:数据库级锁(Database-Level Locking)

  • 3.2+:文档级锁,支持并发读写

进阶题:意向锁(IS/IX)的作用是什么?WiredTiger 的乐观并发控制如何工作?如何用 db.currentOp() 排查锁等待?

1️⃣ Common Answer

意向锁是为了提高并发吧,乐观并发就是不加锁先执行。db.currentOp 能看当前操作。

2️⃣ Impressive Answer

MongoDB 锁机制从全局锁演进到文档级锁,核心机制包括:

  1. 意向锁(IS/IX)
  2. IS(意向共享锁):事务打算读取文档,允许其他事务读取
  3. IX(意向排他锁):事务打算修改文档,允许其他事务读取但不允许修改
  4. 作用:避免锁冲突,提高并发效率
  5. 兼容性:IS 兼容 IS/IX,IX 兼容 IS,IX 不兼容 IX

  6. WiredTiger 乐观并发控制

  7. 无锁读取:读操作不加锁,通过快照读取历史版本
  8. 写时复制:修改文档时创建新版本,旧版本保留
  9. 冲突检测:提交时检查是否有其他事务修改了相同文档
  10. 重试机制:冲突时自动重试(最多 10 次)

  11. db.currentOp() 排查锁等待

  12. 查看当前操作db.currentOp({active: true, secs_running: {$gt: 5}})
  13. 关键字段
    • op:操作类型(insert/update/query)
    • secs_running:运行时间(秒)
    • lock:锁类型(r/w/R/W)
    • waitingForLock:是否等待锁
    • microsecs_running:运行时间(微秒)
  14. 终止操作db.killOp(opId),谨慎使用

  15. Agent 场景应用

  16. 会话高并发写入:文档级锁保证并发性能
  17. 慢查询排查:db.currentOp() 定位长时间运行的操作
  18. 锁竞争优化:避免长时间事务,减少锁持有时间

3️⃣ Key Differences

查看内嵌表格

场景题:高并发写入时出现锁竞争如何分析和优化?

1️⃣ Common Answer

加索引吧,或者拆分数据。具体分析和优化步骤不太清楚。

2️⃣ Impressive Answer

高并发写入锁竞争的分析和优化流程

  1. 问题分析
  2. 监控指标wiredTiger.cache.pages read into cache(读入页数)和 pages evicted(驱逐页数)
  3. 查看锁等待db.currentOp({waitingForLock: true}) 查看等待锁的操作
  4. 查看慢查询db.setProfilingLevel(1, {slowms: 100}) 记录慢查询

  5. 优化方案

  6. 方案一:优化文档设计
    • 避免大文档:拆分嵌套文档,减少单文档大小
    • 分散写入:按用户 ID 分片,避免热点文档
  7. 方案二:优化索引
    • 复合索引:ESR 规则(Equality → Sort → Range)
    • 覆盖索引:查询字段全在索引中,避免回表
  8. 方案三:优化写入策略
    • 批量写入:insertMany + ordered: false
    • 异步写入:writeConcern: w:1,不等待从节点确认
  9. 方案四:架构优化

    • 读写分离:读请求路由到从节点
    • 分片集群:水平扩展,分散写入压力
  10. Agent 场景优化

  11. 会话写入:按用户 ID 分片,避免同一会话高并发写入
  12. 批量导入:使用 insertMany + ordered: false,失败不中断
  13. 监控告警:锁等待时间超过阈值时告警

3️⃣ Key Differences

查看内嵌表格

容易一起考的题

查看内嵌表格


17、因果一致性(Causal Consistency)

难度级别:⭐⭐~⭐⭐⭐⭐(ClientSession、afterClusterTime、线性一致性)

基础题:什么是因果一致性?为什么副本集读写会有一致性问题?

因果一致性:保证有因果关系的操作按顺序执行,如果操作 A 影响 B,则任何看到 B 的节点必须先看到 A。

副本集一致性问题:主节点写入后立即从从节点读取,可能读不到刚写入的数据(复制延迟导致)。

进阶题:ClientSession + afterClusterTime 如何实现因果一致性?与线性一致性有什么区别?

1️⃣ Common Answer

ClientSession 就是会话,afterClusterTime 是时间戳吧。线性一致性更强?

2️⃣ Impressive Answer

因果一致性是 MongoDB 副本集的重要一致性保证,实现机制包括:

  1. ClientSession(客户端会话)
  2. 定义:客户端与 MongoDB 之间的逻辑会话,包含操作序列
  3. 作用:跟踪会话内的所有操作,保证因果顺序
  4. 创建方式client.startSession() 或驱动自动创建

  5. afterClusterTime(集群时间)

  6. 定义:MongoDB 全局递增的时间戳(包含时间 + 计数器)
  7. 作用:标记操作发生的时刻,用于因果排序
  8. 使用方式find().readConcern("majority").afterClusterTime(clusterTime)

  9. 因果一致性实现

  10. 写操作:记录当前 clusterTime
  11. 读操作:使用 afterClusterTime,只读时间戳之后的数据
  12. 保证:如果写操作发生在读操作之前,读操作一定能读到写操作的结果

  13. 与线性一致性的区别

  14. 因果一致性:只保证有因果关系的操作顺序,无因果关系的操作可以乱序
  15. 线性一致性:所有操作按全局顺序执行,保证强一致
  16. 性能对比:因果一致性好于线性一致性,线性一致性只能从主节点读
  17. 适用场景:因果一致性适合大多数业务,线性一致性适合库存扣减等强一致场景

  18. Agent 场景应用

  19. 会话写入后立即读取:使用 ClientSession + afterClusterTime 保证读到最新数据
  20. 多步骤操作:在同一个 ClientSession 中执行,保证因果顺序

3️⃣ Key Differences

查看内嵌表格

场景题:Agent 写入会话后立即读取,如何保证读到最新数据?

1️⃣ Common Answer

用 w:majority 写入,然后从主节点读。具体实现不太清楚。

2️⃣ Impressive Answer

Agent 写入会话后立即读取的一致性保证方案

  1. 方案一:强一致配置
  2. WriteConcern{w: "majority", j: true}
  3. ReadConcernmajority
  4. ReadPreferenceprimary
  5. 缺点:性能差,只能从主节点读

  6. 方案二:因果一致性(推荐)

  7. 使用 ClientSession: ```javascriptconst session = client.startSession();try {session.startTransaction();// 写入会话await db.sessions.insertOne({userId: "user123", content: "你好"}, {session});// 立即读取const result = await db.sessions.findOne({userId: "user123"}, {session,readConcern: {level: "majority"},readPreference: "primary"});await session.commitTransaction();} finally {session.endSession();}
    ```

*   **优势**:性能好,保证因果顺序
  1. 方案三:读后写缓存
  2. 写入后缓存:写入成功后,将数据缓存到 Redis
  3. 读取时先查缓存:优先从缓存读取,缓存未命中再查数据库
  4. 缺点:引入缓存复杂度,缓存一致性需要处理

  5. Agent 场景最佳实践

  6. 会话写入后立即读取:使用 ClientSession + afterClusterTime
  7. 性能要求高:使用读后写缓存
  8. 数据一致性要求高:使用强一致配置

MongoDB 运维与架构

18、连接池与驱动配置优化

难度级别:⭐⭐~⭐⭐⭐⭐(maxPoolSize、minPoolSize、waitQueueTimeoutMS、serverSelectionTimeoutMS)

基础题:MongoDB 驱动的连接池是怎么工作的?

MongoDB 驱动使用连接池管理数据库连接:

  1. 应用启动时创建一定数量的连接

  2. 执行操作时从连接池获取连接

  3. 操作完成后将连接归还连接池

  4. 连接池自动维护连接健康状态

进阶题:maxPoolSize/minPoolSize/waitQueueTimeoutMS/serverSelectionTimeoutMS 各参数的含义是什么?连接泄漏如何排查?

1️⃣ Common Answer

maxPoolSize 是最大连接数,minPoolSize 是最小连接数。其他参数不太清楚。连接泄漏就是连接没释放吧。

2️⃣ Impressive Answer

MongoDB 驱动连接池的核心参数和连接泄漏排查:

  1. 核心参数
  2. maxPoolSize(默认 100):最大连接数,超过则等待
  3. minPoolSize(默认 0):最小连接数,保持连接池中有这么多连接
  4. waitQueueTimeoutMS(默认 0):等待连接超时时间(毫秒),0 表示无限等待
  5. serverSelectionTimeoutMS(默认 30000):选择服务器超时时间(毫秒)
  6. maxIdleTimeMS(默认 0):连接最大空闲时间(毫秒),超过则关闭
  7. connectTimeoutMS(默认 10000):连接超时时间(毫秒)

  8. 连接泄漏排查

  9. 监控指标db.serverStatus().connections 查看当前连接数
  10. 查看连接来源db.currentOp() 查看当前操作和客户端信息
  11. 连接数告警:当前连接数超过 maxPoolSize * 0.8 时告警
  12. 代码审查:检查是否正确关闭连接(如使用 try-finally)

  13. 连接池配置建议

  14. maxPoolSize:根据应用并发量设置,一般 50-200
  15. minPoolSize:设置为 maxPoolSize * 0.5,避免冷启动
  16. waitQueueTimeoutMS:设置为 5000,避免无限等待
  17. maxIdleTimeMS:设置为 60000,关闭空闲连接

  18. Agent 场景优化

  19. 会话高并发:maxPoolSize: 200minPoolSize: 100
  20. 监控告警:连接数超过 160 时告警
  21. 连接泄漏排查:定期检查 db.serverStatus().connections

Spring Boot 集成 MongoDB 的连接池最佳配置是什么?

1️⃣ Common Answer

在 application.yml 里配置连接池参数。具体配置不太清楚。

2️⃣ Impressive Answer

Spring Boot 集成 MongoDB 的连接池最佳配置

  1. application.yml 配置 ```yamlspring:data:mongodb:uri: mongodb://localhost:27017/agent_dbauto-index-creation: true# 连接池配置min-connections-per-host: 50max-connections-per-host: 200max-connection-idle-time: 60000max-connection-life-time: 120000connection-timeout: 10000socket-timeout: 30000# 服务器选择超时server-selection-timeout: 30000# 等待队列超时wait-queue-timeout: 5000# 心跳频率heartbeat-frequency: 10000# 心跳连接超时heartbeat-connect-timeout: 10000# 心跳 socket 超时heartbeat-socket-timeout: 30000

1. **配置说明**
  - **max-connections-per-host**:每个主机最大连接数,根据应用并发量设置
  - **min-connections-per-host**:每个主机最小连接数,避免冷启动
  - **max-connection-idle-time**:连接最大空闲时间,关闭空闲连接
  - **max-connection-life-time**:连接最大生命周期,定期重建连接
  - **connection-timeout**:连接超时时间,避免长时间等待
  - **socket-timeout**:Socket 超时时间,避免长时间阻塞

1. **监控配置**
  - **Actuator 监控**:启用 `spring-boot-starter-actuator`,查看 `/actuator/metrics/mongo.driver.pool.size`
  - **日志配置**:设置 `logging.level.org.mongodb.driver.cluster: DEBUG`,查看连接池日志
  - **告警配置**:连接数超过阈值时告警

1. **Agent 场景最佳实践**
  - 会话高并发:`max-connections-per-host: 200`
  - 监控告警:连接数超过 160 时告警
  - 日志调试:开发环境开启 DEBUG 日志,生产环境关闭

---

### 19、时序数据存储(Time Series Collection)

**难度级别**:⭐⭐~⭐⭐⭐⭐(列式压缩、granularity、性能对比)

#### 基础题:MongoDB 5.0+ 的 Time Series Collection 是什么?和普通集合有什么区别?

**Time Series Collection**:MongoDB 5.0+ 引入的时序数据集合,专门优化时序数据存储和查询。

**与普通集合的区别**:

1. 自动按时间分桶存储

1. 内部列式压缩,节省存储空间

1. 自动优化时序查询性能

1. 支持时间范围聚合查询

#### 进阶题:时序集合的内部存储优化(列式压缩)是怎样的?granularity 如何配置?与普通集合的性能对比如何?

**1️⃣ Common Answer**

时序集合就是自动分桶吧,granularity 是时间粒度。性能应该比普通集合好。

**2️⃣ Impressive Answer**

Time Series Collection 是 MongoDB 时序数据的**专用存储引擎**,优化机制包括:

1. **内部存储优化**
  - **自动分桶**:按时间范围将数据分桶存储,默认桶大小 1 小时
  - **列式压缩**:同一列的数据连续存储,压缩比高(如时间戳压缩比 90%)
  - **数据优化**:自动删除重复数据,减少存储空间
  - **索引优化**:自动创建时间索引,加速时间范围查询

1. **granularity 配置**
  - **定义**:时间粒度,决定分桶大小
  - **可选值**:
    - `seconds`:秒级粒度,适合高频数据
    - `minutes`:分钟级粒度,适合分钟级数据
    - `hours`:小时级粒度,适合小时级数据
  - **配置方式**:`createCollection({timeseries: {timeField: "timestamp", granularity: "minutes"}})`
  - **选择建议**:根据数据频率选择,数据频率越高,粒度越小

1. **性能对比**
  - **存储空间**:时序集合比普通集合节省 50-70% 空间
  - **写入性能**:时序集合比普通集合快 2-3 倍
  - **查询性能**:时间范围查询比普通集合快 5-10 倍
  - **聚合性能**:时间聚合查询比普通集合快 3-5 倍

1. **Agent 场景应用**
  - 调用链路指标:使用时序集合存储,granularity 设置为 `seconds`
  - 用户行为埋点:使用时序集合存储,granularity 设置为 `minutes`
  - 系统监控数据:使用时序集合存储,granularity 设置为 `hours`

#### 场景题:Agent 调用链路的指标数据如何用时序集合存储?

**1️⃣ Common Answer**

创建时序集合,然后写入指标数据。具体设计不太清楚。

**2️⃣ Impressive Answer**

Agent 调用链路指标数据用时序集合存储的**设计方案**:

1. **集合创建** `\``javascriptdb.createCollection("agent_metrics", {timeseries: {timeField: "timestamp",metaField: "metadata",granularity: "seconds"}})

```text
1. **文档结构设计** `\``json{"timestamp": ISODate("2024-01-15T10:00:00Z"),"metadata": {"agentId": "agent123","sessionId": "session456","userId": "user789"},"metrics": {"latency": 123,"tokenCount": 456,"requestCount": 1,"errorCount": 0}}

```text
1. **索引设计**
  - **时间索引**:自动创建,加速时间范围查询
  - **元数据索引**:手动创建,加速元数据查询

1.     `\``javascriptdb.agent_metrics.createIndex({"metadata.agentId": 1, "metadata.sessionId": 1})

1.     `\``

1. **查询优化**
  - **时间范围查询**: `\``javascriptdb.agent_metrics.find({"metadata.agentId": "agent123","timestamp": {$gte: ISODate("2024-01-15T00:00:00Z"),$lte: ISODate("2024-01-15T23:59:59Z")}})

```text
    ```

*   **聚合查询**:

    ```javascript
    db.agent_metrics.aggregate([
      {
        $match: {
          "metadata.agentId": "agent123",
          "timestamp": {
            $gte: ISODate("2024-01-15T00:00:00Z"),
            $lte: ISODate("2024-01-15T23:59:59Z")
          }
        }
      },
      {
        $group: {
          _id: {
            $dateToString: {format: "%Y-%m-%d %H:%M", date: "$timestamp"}
          },
          avgLatency: {$avg: "$metrics.latency"},
          totalTokenCount: {$sum: "$metrics.tokenCount"}
        }
      }
    ])
    ```
  1. 优势总结
  2. 存储优化:列式压缩节省 60% 存储空间
  3. 查询优化:时间范围查询快 8 倍
  4. 聚合优化:时间聚合查询快 4 倍

20、Atlas Vector Search 与 pgvector 对比

难度级别:⭐⭐~⭐⭐⭐⭐(HNSW、ANN 近似最近邻、精度/性能/生态对比)

基础题:MongoDB Atlas 支持向量检索吗?用什么索引?

MongoDB Atlas 支持向量检索,使用 HNSW(Hierarchical Navigable Small World) 索引。

  • 支持向量相似度搜索(欧氏距离、余弦相似度、点积)

  • 与 MongoDB 文档存储集成,无需额外向量数据库

  • 支持 $vectorSearch 聚合阶段进行向量检索

进阶题:HNSW 索引的原理是什么?ANN 近似最近邻算法如何工作?Atlas Vector Search 与 pgvector 在精度/性能/生态上有什么差异?

1️⃣ Common Answer

HNSW 是一种图结构索引,ANN 是近似最近邻算法。pgvector 是 PostgreSQL 的向量扩展。

2️⃣ Impressive Answer

Atlas Vector Search 和 pgvector 是两种主流向量检索方案,对比分析:

  1. HNSW 索引原理
  2. 分层图结构:多层图,上层稀疏,下层密集
  3. 搜索过程:从顶层开始,逐层向下搜索,快速定位到最近邻
  4. 优势:查询速度快,索引构建快,内存占用低

  5. ANN 近似最近邻算法

  6. 定义:近似最近邻搜索,不保证找到绝对最近邻,但速度极快
  7. 精度权衡:通过调整 ef 参数(搜索宽度)平衡精度和性能
  8. 适用场景:大规模向量检索(百万级以上),需要毫秒级响应

  9. Atlas Vector Search vs pgvector

  10. 精度对比
    • Atlas Vector Search:精度 95-98%,可调整 ef 参数
    • pgvector:精度 90-95%,可调整 lists 参数
  11. 性能对比
    • Atlas Vector Search:查询速度 10-50ms,适合实时检索
    • pgvector:查询速度 50-200ms,适合离线检索
  12. 生态对比
    • Atlas Vector Search:MongoDB 原生支持,无需额外组件,与文档存储集成
    • pgvector:PostgreSQL 扩展,需要单独安装,与关系型数据库集成
  13. 成本对比

    • Atlas Vector Search:MongoDB Atlas 云服务,按使用量付费
    • pgvector:开源免费,自建成本低
  14. Agent 场景应用

  15. 知识库向量检索:使用 Atlas Vector Search,实时检索要求高
  16. 用户行为分析:使用 pgvector,离线分析要求高
  17. 混合方案:热数据用 Atlas,冷数据用 pgvector

场景题:Agent 知识库向量检索方案如何在 Atlas、pgvector、Milvus 之间选型?

1️⃣ Common Answer

用 Atlas 吧,MongoDB 原生支持。具体选型不太清楚。

2️⃣ Impressive Answer

Agent 知识库向量检索方案的选型决策

  1. Atlas Vector Search
  2. 适用场景:实时检索、与文档存储集成、中小规模(百万级)
  3. 优势
    • MongoDB 原生支持,无需额外组件
    • 与文档存储集成,方便管理
    • 查询速度快(10-50ms)
  4. 劣势
    • 仅支持 MongoDB Atlas 云服务
    • 成本较高(按使用量付费)
  5. Agent 场景:适合实时知识库检索,与 Agent 会话数据集成

  6. pgvector

  7. 适用场景:离线分析、与关系型数据库集成、中小规模(百万级)
  8. 优势
    • PostgreSQL 扩展,开源免费
    • 与关系型数据库集成,方便管理
    • 支持复杂查询(SQL)
  9. 劣势
    • 查询速度慢(50-200ms)
    • 需要单独安装和配置
  10. Agent 场景:适合离线知识库分析,与用户行为数据集成

  11. Milvus

  12. 适用场景:大规模检索(千万级以上)、高性能要求、独立向量数据库
  13. 优势
    • 查询速度快(1-10ms)
    • 支持大规模向量(十亿级)
    • 开源免费
  14. 劣势
    • 需要单独部署和管理
    • 与文档存储分离,需要额外同步
  15. Agent 场景:适合大规模知识库检索,独立向量数据库

  16. 选型建议

  17. 实时检索 + 文档集成:选择 Atlas Vector Search
  18. 离线分析 + 关系型数据库:选择 pgvector
  19. 大规模检索 + 高性能:选择 Milvus
  20. 混合方案:热数据用 Atlas,冷数据用 Milvus

21、多租户数据隔离方案

难度级别:⭐⭐~⭐⭐⭐⭐(独立数据库、共享集合+tenantId、分片键隔离)

基础题:多租户数据隔离有哪几种方式?

多租户数据隔离三种方式:

  1. 独立数据库:每个租户一个数据库,隔离性最好

  2. 共享集合+tenantId:所有租户共享集合,通过 tenantId 字段隔离

  3. 分片键隔离:使用 tenantId 作为分片键,数据分散到不同分片

进阶题:独立数据库 vs 共享集合+tenantId vs 分片键隔离,各自的安全性、成本、查询性能如何对比?

1️⃣ Common Answer

独立数据库最安全,共享集合最便宜。分片键隔离应该是中间方案吧。

2️⃣ Impressive Answer

多租户数据隔离方案是SaaS 架构的核心决策,多维度对比:

  1. 安全性对比
  2. 独立数据库
    • 隔离性:最高,租户之间完全隔离
    • 数据泄露风险:最低,租户无法访问其他租户数据
  3. 共享集合+tenantId
    • 隔离性:中等,通过 tenantId 字段隔离
    • 数据泄露风险:中等,需要应用层保证查询带 tenantId
  4. 分片键隔离

    • 隔离性:中等,租户数据分散到不同分片
    • 数据泄露风险:中等,需要应用层保证查询带 tenantId
  5. 成本对比

  6. 独立数据库
    • 存储成本:高,每个租户独立数据库,存储空间浪费
    • 运维成本:高,需要管理大量数据库
  7. 共享集合+tenantId
    • 存储成本:低,所有租户共享集合,存储空间利用率高
    • 运维成本:低,只需要管理少量集合
  8. 分片键隔离

    • 存储成本:中等,租户数据分散,存储空间利用率中等
    • 运维成本:中等,需要管理分片集群
  9. 查询性能对比

  10. 独立数据库
    • 查询性能:高,每个租户独立数据库,索引小,查询快
    • 扩展性:差,每个租户独立数据库,无法水平扩展
  11. 共享集合+tenantId
    • 查询性能:低,所有租户共享集合,索引大,查询慢
    • 扩展性:好,可以水平扩展
  12. 分片键隔离

    • 查询性能:中等,租户数据分散,索引中等,查询速度中等
    • 扩展性:好,可以水平扩展
  13. Agent 场景应用

  14. 大客户(独立数据库):安全要求高,查询性能要求高
  15. 中小客户(共享集合+tenantId):成本敏感,查询性能要求低
  16. 混合方案:大客户独立数据库,中小客户共享集合

场景题:SaaS 型 Agent 平台的多租户 MongoDB 架构如何设计?

1️⃣ Common Answer

用共享集合+tenantId 吧,成本低。具体设计不太清楚。

2️⃣ Impressive Answer

SaaS 型 Agent 平台多租户 MongoDB 架构的设计方案

  1. 架构设计
  2. 租户分类
    • 大客户(VIP):独立数据库,安全要求高
    • 中小客户(SMB):共享集合+tenantId,成本敏感
  3. 数据库设计

    • 大客户:每个租户一个数据库,如 tenant_001_dbtenant_002_db
    • 中小客户:共享数据库,如 smb_db,集合中包含 tenantId 字段
  4. 索引设计

  5. 大客户
    • 每个租户独立索引,索引小,查询快
    • 索引示例:{userId: 1, createdAt: -1}
  6. 中小客户

    • 共享索引,索引大,查询慢
    • 索引示例:{tenantId: 1, userId: 1, createdAt: -1}
  7. 查询优化

  8. 大客户
    • 查询时指定数据库,如 db.tenant_001_db.sessions.find({...})
    • 查询性能高,延迟 1-5ms
  9. 中小客户

    • 查询时必须带 tenantId,如 db.smb_db.sessions.find({tenantId: "tenant123", ...})
    • 查询性能中等,延迟 5-20ms
  10. 数据迁移

  11. 中小客户升级为大客户
    • 导出中小客户数据 → 创建独立数据库 → 导入数据 → 删除原数据
  12. 大客户降级为中小客户

    • 导出大客户数据 → 删除独立数据库 → 导入共享数据库
  13. 监控告警

  14. 大客户:监控每个租户的数据库性能,独立告警
  15. 中小客户:监控共享数据库性能,统一告警

  16. Agent 场景最佳实践

  17. 大客户:独立数据库,查询性能高,安全要求高
  18. 中小客户:共享集合+tenantId,成本低,查询性能中等
  19. 混合方案:根据租户规模动态调整

22、MongoDB 监控与可观测性

难度级别:⭐⭐~⭐⭐⭐⭐(mongostat、mongotop、opcounters、connections、wiredTiger cache、慢查询 Profiler)

基础题:如何监控 MongoDB 的健康状态?有哪些常用工具?

MongoDB 监控工具

  1. mongostat:实时监控 MongoDB 状态(QPS、连接数、内存使用等)

  2. mongotop:监控集合读写时间,定位热点集合

  3. db.serverStatus():查看服务器状态(内存、连接、锁等)

  4. db.currentOp():查看当前运行的操作

  5. MongoDB Atlas:云服务自带监控仪表板

进阶题:mongostat/mongotop 的核心指标是什么?opcounters、connections、wiredTiger cache 分别代表什么?慢查询 Profiler 如何配置?

1️⃣ Common Answer

mongostat 看基本指标,mongotop 看集合读写时间。opcounters 是操作数,connections 是连接数,wiredTiger cache 是缓存。慢查询用 Profiler。

2️⃣ Impressive Answer

MongoDB 监控的核心指标和配置方法:

  1. mongostat 核心指标
  2. insert/query/update/delete:每秒操作数(QPS)
  3. command:每秒命令数
  4. vsize:虚拟内存使用量
  5. res:物理内存使用量
  6. faults:每秒 Page Fault 数(内存不足时增加)
  7. qr|qw:读写队列长度(队列过长表示性能瓶颈)
  8. ar|aw:活跃读写连接数
  9. netIn/netOut:网络流入/流出量

  10. mongotop 核心指标

  11. total:集合总读写时间
  12. read:集合读时间
  13. write:集合写时间
  14. 作用:定位热点集合,优化索引

  15. db.serverStatus() 核心指标

  16. opcounters:操作计数器(insert/query/update/delete 总数)
  17. connections:连接数(current/available)
  18. wiredTiger.cache:缓存指标

    • pages read into cache:读入页数(内存不足时增加)
    • pages evicted:驱逐页数(内存不足时增加)
    • percentage dirty:脏页比例(过高时 Checkpoint 频繁)
  19. 慢查询 Profiler 配置

  20. 级别设置
    • 0:关闭
    • 1:记录慢查询(默认超过 100ms)
    • 2:记录所有查询(仅用于调试)
  21. 配置方式\``javascriptdb.setProfilingLevel(1, {slowms: 100})``
  22. 查看慢查询\``javascriptdb.system.profile.find().sort({ts: -1}).limit(10)``

  23. Agent 场景监控

  24. 会话 QPS 监控:mongostat 监控 insert/query QPS
  25. 热点集合监控:mongotop 定位热点集合,优化索引
  26. 内存监控:db.serverStatus() 监控 wiredTiger cache,避免内存不足
  27. 慢查询监控:Profiler 记录慢查询,优化查询

场景题:Agent 平台的 MongoDB 监控告警体系如何搭建?

1️⃣ Common Answer

用 Prometheus + Grafana 吧,监控 QPS、连接数、内存。具体搭建不太清楚。

2️⃣ Impressive Answer

Agent 平台 MongoDB 监控告警体系的搭建方案

  1. 监控架构
  2. 数据采集:MongoDB Exporter 采集指标
  3. 数据存储:Prometheus 存储时序数据
  4. 数据展示:Grafana 可视化展示
  5. 告警通知:Alertmanager 发送告警

  6. 监控指标

  7. 基础指标
    • QPS:mongod_op_counters_total(insert/query/update/delete)
    • 连接数:mongod_connections_current / mongod_connections_available
    • 内存:mongod_wiredtiger_cache_bytes(缓存大小)
  8. 性能指标
    • 延迟:mongod_latency_histogram(操作延迟)
    • Page Fault:mongod_wiredtiger_cache_pages_evicted_total(驱逐页数)
    • 队列长度:mongod_global_lock_current_queue(锁队列长度)
  9. 业务指标

    • 会话 QPS:按 agentId 分组统计
    • 慢查询数:按集合分组统计
  10. 告警规则

  11. QPS 告警:QPS 超过阈值时告警 96yaml
    • alert: HighQPSexpr: rate(mongodopcounters_total[5m]) > 1000for: 5mlabels:severity: warningannotations:summary: "MongoDB QPS 过高"
  12. ```
  13. 连接数告警:连接数超过阈值时告警 96yaml
    • alert: HighConnectionsexpr: mongodconnectionscurrent / mongodconnectionsavailable > 0.8for: 5mlabels:severity: warningannotations:summary: "MongoDB 连接数过高"
  14. ```
  15. 内存告警:内存使用率超过阈值时告警 96yaml
    • alert: HighMemoryexpr: mongodwiredtigercachebytes / nodememoryMemTotalbytes > 0.8for: 5mlabels:severity: warningannotations:summary: "MongoDB 内存使用率过高"
  16. ```
  17. 慢查询告警:慢查询数超过阈值时告警 ```yaml

    • alert: HighSlowQueryexpr: rate(mongodslowqueries_total[5m]) > 10for: 5mlabels:severity: warningannotations:summary: "MongoDB 慢查询过多"
  18. ```

  19. Grafana 仪表板

  20. 基础仪表板:QPS、连接数、内存、延迟
  21. 业务仪表板:会话 QPS、慢查询数、热点集合
  22. 告警仪表板:告警历史、告警趋势

  23. Agent 场景最佳实践

  24. 会话 QPS 监控:按 agentId 分组,定位热点 Agent
  25. 慢查询监控:按集合分组,优化慢查询
  26. 内存监控:监控 wiredTiger cache,避免内存不足
  27. 告警通知:钉钉、邮件、短信多渠道通知