MongoDBNotes

第 18 章:分片键设计

zjc 于 2026-01-18 发布

这是《MongoDB 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 分片键决定数据分布、查询路由、写入热点、chunk 能否分裂和事务范围。它是 MongoDB 分片架构中最难修改的决策,必须在业务建模阶段认真评估。

18.1 设计目标

一个好的分片键应满足:

  1. 高基数:可取值足够多;
  2. 低频率:单值数据不集中;
  3. 单调性可控:递增字段不把写入集中到一个 shard;
  4. 高频查询包含分片键;
  5. 写入分布均匀;
  6. chunk 可以分裂;
  7. 事务跨 shard 少;
  8. 业务语义清晰。

18.2 基数与频率

低基数示例:

shard key = region
possible values = north, south, east, west

只有四个值,最多有效分布到有限 chunk。

高基数示例:

shard key = user_id
millions of values

频率问题:

user_id = null 占 80% documents

即使字段基数高,某个值占比过高仍会形成 jumbo chunk。

18.3 单调递增键的问题

使用自增 ID 或时间作为范围分片键:

time: 10:00 -> shard last
time: 10:01 -> shard last
time: 10:02 -> shard last

新写入集中到最后一个 chunk,形成热点。

替代方案:

  1. 哈希分片;
  2. 复合前缀打散;
  3. 用户 ID 或设备 ID;
  4. 业务分区键 + 时间;
  5. 粗粒度桶 + 前缀随机。

18.4 哈希分片

创建:

sh.shardCollection("shop.orders", { user_id: "hashed" })

优点:

  1. 写入分布均匀;
  2. 对递增键友好;
  3. 减少热点。

限制:

  1. 范围查询通常广播;
  2. 相邻数据不在同一 chunk;
  3. 仍然依赖字段基数;
  4. 热点单值无法解决。

18.5 复合分片键

查询:

db.orders.find({
  tenant_id: "t_100",
  created_at: { $gte: start, $lt: end }
})

分片键:

sh.shardCollection("multi.orders", { tenant_id: 1, user_id: 1 })

优点:

  1. 支持租户定向;
  2. 每个租户内部可分布;
  3. 写入更均匀;
  4. 便于 zone 隔离。

风险:

  1. 大租户仍可能热点;
  2. 查询不带前缀无法定向;
  3. 事务范围可能跨 shard;
  4. chunk 边界更复杂。

18.6 常见业务分片键

业务 候选
订单 tenant_id + order_id
用户行为 user_id + event_time
IoT 数据 device_id + timestamp
聊天消息 conversation_id + message_id
审计日志 service_id + request_id
商品评价 sku_id + review_id
文档服务 tenant_id + doc_id

尽量避免只用时间、自增 ID 或状态这类低基数字段。

18.7 热点识别

信号:

  1. 单 shard 写入明显高于其他;
  2. 某 chunk 持续过大;
  3. 集合写入延迟高但总量不高;
  4. balancer 无法平衡;
  5. cache 压力集中在单 shard;
  6. 某个分片键值文档巨大。

查看:

sh.status()
db.orders.aggregate([
  { $group: { _id: "$tenant_id", count: { $sum: 1 } } },
  { $sort: { count: -1 } },
  { $limit: 20 }
])

18.8 分片键变更

历史版本中分片键基本不可修改;较新版本提供的能力和限制随版本变化。生产操作前必须确认:

  1. 当前版本是否支持;
  2. 集合是否满足条件;
  3. 是否需要停写或低峰;
  4. 磁盘空间是否足够;
  5. 查询是否全部更新;
  6. 回滚窗口;
  7. 官方操作说明。

很多团队更稳妥的做法是新建集合、双写迁移、验证后切换。

18.9 设计验证

用真实数据验证:

top 20 queries
write distribution simulation
cardinality statistics
key frequency distribution
chunk split simulation
cross-shard transaction rate

压测:

  1. 写入吞吐;
  2. P99 延迟;
  3. 查询路由;
  4. balancer 活动;
  5. cache 压力;
  6. 网络流量。

18.10 反模式

反模式 后果
时间范围键 写热点
状态字段键 只有少数值
空值占比高 jumbo chunk
查询不带分片键 scatter-gather
大租户单独键 租户热点
随意改分片键 迁移风险高
过早分片 复杂度收益不成比例

本章小结

分片键要同时考虑基数、频率、写入分布和查询路由。范围分片利于范围查询但易热点,哈希分片分布均匀但范围查询代价高,复合分片键常用于租户与实体组合。设计后必须用真实数据分布和查询压测验证。

思考题

  1. 为什么高基数不等于没有热点?
  2. 自增时间键为什么容易热点?
  3. 哈希分片的代价是什么?
  4. 复合分片键的前缀约束是什么?
  5. 如何发现大租户热点?