大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
复制集与分片
这一课你将能做到
- 说清楚复制集的角色、选举机制与 oplog 的作用,能本地搭一个 3 节点复制集
- 解释 writeConcern / readConcern / readPreference 三者的语义与组合
- 理解分片架构(mongos / config server / shard)与片键选择原则,会用 mongodump/mongorestore 做备份恢复
为什么要学
单机 MongoDB 有两个天花板:机器挂了服务就停(可用性),数据量和 QPS 超过单机就撑不住(扩展性)。复制集解决第一个问题——三节点自动选主,主挂了 10 秒内新主上位;分片解决第二个问题——数据按片键水平切到多台机器。
面试高频:「MongoDB 高可用怎么做的?」「分片片键怎么选?」答不上来就说明只玩过单机。
先建立图像
复制集(Replica Set):同一份数据存 N 份
┌─────────────┐
写 ───────> │ PRIMARY │ 唯一可写
└──────┬──────┘
oplog 复制 │
┌────────────┼────────────┐
▼ ▼ ▼
SECONDARY SECONDARY (ARBITER 只投票不存数据)
│ │
└── 主挂了 → 投票 → 新 PRIMARY(自动故障转移)
分片(Sharding):不同数据存不同机器
应用
│
mongos(路由,无状态)
│ 按片键路由
┌──────┼──────┐
shard1 shard2 shard3 (每个 shard 本身是一个复制集)
[userId [userId [userId
1-1000] 1001-2k] 2k-3k]
config server:存「哪个区间在哪个 shard」的元数据(也是复制集)
writeConcern 直觉:
w: 1 主节点写完就返回(宕机可能丢)
w: "majority" 多数节点确认才返回(主挂了也不丢) ← 关键数据用这个
w: 3 3 个节点都确认(慢,一般不用)
j: true 且写入磁盘日志
跟着做
步骤 1:本地 3 节点复制集(Docker 最省事)
docker network create mongo-net
docker run -d --name m1 --net mongo-net -p 27017:27017 mongo:7 --replSet rs0 --bind_ip_all
docker run -d --name m2 --net mongo-net -p 27018:27017 mongo:7 --replSet rs0 --bind_ip_all
docker run -d --name m3 --net mongo-net -p 27019:27017 mongo:7 --replSet rs0 --bind_ip_all
步骤 2:初始化复制集
mongosh --port 27017
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "m1:27017" },
{ _id: 1, host: "m2:27017" },
{ _id: 2, host: "m3:27017" }
]
})
rs.status() // 看成员角色:一个 PRIMARY,两个 SECONDARY
步骤 3:观察复制与故障转移
主节点写数据:
use codeforge
db.submissions.insertOne({ userId: 1024, lessonSlug: "mdbrm-ops", status: "accepted" })
另开一个终端连从节点(需先允许读从):
mongosh --port 27018
rs.secondaryOk() // 旧命令,新版本用 db.getMongo().setReadPref("secondaryPreferred")
use codeforge
db.submissions.find() // 数据已复制过来
模拟主宕机:
docker stop m1
回主终端执行 rs.status(),几秒内 m2 或 m3 变成 PRIMARY——自动故障转移完成。docker start m1 后它以 SECONDARY 身份回归。
步骤 4:writeConcern 实战
// 关键写:多数派确认
db.submissions.insertOne(
{ userId: 2048, lessonSlug: "mdbrm-ops", status: "accepted" },
{ writeConcern: { w: "majority", j: true, wtimeout: 5000 } }
)
验证复制延迟:从节点读到的数据可能滞后毫秒级,对「提交后立即查看」场景用默认 readPreference(primary)读主。
步骤 5:readConcern 语义
// 只读「已复制到多数派」的数据,避免读到将来会被回滚的写
db.submissions.find().readConcern("majority")
经典坑:主节点写入 w:1 后还没复制,主挂了,新主上没有这条数据——旧主恢复后这条写被回滚。readConcern majority 让你永远读不到这种「未确认的写」。
步骤 6:备份与恢复
# 备份整个 codeforge 库
docker exec m1 mongodump --db codeforge --out /data/backup
# 拷出来
docker cp m1:/data/backup ./backup
# 恢复到新实例
docker cp ./backup m1:/data/restore
docker exec m1 mongorestore --db codeforge /data/restore/codeforge
时间点恢复(PITR)需要 oplog 重放或 Atlas 的持续备份,生产环境直接用 Atlas/云厂商的自动备份最省心。
步骤 7:分片概念验证(了解即可,本地一般不真搭)
片键选择的三个维度:
// 示例:进度集合按 userId 哈希分片——均匀打散,适合点查
sh.shardCollection("codeforge.progress", { userId: "hashed" })
// 区间分片:按时间——范围查询友好,但新数据全打最新分片(热点)
sh.shardCollection("codeforge.events", { createdAt: 1 })
片键三原则:基数高(取值多,分得开)、写分布均匀(避免单点热点)、匹配主要查询模式(查询带片键 = 单分片定向,不带 = 广播所有分片)。
讲清楚要点
- 复制集最少 3 个投票成员(或 P-S-A 仲裁者结构):2 节点无法在网络分区时形成多数派,会双双不可用。多数派 = ⌊N/2⌋+1。
- oplog 是复制的血液:主节点的所有写先记录到 local.oplog.rs(一个 capped collection),从节点不断拉取重放。change streams(生态集成课)也是读它。
- 选举基于 Raft 变体:多数派投票、有任期概念;网络分区时少数派一侧自动降级只读。
- writeConcern 管「写多稳」,readConcern 管「读多新」,readPreference 管「从哪读」——三者正交,按业务组合。
- 分片的每个 shard 仍是复制集:分片管数据分布,复制集管每份数据的高可用,两者叠加。
- 片键一旦设定不可改(7.0 起支持 resharding 换片键,但成本高),设计阶段要想清楚。
- mongodump 是逻辑备份:导出 BSON,恢复慢但灵活;大数据量生产环境用文件系统快照或 Atlas 备份更快。
你可能会懵的地方
- 从节点可以写吗? 永远不能。所有写都走主节点,从节点只复制和提供读。这是和「多主」数据库(如某些 MySQL 方案)的本质区别。
- 主挂了正在写的数据会丢吗? w:1 写入可能丢(未复制到从节点);w:"majority" 不会丢——新主一定包含多数派确认的写。这就是关键数据必须 majority 的原因。
- 分片后还能用唯一索引吗? 唯一索引必须包含片键,否则 MongoDB 无法只查一个分片就保证全局唯一。
- ARBITER 有什么用? 凑票数不存数据。主+从一主一备两个数据节点时,加一个 arbiter 让集群在单点故障时仍能选出多数派。省钱方案,能 3 数据节点就别用。
- 复制延迟大怎么办?
rs.printSecondaryReplicationInfo()看延迟。延迟大通常是从节点 IO 瓶颈、网络问题或 oplog 写入洪峰;读对延迟敏感就走主节点。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 生产 2 节点复制集 | 分区时无法选主,集群不可用 | 3 节点或加 arbiter |
| 所有写都用 w:1 | 主宕机丢数据 | 关键写 w:"majority" + j:true |
| 片键选低基数字段(如 status) | 数据分不开,巨型 chunk 无法均衡 | 高基数字段或哈希片键 |
| 片键选单调递增(时间戳/_id) | 新写全砸最新分片,热点 | 点查多就用 hashed 片键打散 |
| 以为 mongodump 能不停机一致备份 | dump 期间写入可能不一致 | 加 --oplog 重放,或用快照/Atlas |
卡点急救
rs.initiate报 unreachable:容器间主机名不通。确认都在同一 docker network,host 用容器名而非 localhost。- 从节点查到旧数据:复制延迟,正常现象。需要「读己之写」就强制读主:
db.getMongo().setReadPref("primary")。 - 主节点重启后旧写消失:那些写只到 w:1。检查 writeConcern 配置;已被回滚的数据在旧主的 rollback 目录里,需要人工恢复。
面试常问
- MongoDB 高可用怎么实现? 复制集:一主多从 + oplog 异步复制 + Raft 式选举自动故障转移(典型 10 秒级)。配合 w:majority 保证不丢数据,readPreference 控制读写分布。
- 什么时候需要分片? 两个信号:单机存储放不下(磁盘),或单机写吞吐扛不住(分片主要扩写)。数据量 TB 级之前,升级硬件 + 复制集通常够用,分片运维成本高,不要过早引入。
- 片键怎么选? 高基数、写均匀、贴合查询模式。经典反例:时间戳(写热点)、自增 ID(同上)、低基数枚举(分不开)。常见答案:业务 ID 的哈希,或「租户 ID + 时间」复合片键兼顾隔离与范围查询。
自测三问
- 为什么复制集要奇数个投票成员?
- w:"majority" 为什么能防止主切换后丢数据?
- 用
{ createdAt: 1 }做片键会发生什么?什么查询模式它才合适?
小结一句话
复制集用「冗余 + 选举」换高可用,分片用「片键 + 路由」换水平扩展;writeConcern 是你和数据丢失之间的最后一道闸。
延伸
- 官方文档:Replication
- 官方文档:Sharding
- 下一课:安全与加密——认证、RBAC、TLS、字段级加密,给集群上锁。