分库分表是单机数据库容量触顶后的必经之路,但"怎么分"只是开始——真正复杂的是分完之后:请求怎么路由到正确的库表、SQL 怎么写才能分片、跨分片查询怎么聚合、数据迁移与扩容怎么做。如果这些问题全部靠业务代码硬扛,系统会快速腐烂。分库分表中间件(ShardingSphere、MyCat、Vitess 等)把这些横切能力收拢到统一层:要么以 Proxy 形态(服务端代理,业务无感),要么以 JDBC 形态(客户端直连,性能更好)。本指南以 Apache ShardingSphere 为主线,深度拆解分片路由、SQL 改写、结果合并、分布式事务、数据迁移与弹性伸缩,并给出中间件选型与生产实践。
一、分库分表的本质与中间件的价值
1.1 为什么要分片
单库瓶颈:
· 存储容量上限
· 单实例 QPS 上限
· 单库连接数上限
· 单表数据量影响索引与查询
分片两种维度:
· 分库:按业务/租户把库拆分到不同实例
· 分表:单表拆成多张同构子表(orders_0001 ... orders_0016)
分片后的挑战(中间件解决的):
· 路由:SQL 该去哪个库表
· 聚合:跨片 JOIN / 排序 / 分页 / 去重
· 分布式 ID、分布式事务、扩容迁移
· 业务代码复杂度失控
ℹ️ 核心价值:中间件把"分片逻辑"从业务代码中剥离——业务仍然写 orders 表名,中间件把它翻译成对 orders_0003 的访问。业务代码不变,分片对应用透明。
1.2 中间件两种形态
| 形态 | 代表 | 原理 | 优劣势 |
|---|
| JDBC(客户端) | ShardingSphere-JDBC | 重写 JDBC 驱动,库内分片 | 性能高、无网络跳数;侵入应用依赖 |
| Proxy(服务端) | ShardingSphere-Proxy / MyCat | 独立代理,协议层转发 | 应用无感、跨语言;多一跳网络 |
选型:
· 应用可改依赖 + 追求性能 → JDBC
· 多语言/不想动应用 → Proxy
· 团队小、想托管 → 云厂商 Proxy / Vitess
二、分片路由原理
2.1 分片键与分片算法
分片键(Sharding Key):
· 路由的依据,必须在 WHERE 中高效命中
· 选高基数列(user_id / order_id / tenant_id)
分片算法:
· 哈希取模:shard = hash(shard_key) % N
优点:数据均匀;缺点:扩容要重新分布
· 范围分片:按 id/时间 分段(如按月)
优点:扩容友好(加段即可);缺点:热点(最新分区热)
· 一致性哈希:减少扩容时的迁移量
· 复合/自定义:业务规则
2.2 ShardingSphere 路由配置
# shardingsphere.yaml — 逻辑表 orders 分片到 4 库 × 4 表
rules:
- !SHARDING
tables:
orders:
actualDataNodes: ds_${0..3}.orders_${0..3}
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: orders-hash
keyGenerateStrategy:
column: id
keyGeneratorName: snowflake
shardingAlgorithms:
orders-hash:
type: HASH_MOD
props:
sharding-count: 4
keyGenerators:
snowflake:
type: SNOWFLAKE
2.3 路由执行过程
一条 INSERT/查询如何路由:
1. 解析 SQL → 识别分片键与值(order_id=12345)
2. 计算目标数据节点(hash(12345) % 4 → ds_1.orders_1)
3. 改写 SQL(orders → orders_1, 加路由条件)
4. 下发执行 → 结果回传合并
无分片键的查询(全路由):
· SELECT * FROM orders WHERE user_id=1(无 order_id)
→ 广播到所有分片执行 → 结果合并
→ 性能代价高,需靠"索引旁路表/汇总表"规避
三、SQL 改写与结果合并
3.1 改写类型
改写动作:
· 逻辑表名 → 物理表名(orders → orders_1)
· 追加分片条件(隐式路由)
· 分页改写(LIMIT 跨片需重算)
· 排序/聚合改写(ORDER BY / COUNT / SUM)
· 去重改写(DISTINCT / UNION)
3.2 分页与排序的正确姿势
-- 跨分片分页(错误示范):
SELECT * FROM orders ORDER BY created_at LIMIT 10, 20;
-- 中间件需把每片 LIMIT 扩成 0,30 再归并重排序
-- 深分页(LIMIT 100000,20)各片取 100020 行,性能灾难
-- 正确姿势:
-- 1. 用"上一页最后一条"游标翻页(WHERE id < last_id ORDER BY id LIMIT 20)
-- 2. 避免深分页
-- 3. 分片键排序(同一分片内排序可下推)
3.3 结果合并策略
合并场景:
· 简单聚合:COUNT/SUM 各片求和
· 分组聚合:GROUP BY → 各片聚合 → 归并重聚合
· 排序:多路归并
· 去重:全局 set
· AVG:需 SUM/COUNT 联合重算(不能直接平均各片均值)
正确性注意:
· 分页 offset 需从各片"截取后合并再偏移"
· GROUP BY + ORDER BY 需在归并层再排序
四、分布式事务与全局治理
4.1 跨分片事务
分片后,一条业务请求可能跨多个分片(数据在不同库):
· 本地事务失效(XA / TCC / SAGA / Outbox)
ShardingSphere 支持:
· 本地事务(单分片)
· XA(两阶段,强一致,性能开销大)
· BASE(Seata AT / TCC / SAGA,最终一致)
# 开启 BASE 事务(Seata)
# 应用侧 @GlobalTransactional 注解
@GlobalTransactional
public void transferMoney() {
// 跨分片扣减/增加,Seata 保证最终一致
}
4.2 分布式 ID
分片键需要全局唯一且可路由:
· Snowflake:时间戳 + 机器 + 序列(趋势递增,可路由)
· Leaf(美团):号段模式 / Snowflake
· UUID:唯一但无序(不适合做分片键)
ShardingSphere 内置 Snowflake 生成器(见前 yaml keyGeneratorName)
4.3 全局索引与旁路表
跨片查询优化:
· 全局表(广播表):维度表全片复制(status_dict 等)
· 绑定表:JOIN 的表保持同分片(order + order_item 同 order_id)
· 索引旁路:按 user_id 查 order_id 的反查表
# 绑定表(JOIN 下推到同分片)
rules:
- !SHARDING
bindingTables:
- orders, order_items
broadcastTables:
- status_dict
五、ShardingSphere 实践配置
5.1 JDBC 集成(Spring Boot)
# application.yml — ShardingSphere-JDBC
spring:
shardingsphere:
datasource:
names: ds0, ds1, ds2, ds3
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://mysql-0:3306/orders?useSSL=false
username: app
password: ***
# ds1-ds3 同理...
rules:
sharding:
tables:
orders:
actual-data-nodes: ds$->{0..3}.orders_$->{0..3}
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: orders-hash
sharding-algorithms:
orders-hash:
type: HASH_MOD
props:
sharding-count: 4
5.2 Proxy 部署(跨语言无感)
# ShardingSphere-Proxy 独立进程,兼容 MySQL 协议
# 应用改连接地址指向 Proxy(3307 端口),业务零改动
config:
schema:
logic_db:
dataSources:
ds0:
url: jdbc:mysql://mysql-0:3306/orders
# ...
rules:
- !SHARDING
tables:
orders:
actualDataNodes: ds${0..3}.orders${0..3}
应用只需:
jdbc:mysql://sharding-proxy:3307/logic_db
→ 连接 Proxy,SQL 透传,Proxy 完成分片
六、数据迁移与弹性伸缩
6.1 数据迁移场景
扩容三阶段:
1. 存量迁移(历史数据重分布)
2. 增量同步(binlog 持续追)
3. 校验与切换(流量灰度到新拓扑)
ShardingSphere ElasticJob / Data Migration:
· 按分片并发迁移
· binlog 增量追平
· 一致性校验(数据对账)
6.2 扩容方案对比
| 方案 | 思路 | 优点 | 缺点 |
|---|
| 翻倍扩容 | 2N 倍(重新哈希映射) | 数据可均匀重放 | 全量迁移成本 |
| 一致性哈希 | 环上加节点,局部迁移 | 迁移量少 | 数据略不均 |
| 范围+热度 | 按范围预分区 + 热点拆分 | 平滑 | 需业务配合 |
| 双写过渡 | 新旧并存写、灰度读 | 低风险 | 双写复杂度 |
6.3 扩容最小化原则
· 预留分片余量(初期 8 片,未来扩 16)
· 分片键稳定(一旦定了别改)
· 优先"加库不拆表"(表内自增保留)
· 扩容在低峰 + 限速 + 可回滚
七、中间件对比与选型
| 方案 | 形态 | 生态 | 适用 |
|---|
| ShardingSphere | JDBC + Proxy | 全(分片/治理/迁移) | 大多数企业,首选 |
| MyCat | Proxy | 老牌,水平拆分 | 老项目迁移 / 简单分片 |
| Vitess | Proxy | 云原生,大厂级 | 超大规模,K8s 原生 |
| 云原生(PolarDB-X/TiDB) | 原生分布式 | 全托管 | 从零建设,替代分片 |
选型要点:
· 团队熟悉度 + 维护成本
· 是否需要分布式事务(Seata 生态)
· 是否 K8s 原生部署
· 超大规模 → Vitess / 原生分布式
· 常规规模 → ShardingSphere
八、中间件带来的新问题
8.1 运维与排查复杂度
· SQL 链路变长(应用→中间件→多库)
· 慢查询定位需聚合多库执行日志
· 监控需覆盖:中间件路由指标、各分片负载
· 故障域扩大(一个分片挂了影响整体)
8.2 常见反模式
| 反模式 | 危害 | 对策 |
|---|
| 无分片键全路由 | 扫描所有分片 | 索引旁路表 |
| 深分页 | 每片取海量行 | 游标分页 |
| 跨片 JOIN 太多 | 中间件内存聚合 | 绑定表/冗余/应用聚合 |
| 分片键用时间 | 写热点 | 哈希/租户维度 |
| 不分优先拆表 | 复杂度先失控 | 先分库,再分表 |
| 每张表都拆 | 中间件负担 + 管理难 | 只拆热点大表 |
8.3 演进:分片 vs 分布式数据库
当"分片治理"成本逼近"自建分布式"时,可考虑:
· TiDB / OceanBase / PolarDB-X:原生分布式,无需分片中间件
· 保留业务写"单表"的心智,底层自动分布
→ 中间件是"存量改造"方案,分布式库是"新架构"方案
九、生产落地清单
9.1 分片设计 Checklist
□ 分片键选择:高基数、稳定、查询高频(user_id/tenant_id)
□ 分片数量:当前 8-16 倍预留(含未来 3 年增长)
□ 分片算法:哈希取模(均匀)或 范围(扩容友好)
□ 绑定表 / 广播表 规划
□ 分布式 ID:Snowflake 类(可路由、趋势递增)
□ 全局查询规避:旁路表/汇总表
□ 分布式事务方案:XA / Seata
□ 监控与慢查询聚合能力
□ 扩容演练预案
9.2 上线监控指标
# 中间件路由指标
shardingsphere_sql_total # SQL 总量
shardingsphere_route_success # 路由成功
shardingsphere_execute_latency # 执行延迟
# 分片负载分布(每库 QPS/连接数)
# 慢 SQL:中间件慢日志 + 各库慢查询日志
# 全路由 SQL 占比(应趋近 0)
9.3 常见坑速查
| 坑 | 表现 | 对策 |
|---|
| 分片键没进 WHERE | 全路由 | 强制带分片键 / 旁路表 |
| 跨片分页慢 | 深分页 | 游标翻页 |
| 扩容后数据不均 | 热点库 | 一致性哈希 / 翻倍重放 |
| 分布式事务超时 | 业务挂起 | 降级最终一致 |
| 某分片故障 | 部分功能不可用 | 分片容错 + 故障隔离设计 |
总结:分库分表中间件决策表
| 环节 | 关键动作 |
|---|
| 形态 | JDBC(性能)/ Proxy(无感) |
| 路由 | 分片键 + 哈希/范围算法 |
| 改写合并 | 表名改写、分页/排序/聚合归并 |
| 治理 | 绑定表、广播表、分布式 ID |
| 事务 | 本地 / XA / Seata BASE |
| 扩容 | 翻倍 / 一致性哈希 / 双写过渡 |
| 演进 | 存量中间件 → 新架构原生分布式 |
分库分表中间件把"分片"从业务的地狱变成了工程的常规——业务照常写单表 SQL,中间件完成路由、改写、合并与治理。但中间件不是银弹:分片键设计、跨片查询规避、扩容预案才是决定成败的三板斧。落地时记住:分片键选高基数稳定列、绑定表保证 JOIN 同片、全局查询靠旁路表、扩容用翻倍 + 双写过渡。当分片治理的复杂度逼近自建分布式数据库的成本时,就该考虑 TiDB 这类原生方案——中间件解决"现在",分布式数据库面向"未来"。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。