1. 项目背景

业务场景:周四下午 4 点,本地生活电商的运营实习生在执行"清理测试数据"时,把生产环境 products 集合的全部文档当成测试数据执行了 deleteMany({})。3 万条真实商品数据和对应的 50 万条评论全部消失。整个运营后台变成空白页,只有 10 分钟前的一份手动导出的 JSON 文件躺在同事的下载文件夹里。团队惊慌失措——有人提议直接从数据库里恢复(但 MongoDB 没有回收站);有人提议重新录入(需要一周才能补完);运维说"我们有 mongodump 啊",结果发现上一次备份是 3 天前。

痛点:备份是最后一道防线,但多数项目上线时根本没配置。常见问题:mongodump 用了但恢复时发现缺少索引导致查询极慢;备份时没有考虑一致性,导出的数据中间状态可能不完整;备份文件本身也在同一个磁盘上——磁盘一坏,备份和数据同归于尽;从来没做过恢复演练,备份文件能不能用、恢复要多久,没人知道。

2. 项目设计

小胖(手忙脚乱地查文档):大师!运营误删了商品数据怎么办!我没备份,只找到一份上周的手动 JSON!

大师:先冷静,这不是第一次也不会是最后一次。我们先搞清楚两件事:怎么备份(预防),怎么恢复(急救)。

小胖:MongoDB 的备份工具我听说过——mongodump 和 mongorestore。它们到底导出了什么?是 JSON 文件吗?

大师:mongodump 导出的是 BSON 文件(不是 JSON),它是 MongoDB 的原生二进制格式,保留了所有 BSON 类型信息(包括 Date、ObjectId、Decimal128 等),而 JSON 导出方式(mongoexport)会把很多类型转成字符串,Date 变成了 {"$date":"2026-01-01"} 这种格式,恢复时需要手动处理类型转换。对比表:

对比维度 mongodump(BSON) mongoexport(JSON)
文件格式 BSON 二进制 JSON 文本
类型保真 完整 Date→字符串, Decimal128→字符串
索引 可选导出一并恢复 不支持
跨系统交换 BSON 仅在 MongoDB 使用 JSON 通用可读
按条件导出 支持 --query 支持 --query
恢复命令 mongorestore mongoimport

技术映射mongodump = 物理级备份(保类型、保索引),mongoexport = 逻辑级导出(纯数据,可读性好但丢类型)。

小白:那备份的时候要不要停写?如果备份过程中有数据在写入,导出的快照是不是不一致?

大师:好问题。mongodump 在做备份时,默认对所有集合做逐个扫描,不会锁库。这意味着:如果备份了 100 个集合,第 1 个和第 100 个集合的快照时间点不同——订单集合可能是 14:00 的状态,支付集合可能是 14:05 的状态。这叫"时间点不一致"。

技术映射:mongodump 不带任何一致性保证。如果你的备份涉及多个集合且它们互相引用(如订单引用优惠券),跨集合的一致性无法保证。

小白:那怎么保证跨集合的一致备份?

大师:三种方案:

  1. 业务停写:备份窗口内停止写入——最简单但停机成本高。
  2. 文件系统快照:LVM/ZFS 快照。在快照瞬间所有文件得到一个一致性时间点——快,但依赖底层存储支持。
  3. Oplog 增量恢复:mongodump + --oplog 参数。备份开始时记录 Oplog 位置,备份结束后重放 Oplog 把备份期间的操作补回去——实现时间点一致。这是生产环境推荐的方案。

小胖:那恢复的时候呢?直接 mongorestore 一股脑怼回去?

大师:视情况而定。零星数据恢复不要全量恢复——按库/集合甚至按条件恢复。关键步骤:

# 全量恢复(危险:会覆盖已有数据)
mongorestore --uri="mongodb://user:pwd@host:27017" /backup/dump/

# 按库恢复
mongorestore --db=local_life /backup/dump/local_life/

# 按集合恢复(不覆盖已有数据)
mongorestore --db=local_life --collection=products /backup/dump/local_life/products.bson

# 恢复到新库(验证用,不影响生产)
mongorestore --db=local_life_restored /backup/dump/local_life/

大师(总结):记住备份的"3-2-1 原则":3 份数据副本、2 种不同介质、1 份存在异地。然后每季度做一次恢复演练——不是看备份在不在,而是能不能在目标时间内恢复完成。

3. 项目实战

3.1 环境准备

备份测试需要在宿主机上操作(继承已有 Docker 环境)。

# 确认目标容器运行中
docker compose -f mongodb-lab/docker-compose.yml ps

# 在宿主机上安装 mongodump 工具
# 方式1:安装 MongoDB Database Tools
# https://www.mongodb.com/try/download/database-tools

# 方式2:通过容器执行 mongodump(推荐,无需本地安装)
docker exec -it mongodb-lab mongodump --help

3.2 分步实现

步骤一:准备测试数据并执行全量备份

目标:创建测试数据后执行一次全量备份。

// 连接 mongosh
use local_life

// 创建备份测试专用的商品数据
db.products_backup.drop()
for (let i = 1; i <= 200; i++) {
  db.products_backup.insertOne({
    name: `备份测试商品_${i}`,
    category: ["数码","家居","食品"][i % 3],
    price: NumberDecimal((i * 19.9).toFixed(2)),
    stock: i * 5,
    tags: [i % 2 === 0 ? "热销" : "新品"],
    createdAt: new Date()
  })
}
print("插入完成:", db.products_backup.countDocuments(), "条")
# 在宿主机上通过 Docker 执行 mongodump
# 备份到宿主机 ./mongodb-lab/backups/ 目录
docker exec mongodb-lab mongodump \
  --uri="mongodb://admin:admin123@localhost:27017/?authSource=admin" \
  --db=local_life \
  --collection=products_backup \
  --out=/data/backups/full_$(date +%Y%m%d)

# 将容器内的备份文件拷贝到宿主机
docker cp mongodb-lab:/data/backups ./mongodb-lab/backups-from-container

# 查看备份文件结构
ls -la ./mongodb-lab/backups-from-container/
# 期望看到:local_life/products_backup.bson 和 products_backup.metadata.json
# Windows PowerShell 版本
docker exec mongodb-lab mongodump `
  --uri="mongodb://admin:admin123@localhost:27017/?authSource=admin" `
  --db=local_life `
  --collection=products_backup `
  --out=/data/backups/full_20260321

docker cp mongodb-lab:/data/backups/full_20260321 ./mongodb-lab/backups/

步骤二:按条件备份——只导出特定类目的商品

# 只备份"数码"类目的商品(条件导出)
docker exec mongodb-lab mongodump \
  --uri="mongodb://admin:admin123@localhost:27017/?authSource=admin" \
  --db=local_life \
  --collection=products_backup \
  --query='{"category":"数码"}' \
  --out=/data/backups/digital_only

# 验证:BSON 文件中只有数码类目的数据
# 通过 mongorestore 的 --dryRun 查看即将恢复的文档数
docker exec mongodb-lab mongorestore \
  --uri="mongodb://admin:admin123@localhost:27017/?authSource=admin" \
  --db=local_life \
  --collection=products_backup_digital \
  --dryRun \
  /data/backups/digital_only/local_life/

步骤三:备份包含索引——连同索引结构一起导出/恢复

# 先为目标集合创建索引
# 在 mongosh 中:
use local_life
db.products_backup.createIndex({ category: 1, price: 1 })
db.products_backup.createIndex({ tags: 1 })

# 备份时带索引信息
# 注意:mongodump 默认不导出索引(索引需要重建)
# mongodump 的默认行为:只导出数据和集合的元数据(不含索引)
# 索引需要在恢复后手动重建,或用 MONGODUMP --oplog 捕获建索引操作

# 查看备份的 metadata.json(含集合选项,如 validator、collation)
docker exec mongodb-lab cat /data/backups/full_20260321/local_life/products_backup.metadata.json
# 恢复后需手动重建索引或通过脚本执行

步骤四:模拟误删 + 恢复演练

目标:模拟运维误删集合后,通过备份恢复数据,验证数据完整性。

// mongosh
use local_life

// 记录删除前数据量
const beforeDelete = db.products_backup.countDocuments()
print("删除前文档数:", beforeDelete)

// 记下一条文档的 _id 用于事后对比
const sampleId = db.products_backup.findOne()._id
print("样本 _id:", sampleId)
// 模拟误删!
db.products_backup.deleteMany({})
print("删除后文档数:", db.products_backup.countDocuments())
// → 0
# 执行恢复!
docker exec mongodb-lab mongorestore \
  --uri="mongodb://admin:admin123@localhost:27017/?authSource=admin" \
  --db=local_life \
  --collection=products_backup \
  --drop \
  /data/backups/full_20260321/local_life/products_backup.bson

# --drop 参数:先清空目标集合,再导入(确保恢复结果干净)
# 不加 --drop:追加到已有数据中(可能产生重复)
// mongosh 验证恢复结果
use local_life
const afterRestore = db.products_backup.countDocuments()
print("恢复后文档数:", afterRestore)
print("数据完整性:", afterRestore === beforeDelete ? "PASS (与删除前一致)" : "FAIL")

// 验证样本数据是否存在
const sample = db.products_backup.findOne({ _id: sampleId })
print("样本数据存在:", sample !== null ? "PASS" : "FAIL")

步骤五:mongoexport / mongoimport —— JSON 数据交换

目标:用 JSON 导出用于跨系统交换,理解其类型丢失问题。

# 导出为 JSON(适合数据分析师在 Excel 中使用)
docker exec mongodb-lab mongoexport \
  --uri="mongodb://admin:admin123@localhost:27017/?authSource=admin" \
  --db=local_life \
  --collection=products_backup \
  --query='{"category":"家居"}' \
  --limit=5 \
  --out=/data/backups/products_home.json

# 查看导出的 JSON 内容
docker exec mongodb-lab cat /data/backups/products_home.json
# 注意:Date 类型变成了 {"$date":"2026-03-21T..."}
#       Decimal128 类型变成了 {"$numberDecimal":"199.00"}
# 从 JSON 导入
docker exec mongodb-lab mongoimport \
  --uri="mongodb://admin:admin123@localhost:27017/?authSource=admin" \
  --db=local_life \
  --collection=products_from_json \
  --file=/data/backups/products_home.json

步骤六:使用 Oplog 做时间点恢复(概述)

# Oplog 备份(复制集环境可用,本章仅展示概念)
# 1. 备份时记录 Oplog 位置
# mongodump --oplog --out=/backup/with_oplog/

# 2. 恢复后用 Oplog 重放到指定时间点
# mongorestore --oplogReplay --oplogLimit="2026-03-21T14:00:00Z" /backup/with_oplog/

# 注意:单节点(非复制集)没有 Oplog,此功能不可用
# Oplog 机制将在第 17-18 章详细讲解

3.3 完整代码清单

文件/命令 用途
mongodb-lab/scripts/ch14-prepare-backup-data.js 准备备份测试数据
mongodb-lab/backups/* 备份文件输出目录
docker exec ... mongodump 全量/条件备份
docker exec ... mongorestore 恢复操作

3.4 测试验证

// mongosh 验证脚本
use local_life

// 1. 验证恢复后数据量与删除前一致
const backupCount = 200
const restoredCount = db.products_backup.countDocuments()
print("恢复验证:", restoredCount === backupCount ? "PASS" : `FAIL (${restoredCount} vs ${backupCount})`)

// 2. 验证 BSON 类型保真
const doc = db.products_backup.findOne()
print("Decimal128:", doc.price.constructor.name === "Decimal128" ? "PASS" : "FAIL")
print("Date:", doc.createdAt instanceof Date ? "PASS" : "FAIL")

// 3. 验证条件备份的数据
const digitalOnly = db.products_backup_digital
print("条件备份集合", digitalOnly ? "存在" : "不存在")

// 4. 验证索引
const indexes = db.products_backup.getIndexes()
print("索引恢复:", indexes.length > 1 ? "PASS (含业务索引)" : "FAIL (仅有_id索引)")

// 提醒:mongodump 不导出索引定义,恢复后需手动重建
// 建议:备份索引定义脚本,恢复后执行
print("\n注意:索引需手动重建——运行 db.products_backup.createIndex(...)")

4. 项目总结

4.1 备份方案对比

方案 速度 一致性 恢复粒度 存储代价 推荐场景
mongodump + mongorestore 慢(全量扫描) 需停写或 +oplog 库/集合/条件 中(可压缩) 小型数据库(< 100GB)
文件系统快照(LVM/ZFS) 极快(秒级) 瞬间一致 整个实例 高(块级) 大型数据库 + 支持快照的存储
云厂商备份(Atlas) 自动 可配置 PITR 支持任意时间点 按量付费 生产环境首选
mongoexport 不一致(逐个扫描) 文档级 数据交换/分析用途
Oplog 增量 依赖全量备份 高(时间点恢复) 与全量备份组合 中(Oplog 大小 × 天数) 所有生产环境

4.2 适用场景

mongodump/mongorestore 适用:中小型数据库(< 100GB)的定期全量备份、按集合的细粒度恢复、开发/测试环境数据克隆。

不适用场景

  1. 超大型数据库(TB 级)——mongodump 全量扫描时间过长,用文件系统快照。
  2. 需要秒级 RPO 的金融系统——mongodump 的备份窗口太宽,用 Oplog + 快照组合。

4.3 注意事项

注意事项 说明
备份文件与数据同盘 磁盘损坏导致备份和数据一起丢失,务必至少有一份异地备份
mongorestore 的 --drop 先清空再导入,确认目标集合名无误后再加此参数
大集合恢复时间 恢复是单线程的,大集合可能很慢,提前在测试环境衡量恢复时间
备份不包含索引数据 只含索引声明(4.2+),不含索引 B-Tree 数据,恢复后需重新构建(耗时且占资源)
--oplog 仅复制集可用 单节点 mongod 没有 Oplog(Oplog 是复制集的特性),故无法使用增量备份

4.4 常见踩坑经验

故障案例一:备份了但恢复时发现数据不完整

某团队 mongodump 备份执行了 2 小时,期间业务持续写入。恢复后发现订单表比支付表多了 5000 条——因为备份订单表比备份支付表晚了一小时,那 5000 条支付记录还没产生。根因:mongodump 逐个集合扫描,无全局快照。解决:启用 --oplog 补录备份期间的操作,或先停写再备份。

故障案例二:恢复后发现查询极慢,因为没有索引

数据库故障切换到备份副本后,所有查询都变成全表扫描。根因:mongodump 默认不导出索引数据,只导出索引声明(createIndexes 在 metadata 中)。恢复后索引可以重建但耗时数小时(大集合)。解决:恢复后第一时间并行重建主要索引;使用文件系统快照避免索引重建。

故障案例三:JSON 导入后类型全部丢失

某数据分析师用 mongoexport 导出了商品表交给数据团队,数据团队用 Python 处理后回到 mongoimport 生产库。结果所有 Date 字段变成了字符串,Decimal128 金额变成了字符串,后续聚合计算全部崩掉。根因:mongoexport 使用宽松模式,Date 序列化为 {"$date":"2026-01-01"},但 mongoimport 默认不会自动还原。解决:用 --jsonFormat=canonical 参数保持 BSON 类型标记;跨系统交换优先用 BSON 而非 JSON。

4.5 思考题

  1. 如果每天有 1TB 增量数据,mongodump 全量备份需要 8 小时,有什么策略可以缩短备份窗口?
  2. 分片集群的 mongodump 备份和复制集的备份有什么关键区别?备份一个分片集群时如何保证跨分片数据的一致性?

(答案将在第 15 章末尾揭晓)


上一章思考题答案

  1. 用户有 database_A 的 readWrite 和 database_B 的 read,他可以执行跨库 $lookup——$lookup 需要外表有 find 权限,而 user 在 database_B 正好有 read 角色(包含 find 权限)。但如果 user 在 database_B 只有 read,他在 database_A 执行 aggregate 时做 $lookup 到 database_B 是被允许的,前提是 $lookup 的 from 集合与目标库匹配且权限覆盖。

  2. 客户端字段级加密(CSFLE)加密在客户端 Driver 层执行——数据在离开应用之前就已加密,MongoDB 只存储密文。解密同样在 Driver 层完成。数据库管理员看到的全是密文,没有解密密钥无法读取明文。密钥由独立的 KMS(如 AWS KMS、Azure Key Vault)管理,Driver 在内存中缓存密钥。这确保了即便 MongoDB 管理员或云服务商也无法直接读取敏感字段(如密码、身份证号)的明文。

延伸阅读与资源

Python 3实战精进:从脚本到高并发订单引擎
MongoDB 实战进阶与内核修炼
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经
Milvus向量数据库实战修炼:从 0 到 1精通向量检索与生产落地
后端工程师的 AI 转型第一课:Ollama 与私有化大模型实战
10倍开发者的 Dify 魔法书:从零构建全栈 AI 应用
后端工程师转型AI第一课-Ollama 与私有化大模型实战
大型语言模型(LLM) vLLM 高性能推理落地实战
Agent开发之LlamaIndex 实战修炼与源码进阶
大语言模型Transformers 实战修炼与源码剖析

posted on 2026-07-27 11:05  一天不进步,就是退步  阅读(5)  评论(0)    收藏  举报