分库分表实战:垂直/水平分片 + ShardingSphere 5.x
订单表破亿那一刻就知道——分表不是"拆成十张表"那么简单。最痛苦的决策不是选 ShardingSphere 还是 MyCat,而是选什么字段做分片键。选 user_id——按用户查订单飞快,但运营要"昨天的 Top 100 订单"——垮了,全库扫描。选 order_id——反过来。选错了键,80% 查询跨库,改回来全量数据迁移+停机窗口。这篇文章把我从选键→配置→扩容踩过的坑全记下来了,附一份分片键 4 原则决策清单。
阅读约 14 分钟 | 系列第 16/17 篇
一、为什么需要分库分表
1.1 单表的物理极限
MySQL 单表在 500 万行以下通常表现良好,超过 2000 万行后 B+Tree 层数增加、索引维护开销上升、DDL 操作(加字段/改索引)变得不可控。阿里的规范建议单表控制在 500 万以内,超过则考虑分表。
1.2 分库分表解决的核心问题
| 问题 | 表现 | 分库分表的解法 |
|---|---|---|
| 数据量过大 | 索引层级增加,查询变慢 | 水平分表——将一张表的数据分散到多张表 |
| 写入瓶颈 | 单库连接数有限,写操作排队 | 水平分库——写入分散到多个数据库实例 |
| 字段过多 | 一张表上百字段,查询总带大量无用列 | 垂直分片——按业务拆分为多张窄表 |
二、垂直分片 vs 水平分片
⚠️ 概念辨析:垂直分片与垂直拆分是两个容易混淆的概念。本文讨论的"垂直分片"指将不同表按业务模块拆分到不同库,不是指将一张表的列拆成多张表(那是"垂直拆分/分区")。
2.1 垂直分片
定义:将不同的表按照业务模块拆分到不同的数据库节点上。
垂直分片前(单库):
order_db: orders | order_items | users | products
垂直分片后:
order_db: orders | order_items ← 订单相关表
user_db: users ← 用户相关表
product_db: products ← 商品相关表
适用场景:不同业务表的访问频率和重要性差异大。交易表访问频繁需要高性能实例,日志表访问少可以用低成本实例——物理隔离让资源分配更精准。
2.2 水平分片
定义:将同一张表的数据行按照分片键拆分到多个数据库或多张表中。
水平分库分表:
ds0: orders_0, orders_1 ← user_id % 2 = 0 的数据
ds1: orders_0, orders_1 ← user_id % 2 = 1 的数据
分片键选择是水平分片最关键的设计决策——选错了整个架构都要推倒重来。
| 分片键 | 优点 | 缺点 |
|---|---|---|
| user_id | 用户维度数据集中,单用户查询不跨库 | 跨用户统计需要全库扫描 |
| order_id(雪花算法) | 数据均匀分布 | 按用户查订单需要路由到多个库 |
| create_time | 按时间段查询效率高 | 热点集中——所有写入压在最新日期对应的分片上 |
黄金法则:分片键 = 最高频查询的 WHERE 条件。如果 90% 的查询都带
user_id,用它做分片键就是正确的选择。
三、ShardingSphere 5.x 核心配置
3.1 ShardingSphere 是什么
Apache ShardingSphere 是一站式分布式数据库解决方案,核心组件:
| 组件 | 定位 | 原理 |
|---|---|---|
| Sharding-JDBC | 轻量级 JDBC 增强 | 拦截 SQL → 解析 → 路由 → 改写 → 执行 → 结果归并 |
| Sharding-Proxy | 透明数据库代理 | 作为 MySQL 协议的代理层,对客户端零侵入 |
选型建议:Java 技术栈项目直接用 Sharding-JDBC(零部署、性能最优);异构语言或需要对业务零侵入选 Sharding-Proxy。
3.2 5.x YAML 配置(关键变化)
ShardingSphere 5.x 的 YAML 语法相比 4.x 有重要变化。以下为水平分库分表的完整配置:
# ShardingSphere 5.x 水平分库分表配置
dataSources:
ds0:
url: jdbc:mysql://localhost:3306/order_db0
username: root
password: root
ds1:
url: jdbc:mysql://localhost:3306/order_db1
username: root
password: root
rules:
- !SHARDING
tables:
t_order:
actualDataNodes: ds$->{0..1}.t_order_$->{0..1} # ds0.t_order_0, ds0.t_order_1, ds1.t_order_0, ds1.t_order_1
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: database_inline
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: table_inline
shardingAlgorithms:
database_inline:
type: INLINE
props:
algorithm-expression: ds$->{user_id % 2}
table_inline:
type: INLINE
props:
algorithm-expression: t_order_$->{order_id % 2}
3.3 读写分离配置
rules:
- !READWRITE_SPLITTING
dataSources:
order_rw:
writeDataSourceName: ds0
readDataSourceNames:
- ds0_read1
- ds0_read2
loadBalancerName: round_robin
loadBalancers:
round_robin:
type: ROUND_ROBIN
强制走主库:HintManager.getInstance().setWriteRouteOnly()——关键业务(如支付后立即查订单状态)必须从主库读,避免主从延迟导致"支付成功了但查询显示未支付"。
四、广播表与绑定表
4.1 广播表
定义:数据量小、变更少、但每个分片都需要的表。ShardingSphere 将这类表的数据复制到所有数据源中。
rules:
- !SHARDING
broadcastTables:
- t_config # 系统配置表
- t_dict # 字典表
适用:字典表、配置表、省市区表——不用 JOIN 跨库也能查到完整数据。
4.2 绑定表
定义:存在关联关系的主表和从表,ShardingSphere 确保它们的数据落在一个分片上,避免跨库 JOIN。
rules:
- !SHARDING
bindingTables:
- t_order, t_order_item
工作原理:t_order 和 t_order_item 都按 order_id 分片,ShardingSphere 保证 order_id=100 的订单和订单明细落在同一个数据库分片上。查询 SELECT * FROM t_order o JOIN t_order_item i ON o.id=i.order_id 时无需跨库。
⚠️ 5.x 语法变更:4.x 的
shardingTables和bindingTables独立存在,5.x 改为 YAML tag 语法——!BROADCAST和!BINDING。升级时注意适配。
五、分布式 ID 与分片算法
5.1 雪花算法(Snowflake)
// ShardingSphere 内置雪花算法
spring:
shardingsphere:
rules:
sharding:
keyGenerators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 1
组成:1 位符号位 + 41 位时间戳 + 10 位工作机器 ID + 12 位序列号。每毫秒可生成 4096 个有序 ID,趋势递增不依赖数据库。
5.2 分片算法选择
| 算法 | 原理 | 适用 | 优缺点 |
|---|---|---|---|
| INLINE | ds$->{user_id % 2} | 简单取模 | 配置简单,但扩容时所有数据需重分布 |
| MOD | 取模分片 | 均匀分布 | 同上 |
| HASH_MOD | 一致性 Hash | 动态扩缩容 | 扩容时仅部分数据迁移 |
| RANGE | 按范围分片 | 按时间/ID范围 | 可能导致数据倾斜 |
一致性 Hash 的核心价值:新增一个节点时,只有该节点"虚拟环"上相邻节点的部分数据需要迁移,而非全部数据重分布。这是动态扩容场景下的首选。
六、分布式事务
ShardingSphere 集成 Seata 实现分布式事务:
rules:
- !SHARDING
...
- !TRANSACTION
defaultType: BASE
providerType: Seata
Seata AT 模式:一阶段执行 SQL → 二阶段提交(删除 undo_log)/ 回滚(通过 undo_log 反向补偿)。核心理解:AT 模式的回滚不是"撤销 SQL",而是"执行反向 SQL 补偿"——INSERT 的回滚是 DELETE,UPDATE 的回滚是反向 UPDATE。
选择建议:
undo_log表必须和业务表在同一个数据库,Seata Server 生产必须集群部署(否则单点故障导致全局事务不可用)。
七、分库分表的代价
| 代价 | 说明 | 缓解方案 |
|---|---|---|
| 跨分片查询 | 不带分片键的查询需广播到所有分片 | 强制查询带分片键,或用 ES 做搜索层 |
| 跨分片 JOIN | GROUP BY / ORDER BY 需在内存归并 | 绑定表 + 应用层聚合 |
| 分布式事务 | 跨库事务一致性复杂 | Seata AT/TCC + 事务消息 |
| 扩容困难 | 新增节点需要数据迁移 | 一致性 Hash + 平滑迁移工具 |
| ID 全局唯一 | 数据库自增 ID 不可用 | 雪花算法 / 美团 Leaf |
核心要点回顾
垂直分片与水平分片是两把不同尺寸的刀:垂直分片拆分不同的表到不同库(按业务模块),解决的是"不同业务隔离"的问题;水平分片拆分同一张表的数据行到多个库/表(按分片键),解决的是"单表数据量过大"的问题。真实项目通常是两者叠加——先按业务垂直分库,再对核心表水平分表。
ShardingSphere 5.x 在 4.x 基础上调整了 YAML 语法——广播表用 !BROADCAST tag、绑定表用 !BINDING tag,分片算法从 type: INLINE 配置。广播表将小表数据复制到所有分片,绑定表保证关联表数据落在同一分片避免跨库 JOIN。
分片键是水平分片最关键的决策——选择最高频查询的 WHERE 条件作为分片键,用时间做分片键是经典反模式(写入热点集中在一个分片上)。一致性 Hash 是支持动态扩容的最佳分片算法。
分库分表最难的从来不是技术实现——是分片键选型和扩容方案。收藏这份分片决策指南,表快到瓶颈时翻出来对照。
上一篇:《设计模式》 | 下一篇:《微服务治理六大支柱》 系列合集:掘金Java合集