1. 为什么需要分库分表
单机 MySQL 的瓶颈通常出现在三个维度:
- 存储容量:单表数据量过亿后,索引膨胀、DDL 变慢、备份恢复时间不可控。
- 并发连接:单实例连接数与 CPU 核数有限,QPS 达到瓶颈后响应时间急剧上升。
- 写入吞吐:单主库的 binlog 与磁盘 IO 成为写入上限,主从延迟随之放大。
当垂直拆分(按业务拆库、按字段拆表)已经无法缓解时,就需要水平拆分,也就是常说的分库分表。
2. 先做垂直拆分,再做水平拆分
分库分表不是第一选择,合理的演进顺序是:
- SQL 与索引优化:慢查询治理、覆盖索引、避免大事务。
- 读写分离:主库写、从库读,缓解读压力。
- 缓存与异步化:热点数据前置到 Redis,非核心链路走消息队列。
- 垂直拆分:按业务域拆库(用户库、订单库、商品库),按冷热字段拆表。
- 水平拆分:单表数据量仍然过大时,才按分片键拆分。
3. 水平拆分的四种常见形态
| 形态 | 说明 | 适用场景 |
|---|---|---|
| 只分表 | 同一库内拆成多张物理表 | 单库容量够,只是单表过大 |
| 只分库 | 按分片键拆到多个库,表结构不变 | 单实例连接数或 IO 达到瓶颈 |
| 分库分表 | 既拆库又拆表 | 数据量与并发同时很高 |
| 分库分表 + 读写分离 | 每个分片再挂从库 | 读多写少的大流量场景 |
4. 分片键的选择
分片键决定了数据分布是否均匀,也决定了后续查询能否避免全路由。
- 用户维度 :
user_id,适合用户中心、账户、订单(按买家查询为主)。 - 订单维度 :
order_id,适合订单详情、支付流水。 - 商户维度 :
merchant_id,适合 B 端 SaaS 场景。 - 时间维度:按月/按年分表,适合日志、账单类冷热分明的数据。
选择原则:
- 尽量让高频查询条件与分片键一致,避免跨分片扫描。
- 分片键一旦确定,后期变更成本极高,需要预留扩展方案。
- 避免使用自增 ID 作为分片键,容易造成热点写入。
5. 常见分片算法
5.1 取模分片
sql
-- 假设分 4 个库,每个库 8 张表
-- 库序号 = user_id % 4
-- 表序号 = (user_id / 4) % 8
优点是数据分布均匀,缺点是扩容时需要大量数据迁移。
5.2 范围分片
按 ID 区间或时间区间划分,例如 user_id 在 0~1000万 落在分片 0。
优点是扩容简单、范围查询友好,缺点是容易出现热点分片。
5.3 一致性哈希
将分片节点和数据都映射到哈希环上,扩容时只影响相邻节点的数据。
适合节点数量动态变化的场景,但实现复杂度较高。
5.4 基因法
把分片键的基因位嵌入到业务主键中,使得按主键查询也能直接定位分片。
例如订单号生成时,把 user_id 的低 4 位拼接到订单号末尾,这样按 order_id 查询时也能反解出分片位置。
6. 主流中间件方案对比
| 方案 | 类型 | 特点 |
|---|---|---|
| ShardingSphere-JDBC | 客户端 | 轻量、无额外部署,与应用同进程 |
| ShardingSphere-Proxy | 代理 | 独立部署,支持多语言客户端 |
| MyCat | 代理 | 老牌方案,社区活跃度下降 |
| Vitess | 代理 | 云原生,适合 Kubernetes 环境 |
| 业务层自研 | 客户端 | 灵活可控,但开发与运维成本高 |
对于大多数中小团队,ShardingSphere-JDBC 是性价比最高的起点:无需额外运维组件,接入成本低,功能覆盖分片、读写分离、数据加密等。
7. 基于 ShardingSphere-JDBC 的配置示例
yaml
spring:
shardingsphere:
datasource:
names: ds0,ds1
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db_0
username: root
password: root
ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db_1
username: root
password: root
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..7}
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: db-inline
table-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: table-inline
sharding-algorithms:
db-inline:
type: INLINE
props:
algorithm-expression: ds$->{user_id % 2}
table-inline:
type: INLINE
props:
algorithm-expression: t_order_$->{user_id % 8}
对应的建表语句需要在每个物理库中执行:
sql
CREATE TABLE t_order_0 (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
order_no VARCHAR(64) NOT NULL,
amount DECIMAL(12, 2) NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_user_id (user_id),
KEY idx_order_no (order_no)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;
8. 分库分表带来的新问题
8.1 跨分片查询
不带分片键的查询会被广播到所有分片,性能急剧下降。常见应对:
- 建立异构索引表,按另一维度冗余一份数据。
- 引入 Elasticsearch 承担多条件检索。
- 对低频查询走离线数仓,不占用在线库资源。
8.2 分布式事务
跨库写入无法用本地事务保证一致性,可选方案:
- 本地消息表 + 定时补偿:实现简单,最终一致。
- Seata AT 模式:对业务侵入小,但需要额外部署 TC。
- TCC / Saga:适合强一致要求较高的核心链路。
8.3 全局唯一 ID
分库分表后自增主键不再全局唯一,常用方案:
- 雪花算法:时间戳 + 机器 ID + 序列号,趋势递增。
- 号段模式:数据库批量分配 ID 段,减少数据库压力。
- Redis INCR:实现简单,但依赖 Redis 可用性。
8.4 分页与排序
LIMIT offset, size 在分片场景下需要各分片取 offset + size 条再归并,深分页性能很差。建议改为游标分页 (基于 id > last_id)。
8.5 扩容与数据迁移
取模分片扩容时,通常采用成倍扩容(2 库扩到 4 库),配合双写 + 数据校验 + 灰度切流,降低迁移风险。
9. 方案选型建议
- 数据量在千万级、单库仍可承载:优先优化索引与读写分离,暂不分片。
- 单表过亿、查询基本带分片键:ShardingSphere-JDBC + 取模分片,成本最低。
- 多语言客户端、需要统一入口:选择 ShardingSphere-Proxy。
- 云原生、Kubernetes 环境:可评估 Vitess。
- 业务复杂、分片规则频繁变化:考虑业务层自研路由,但要评估长期维护成本。
10. 总结
分库分表是解决单机 MySQL 容量与并发瓶颈的有效手段,但它把简单问题变成了分布式问题。落地前应确认垂直拆分、读写分离、缓存等手段已经用尽,落地后要同步规划好跨分片查询、分布式事务、全局 ID、数据迁移等配套能力。选型上,ShardingSphere 生态是目前国内最主流、文档最完善的选择,适合作为大多数团队的起点。