1. 分库分表基础概念解析
分库分表是数据库架构设计中应对数据量增长的经典解决方案。当单库单表的数据量达到千万级甚至亿级时,性能瓶颈会逐渐显现。我经历过一个电商项目,订单表数据量突破5000万后,简单查询都需要3秒以上响应时间,这就是典型的分库分表适用场景。
1.1 为什么要分库分表
数据库性能下降主要来自三个方面:
- IO瓶颈:单机磁盘吞吐量有限,频繁读写导致IO等待
- 连接数限制:MySQL默认最大连接数151,高并发时连接耗尽
- 锁竞争加剧:大量事务争抢同一资源的锁
通过将数据分散到不同数据库实例(分库)和不同表(分表),可以有效缓解这些问题。去年我们一个金融系统实施分库分表后,TPS从200提升到1200,效果立竿见影。
1.2 分库与分表的区别
| 维度 | 分库 | 分表 |
|---|---|---|
| 数据分布 | 不同数据库实例 | 同一实例的不同表 |
| 主要目的 | 分散连接压力 | 减少单表数据量 |
| 实施难度 | 较高(需跨实例事务) | 较低 |
| 典型场景 | 高并发读写 | 大数据量存储 |
实际项目中往往需要同时使用分库和分表,比如先按业务分库,再对单库中的大表进行分表
2. 分库分表核心策略
2.1 水平拆分 vs 垂直拆分
水平拆分(横向拆分)是最常用的方式,将同一表的不同行数据分散到不同库/表。例如订单表按订单ID范围拆分:
- 订单ID 1-1000万 → 分表1
- 订单ID 1000万-2000万 → 分表2
垂直拆分(纵向拆分)则是按列拆分,把不常用的字段或大字段单独存放。比如把商品详情字段从商品主表拆分出来。
2.2 常见分片策略
2.2.1 哈希取模分片
java复制// 示例:按用户ID分4个库
int dbIndex = userId.hashCode() % 4;
优点:数据分布均匀
缺点:扩容需要数据迁移
2.2.2 范围分片
按时间范围或ID范围分片,如:
- 2023年订单 → 库1
- 2024年订单 → 库2
优点:易于扩容和历史数据归档
缺点:可能产生热点
2.2.3 一致性哈希
使用哈希环减少扩容时的数据迁移量,适合需要频繁扩容的场景。
3. 技术实现方案
3.1 客户端分片 vs 中间件分片
客户端分片:在应用层实现分片逻辑
python复制# 示例:Django分片路由
class UserShardingRouter:
def db_for_read(self, model, **hints):
if model._meta.model_name == 'user':
return f'user_db_{hash(hints['instance'].id) % 3}'
中间件分片:使用MyCat、ShardingSphere等中间件
| 方案 | 优点 | 缺点 |
|---|---|---|
| 客户端分片 | 性能好,无额外组件 | 侵入性强,各语言需实现 |
| 中间件分片 | 对应用透明,支持多语言 | 存在单点风险,性能损耗 |
3.2 分布式ID生成
分库分表后,自增ID不再适用。常用方案:
- UUID:简单但无序影响性能
- 雪花算法:64位ID = 时间戳(41位)+机器ID(10位)+序列号(12位)
- 号段模式:从数据库批量获取ID段
4. 实战问题与解决方案
4.1 跨库JOIN问题
解决方案:
- 字段冗余:将关联字段冗余到主表
- 多次查询:先查主表再查关联表
- 数据异构:使用ES等搜索引擎建立宽表
4.2 分布式事务
典型方案对比:
| 方案 | 一致性 | 性能 | 复杂度 |
|---|---|---|---|
| XA协议 | 强一致 | 差 | 高 |
| TCC | 最终 | 中 | 中 |
| SAGA | 最终 | 好 | 低 |
| 本地消息表 | 最终 | 好 | 低 |
4.3 扩容数据迁移
我们采用的双写方案步骤:
- 新库配置双写代理
- 旧数据全量迁移
- 增量数据双写
- 校验数据一致性
- 切换读流量
- 最终切换写流量
5. 监控与优化
5.1 关键监控指标
- 分片均衡度:各分片数据量差异
- 热点分片:QPS异常高的分片
- 慢查询:跨分片查询性能
5.2 常见优化手段
- 索引优化:确保分片键有索引
- 查询优化:避免跨分片查询
- 缓存策略:热点数据多级缓存
实施分库分表后,我们的订单查询P99从5秒降到了200毫秒以内。但要注意,分库分表是最后手段,只有当单表数据确实过大时才应考虑。前期可以通过读写分离、缓存等手段先尝试优化。
