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 不带任何一致性保证。如果你的备份涉及多个集合且它们互相引用(如订单引用优惠券),跨集合的一致性无法保证。
小白:那怎么保证跨集合的一致备份?
大师:三种方案:
- 业务停写:备份窗口内停止写入——最简单但停机成本高。
- 文件系统快照:LVM/ZFS 快照。在快照瞬间所有文件得到一个一致性时间点——快,但依赖底层存储支持。
- 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)的定期全量备份、按集合的细粒度恢复、开发/测试环境数据克隆。
不适用场景:
- 超大型数据库(TB 级)——mongodump 全量扫描时间过长,用文件系统快照。
- 需要秒级 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 思考题
- 如果每天有 1TB 增量数据,mongodump 全量备份需要 8 小时,有什么策略可以缩短备份窗口?
- 分片集群的 mongodump 备份和复制集的备份有什么关键区别?备份一个分片集群时如何保证跨分片数据的一致性?
(答案将在第 15 章末尾揭晓)
上一章思考题答案:
用户有 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 集合与目标库匹配且权限覆盖。客户端字段级加密(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 实战修炼与源码剖析

微信公众号: 架构师日常笔记 欢迎关注!
浙公网安备 33010602011771号