这是《MongoDB 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 分片键决定数据分布、查询路由、写入热点、chunk 能否分裂和事务范围。它是 MongoDB 分片架构中最难修改的决策,必须在业务建模阶段认真评估。
18.1 设计目标
一个好的分片键应满足:
- 高基数:可取值足够多;
- 低频率:单值数据不集中;
- 单调性可控:递增字段不把写入集中到一个 shard;
- 高频查询包含分片键;
- 写入分布均匀;
- chunk 可以分裂;
- 事务跨 shard 少;
- 业务语义清晰。
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,形成热点。
替代方案:
- 哈希分片;
- 复合前缀打散;
- 用户 ID 或设备 ID;
- 业务分区键 + 时间;
- 粗粒度桶 + 前缀随机。
18.4 哈希分片
创建:
sh.shardCollection("shop.orders", { user_id: "hashed" })
优点:
- 写入分布均匀;
- 对递增键友好;
- 减少热点。
限制:
- 范围查询通常广播;
- 相邻数据不在同一 chunk;
- 仍然依赖字段基数;
- 热点单值无法解决。
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 })
优点:
- 支持租户定向;
- 每个租户内部可分布;
- 写入更均匀;
- 便于 zone 隔离。
风险:
- 大租户仍可能热点;
- 查询不带前缀无法定向;
- 事务范围可能跨 shard;
- 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 热点识别
信号:
- 单 shard 写入明显高于其他;
- 某 chunk 持续过大;
- 集合写入延迟高但总量不高;
- balancer 无法平衡;
- cache 压力集中在单 shard;
- 某个分片键值文档巨大。
查看:
sh.status()
db.orders.aggregate([
{ $group: { _id: "$tenant_id", count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 20 }
])
18.8 分片键变更
历史版本中分片键基本不可修改;较新版本提供的能力和限制随版本变化。生产操作前必须确认:
- 当前版本是否支持;
- 集合是否满足条件;
- 是否需要停写或低峰;
- 磁盘空间是否足够;
- 查询是否全部更新;
- 回滚窗口;
- 官方操作说明。
很多团队更稳妥的做法是新建集合、双写迁移、验证后切换。
18.9 设计验证
用真实数据验证:
top 20 queries
write distribution simulation
cardinality statistics
key frequency distribution
chunk split simulation
cross-shard transaction rate
压测:
- 写入吞吐;
- P99 延迟;
- 查询路由;
- balancer 活动;
- cache 压力;
- 网络流量。
18.10 反模式
| 反模式 | 后果 |
|---|---|
| 时间范围键 | 写热点 |
| 状态字段键 | 只有少数值 |
| 空值占比高 | jumbo chunk |
| 查询不带分片键 | scatter-gather |
| 大租户单独键 | 租户热点 |
| 随意改分片键 | 迁移风险高 |
| 过早分片 | 复杂度收益不成比例 |
本章小结
分片键要同时考虑基数、频率、写入分布和查询路由。范围分片利于范围查询但易热点,哈希分片分布均匀但范围查询代价高,复合分片键常用于租户与实体组合。设计后必须用真实数据分布和查询压测验证。
思考题
- 为什么高基数不等于没有热点?
- 自增时间键为什么容易热点?
- 哈希分片的代价是什么?
- 复合分片键的前缀约束是什么?
- 如何发现大租户热点?