分库分表实战与 ShardingSphere
当单库单表的数据量持续增长(千万、上亿),或并发写入远超单库上限时,就需要分库分表。本文系统讲解拆分策略、分片键选择、全局 ID、扩容与跨库 Join/事务,以及主流的 ShardingSphere 中间件实践。
一、为什么需要分库分表
单库单表的容量与性能瓶颈:
- 数据量瓶颈:单表数据上亿后,B+ 树层级变深,索引维护和扫描成本上升,查询变慢。
- 写入瓶颈:单库写并发受限(锁竞争、binlog、主从复制延迟)。
- 存储瓶颈:单机磁盘容量有限,无法无限扩展。
分库分表的目的:通过水平拆分把数据分布到多库多表,降低单表数据量、分散读写压力,实现水平扩展。
二、拆分策略
2.1 垂直拆分(按业务/字段)
- 垂直分库:按业务模块拆到不同库(订单库、用户库、商品库),各库独立,降低耦合与并发压力。
- 垂直分表:把一张大表的字段拆分(如把不常访问的大字段拆到扩展表),减少行宽,提高单页记录数。
垂直分库: 订单库(订单表) | 用户库(用户表) | 商品库(商品表)
垂直分表: user(常用字段) + user_ext(大字段: bio/avatar)2.2 水平拆分(按行数据)——核心
水平分表:把同一张表的数据按某规则分到多张结构相同的表。 水平分库:把数据分布到多个数据库实例。
水平分表: order_0, order_1, order_2, order_3(按 order_id 取模)
水平分库: db_order_0, db_order_1(每库内再分表)2.3 分片算法
| 算法 | 说明 | 示例 | 特点 |
|---|---|---|---|
| 取模 | hash(key) % 表数 | user_id % 4 | 简单,但扩容需重分片 |
| 范围 | 按 ID/时间区间 | id < 1000 -> t0 | 扩容友好,但可能数据倾斜 |
| 一致性哈希 | 环形哈希,减少迁移 | 均匀分布 + 虚拟节点 | 扩容只需迁移少量数据 |
| 哈希分片 | 对分片键做哈希 | md5(user_id) % N | 分布均匀 |
取模 vs 范围的选择:
- 取模:数据均匀,但扩容困难(从 4 变 8 需要 rehash 迁移大量数据)。
- 范围:扩容简单(新表即可),但热点可能集中在某段(如新订单都在最新表)。
三、分片键(Sharding Key)的选择
分片键是分库分表的核心决策,选错会导致大部分查询变成"全分片扫描"。
选择原则:
- 高基数:取值足够多,避免单分片数据倾斜。
- 高频查询条件:业务查询最多的字段(如
user_id、order_id)应作为分片键。 - 均匀分布:保证各分片数据量大致均匀。
- 避免跨分片关联:分片键相同的记录尽量落在同一分片。
常见问题——非分片键查询怎么办:
- 若经常用
order_id查但按user_id分片,可用 映射表(维护 order_id→user_id 关系)或 冗余字段(在表里冗余 user_id)。 - 或者对热点查询字段建立 全局索引表,先查到分片再查询。
四、全局 ID(分布式 ID)
分库分表后,数据库自增主键失效,需要全局唯一 ID。常用方案(详见「分布式 ID 生成方案」):
- 雪花算法(Snowflake):趋势递增、高性能,主推。
- 号段模式(Leaf-segment):批量取号。
- Redis INCR:简单但依赖 Redis。
- UUID:无序、过长,不适合主键。
分片键若用全局 ID,建议ID 末尾带分片信息(如 snowflake 的机器位/或 ID 后拼分片号),便于按 ID 定位分片,避免每次都 rehash。
五、跨库 Join 与分布式事务
分库分表最大的代价是牺牲了关系型数据库的关联与事务能力:
5.1 跨库 Join 的解决
- 宽表冗余:把关联字段冗余到查询侧,避免 Join。
- 应用层组装:拆成多次单表查询,在应用层(内存)组装结果。
- 全局表:字典、配置等小表在所有分片冗余一份(广播表)。
- 中间件聚合:ShardingSphere 支持跨库 Join(有性能成本)。
5.2 分布式事务
跨库写操作无法用单库事务,需分布式事务方案(详见「分布式事务与最终一致性」):
- 2PC/3PC:强一致,性能差。
- TCC:补偿式,性能较好,实现复杂。
- 本地消息表 + 消息队列:最终一致,主流。
- Seata(AT 模式):基于代理的最终一致方案,业界常用。
六、数据扩容与迁移
取模分片的扩容难题:从 N 个分片扩到 M 个,hash(key) % N 变成 % M,几乎所有数据都要迁移。
解决思路:
- 一致性哈希:只有部分数据需要迁移,但实现复杂。
- 翻倍扩容(2N→2N):取模扩容时,key 原本落在
% 2N的,扩容到% 4N后,只有一半数据需要迁移,且迁移逻辑简单(旧分片的一半)。 - 双写 + 迁移 + 切换:写两个集群,迁移完成后切换读流量。
- 基于 binlog 的增量迁移:用订阅 binlog 把增量数据同步到新分片,完成后再全量校验。
七、ShardingSphere 中间件
ShardingSphere 是 Apache 顶级开源项目,核心产品:
| 产品 | 定位 | 说明 |
|---|---|---|
| ShardingSphere-JDBC | JDBC 增强(客户端) | 应用内代理,改连接即可,无需独立部署,主流 |
| ShardingSphere-Proxy | 独立代理(服务端) | 模拟数据库,客户端无需改动 |
| ShardingSphere-Sidecar | 云原生 | 用于 Kubernetes 场景 |
7.1 ShardingSphere-JDBC 使用(以 Spring Boot + MyBatis 为例)
# application.yml
spring:
shardingsphere:
datasource:
names: ds0,ds1
ds0:
url: jdbc:mysql://localhost:3306/order_db_0
ds1:
url: jdbc:mysql://localhost:3306/order_db_1
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds${0..1}.t_order_${0..3}
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: order_inline
key-generate-strategy:
column: order_id
key-generator-name: snowflake
sharding-algorithms:
order_inline:
type: INLINE
props:
algorithm-expression: t_order_${order_id % 4}
key-generators:
snowflake:
type: SNOWFLAKE// 使用上与普通 Mapper 完全一致,ShardingSphere 自动路由分片
orderMapper.insert(order); // 自动生成雪花ID并按 order_id 路由
Order o = orderMapper.selectById(id); // 自动路由到正确分片ShardingSphere-JDBC 的核心能力:
- SQL 路由:根据分片键把 SQL 路由到正确的数据源/表。
- 分片结果归并:多分片查询结果合并、排序、分页。
- 主键生成:内置雪花算法。
- 读写分离:读写分离配置。
- 分布式事务:集成 Seata。
7.2 使用注意事项
- 没有分片键的查询会广播到全部分片再归并,性能差,尽量避免。
- 不支持的部分 SQL:部分跨分片的子查询、
LIMIT跨分片的深度分页需注意。 - 分布式事务:ShardingSphere 支持事务类型需在配置中指定(LOCAL/XA/BASE)。
- 深度分页(如
LIMIT 100000,10)在分片下会全分片取出再归并,性能差,需用游标/分页键优化。
八、分库分表的时机与代价
不要过早分库分表。它带来的问题:
- 复杂度剧增:SQL 路由、事务、Join 全部受影响。
- 运维成本高:多实例、多表维护。
- 扩容迁移风险大。
建议:优先用读写分离、缓存、索引优化、归档冷数据解决;当单表数据量到千万级且持续增长、写入并发成为瓶颈时,再考虑分库分表,并做好分片键设计与扩容预案。
小结
- 分库分表核心是水平拆分,关键是选对分片键和分片算法(取模/范围/一致性哈希)。
- 全局 ID 用雪花算法;跨库 Join 用宽表/应用组装/广播表;跨库事务用分布式事务方案。
- ShardingSphere-JDBC 是最主流的分库分表方案,应用透明接入。
- 分库分表是最后的手段,优先用缓存、索引、归档等低成本方案,并做好扩容预案。
版本差异(MySQL 5.7 → 8.0/8.4)
| 特性 | 旧版(本文编写时,MySQL 5.7) | 当前(MySQL 8.0/8.4 LTS) |
|---|---|---|
| 默认字符集 | utf8(需显式配置 utf8mb4) | utf8mb4(MySQL 8.0 起默认) |
| 索引 | 普通 B+Tree | 降序索引、隐藏索引、函数索引(8.0+) |
| SQL 能力 | 常规查询 | 递归 CTE、窗口函数(8.0+) |
| 版本策略 | 5.7 | 8.0(主流)/ 8.4 LTS / 9.x(创新版) |
| Java 驱动 | mysql-connector-java 5.x/8.0 | mysql-connector-j 8.x/9.x |
本文基于 MySQL 5.7 编写,核心概念(索引、事务、锁、MVCC、InnoDB)在 8.0/8.4 中依然适用;8.0 的默认字符集、隐藏索引与 SQL 增强(CTE/窗口函数)是升级后的主要差异。