分库分表策略与跨库查询¶
30 秒版(开场)¶
单库瓶颈时 垂直拆库(按业务)或 水平分片(按 shard key 取模/范围)。核心:选对 shard key(高基数、查询必带)、避免跨片事务与 join。生产关键词:扩容 rebalance、全局 ID、跨库分页、双写迁移。
3 分钟版(精讲深度)¶
- 是什么:垂直拆分把用户/订单/商品分库;水平拆分把同表数据按规则散到多库多表
orders_0..15。 - 为什么:单机连接数、磁盘、备份、锁竞争上限;单表亿级行索引维护成本陡增。
- 怎么做:优先索引、归档、读扩展和垂直边界;必须分片时选高覆盖率且分布可控的 key。扩容常用虚拟 shard/目录映射或翻倍迁移;一致性哈希并不是关系型分片的万能方案。
10 分钟版(原理 + 图示)¶
拆分策略
| 策略 | 规则 | 优点 | 缺点 |
|---|---|---|---|
| 取模 | hash(key) % N |
简单 | 扩容要迁移 |
| 范围 | user_id 1-1M → shard0 | 范围查方便 | 热点尾部 |
| 一致性哈希 | 虚拟节点 | 扩缩容迁移少 | 实现复杂 |
| 目录表 | 映射表查 shard | 灵活 | 目录成瓶颈 |
flowchart TB
App[Go Service] --> Router[Sharding Router]
Router -->|user_id % 4 = 0| DB0[(shard_0)]
Router -->|user_id % 4 = 1| DB1[(shard_1)]
Router -->|user_id % 4 = 2| DB2[(shard_2)]
Router -->|user_id % 4 = 3| DB3[(shard_3)]
App --> ES[Elasticsearch 跨片检索]
跨库难题:传统分片中跨片 JOIN/事务代价高,分布式 SQL/中间件虽可支持,也需评估协调与扫描成本;常见替代是应用聚合、读模型冗余。全局分页需各片 TopK 归并或独立索引;扩容采用回填/CDC、校验、切读和停止旧写。
中间件:ShardingSphere、Vitess、自研路由层。GORM dbresolver 主要做多库/读写分离,不会自动按 user_id 完成业务分片;分片键路由仍需插件或显式实现。
生产场景¶
- 订单表 5 亿行:按
user_id分 64 库,查询带 user_id;后台按 order_id 查需 order_id 嵌入 shard hint 或全局二级索引表。 - 多租户 SaaS:
tenant_id分片,大租户可 独立 shard(Silo)。 - 从单库迁移:双写新旧、Binlog 校验、按片灰度切流。
排查与工具¶
| 工具 | 用途 |
|---|---|
| 自研路由 metrics | 各 shard QPS/lag |
| pt-table-checksum | 双写一致性 |
| Vitess VTGate | 跨片查询计划 |
| 慢查询 per shard | 热点片 |
路径:某 shard CPU 高 → 是否 shard key 倾斜(大 V)→ 重分片或 silo;跨片慢 → 是否误做 fan-out 全片扫。
架构取舍¶
| 方案 | 适用 | 不适用 |
|---|---|---|
| 垂直拆库 | 业务边界清晰 | 单表仍亿级 |
| 水平分片 | 单表海量 | 频繁跨片 join |
| 归档 | 历史冷数据 | 实时查三年全量 |
| TiDB/Cockroach | 要分布式 SQL | 运维/成本 |
| ES 宽表 | 复杂列表筛选 | 强一致交易 |
深挖问答¶
- shard key 怎么选? → 查询 90% 带它、高基数、业务均匀;避免时间单一分片。
- 取模扩容 4→8? → 翻倍迁移:key%8 中一半留原库一半迁新库。
- 跨库事务? → Saga/最终一致,避免 XA。
- 全局 order by? → 各片 limit N 归并,或同步 ES。
- GORM 分片? → 多 DB 实例 + 路由层,或 sharding plugin。
反模式与事故¶
- 用
created_at分片——当月热点,历史片闲置。 - 跨 16 片
SELECT * WHERE mobile=?无 shard key——16 倍 RT。 - 扩容不停写直接改模——丢数据。
- 分片过早——单库 MySQL 仍可容纳 5000 万行良好索引。
代码示例¶
const shardCount = 16
func shardDB(userID uint64, dbs []*gorm.DB) *gorm.DB {
return dbs[userID%shardCount]
}
func GetOrder(ctx context.Context, userID, orderID uint64, dbs []*gorm.DB) (Order, error) {
db := shardDB(userID, dbs)
var o Order
err := db.WithContext(ctx).
Where("user_id = ? AND id = ?", userID, orderID).
First(&o).Error
return o, err
}