MongoDB 分片键选择策略

候选人小张在字节 P7 架构面中,面试官问:

"你们 MongoDB 用什么分片键?为什么?"

小张说:"用 order_id 分片。"

面试官追问:"那如果查某个用户的订单呢?"

小张说:"那就需要扫描所有分片了..."

面试官继续追问:"那你觉得应该用什么分片键?"

小张答不上来了。

【面试官心理】 这道题我用来测试候选人对 MongoDB 分片键选择的理解深度。能说出分片键重要性的占 50%,能讲清选择原则的占 20%,能说清常见场景分片键的占 10%。

一、分片键的重要性 🔴

1.1 分片键决定了数据分布

// 分片键选择不当的后果:
// 1. 数据分布不均(热分片)
// 2. 查询变成广播查询(性能差)
// 3. 无法高效执行范围查询
// 4. 分片键无法修改

1.2 分片键不可更改

// ⚠️ MongoDB 不允许修改分片键
// 如果必须修改,只能:
// 1. 导出数据
// 2. 删除集合
// 3. 重新选择分片键
// 4. 导入数据

// 所以分片键选择要谨慎!

1.3 分片键的影响

分片键数据分布查询效率范围查询
好的分片键均匀高效支持
差的分片键不均匀广播查询不支持

二、分片键选择原则 🔴

2.1 基础原则

// 好的分片键应该:
// 1. 高基数(Cardinality):取值范围足够大
// 2. 低频率(Frequency):取值分布均匀
// 3. 常用查询字段:匹配大部分查询
// 4. 不可更改:一旦选择无法修改

2.2 常见分片键策略

// 策略1:哈希分片
// 适用于:写入量大,查询随机
sh.shardCollection("test.logs", {log_id: "hashed"})
// 数据均匀分布,但不支持范围查询

// 策略2:范围分片
// 适用于:需要范围查询,查询有局部性
sh.shardCollection("test.orders", {order_id: 1})
// 相邻的 _id 在同一分片,支持范围查询

// 策略3:复合分片键
// 适用于:单一字段基数不够
sh.shardCollection("test.events", {user_id: 1, create_time: -1})
// 查询 user_id 时高效,也可以按时间范围查询某用户数据

三、常见场景分片键 🟡

3.1 用户数据

// 场景:用户中心,按 user_id 查询
// ❌ 差的选择:_id (ObjectId 无序)
// ❌ 差的选择:created_at (时间集中)

sh.shardCollection("users", {user_id: 1})
// ✅ 好:user_id 高基数,直接定位

// 如果需要按地区查询:
sh.shardCollection("users", {region: 1, user_id: 1})
// 前缀匹配 region,user_id 保证高基数

3.2 订单数据

// 场景:订单系统,需要按用户查订单、按时间查订单
// ❌ 差的选择:order_id (无业务意义)
// ❌ 差的选择:created_at (时间集中)

sh.shardCollection("orders", {user_id: 1, order_id: 1})
// ✅ 好:按用户查高效
// ✅ 好:order_id 保证高基数

// 如果主要按时间范围查询:
sh.shardCollection("orders", {created_at: 1, order_id: 1})
// ✅ 好:按时间范围查询高效
// ⚠️ 问题:user_id 查询会广播

3.3 时序数据

// 场景:日志、监控数据、IoT 数据
// 高频写入,按时间查询

sh.shardCollection("logs", {device_id: 1, timestamp: -1})
// ✅ 好:device_id 保证数据按设备分布
// ✅ 好:timestamp 倒序,同设备数据按时间倒序

// 或者使用范围分片 + 预分割
sh.shardCollection("logs", {timestamp: 1})
// ✅ 好:按时间范围查询高效
// ✅ 好:时间单调递增,写入分布均匀

四、分片键避坑 🟡

4.1 低基数分片键

// ❌ 错误:性别只有 male/female 两个值
sh.shardCollection("users", {sex: 1})
// 所有 male 在一个分片,所有 female 在另一个分片
// 数据无法分散到多个分片

// ✅ 正确:使用高基数字段
sh.shardCollection("users", {user_id: 1, sex: 1})

4.2 递增分片键

// ❌ 错误:使用自增 ID 分片
sh.shardCollection("orders", {order_id: 1})
// 所有新订单写入同一个分片
// 造成写入热点

// ✅ 正确:使用哈希分片
sh.shardCollection("orders", {order_id: "hashed"})
// 数据均匀分布到所有分片

// ✅ 正确:使用业务上有意义的复合键
sh.shardCollection("orders", {user_id: 1, order_id: 1})
// 按用户分片,用户订单分布均匀

4.3 随机分片键 vs 业务分片键

类型示例优点缺点
随机分片_id: hashed数据均匀无法范围查询
业务分片user_id: 1查询高效可能不均匀
时间分片timestamp: 1范围查询写入热点

五、分片键变更 🟡

5.1 无法直接修改分片键

// MongoDB 不允许直接修改分片键
// 必须重新创建集合

// 步骤:
// 1. 导出原集合数据
// 2. 删除原集合(drop)
// 3. 用新分片键创建集合
// 4. 导入数据

5.2 辅助分片键

// 如果分片键无法满足某些查询
// 可以使用辅助索引

// 例如:user_id 分片,但需要按时间查所有订单
db.orders.createIndex({created_at: -1, order_id: 1})
// 虽然不能直接定位分片,但可以利用索引

5.3 MongoDB 5.0+ 变更流

// MongoDB 5.0+ 支持更改流(Change Streams)
// 可以监听数据变更,同步到其他系统

const changeStream = db.orders.watch([
    { $match: { operationType: "insert" } }
]);

changeStream.on("change", (change) => {
    console.log(change.fullDocument);
});
💡

分片键选择是 MongoDB 架构设计中最关键的决定。选择前要充分分析查询模式和数据分布。一经选择,无法更改。

【面试官心理】 能说出"分片键不可更改"和"递增分片键造成写入热点"的候选人,基本都有实际踩坑经验。这是 P7 的水准。