当单表数据突破千万、QPS 逼近数据库连接上限,分库分表就成了绕不开的架构升级。本文用 ShardingSphere 实战,拆解从垂直拆分到水平分片的全流程:如何选分片键、配路由策略、避开跨分片查询与分布式事务的坑,给出可落地的配置与上线路线。
一、单库单表的瓶颈在哪里
当一张 MySQL 表的数据量涨到千万甚至上亿,即使你给MySQL 做了深度调优、给热点列建好了B+树索引,性能还是会陡降。根因不在单条 SQL,而在单机资源天花板:连接数被撑满、磁盘 IO 打满、热点行的行锁互相阻塞、一张大表的备份恢复就要几小时。此时加只读副本做主从复制与读写分离能缓解读压力,但写压力和无处安放的数据量依然卡在单库。分库分表就是把数据按规则拆到多个库、多张表,用水平扩展换吞吐。行业里常见的经验阈值是单表行数超过两千万、或单表容量超过 50GB 时就要启动分片评估,但这只是参考,真实拐点取决于你的写入压力与硬件规格。
二、垂直拆分与水平拆分
垂直拆分按”业务边界”或”字段冷热”拆:把用户库、订单库分到不同实例,或者把大宽表里不常用的长文本字段剥到扩展表。它解决的是”单库表太多、单表字段太宽”的问题,但每张表的数据行数没变。水平拆分才是真正解决数据量爆炸的方案:同一张逻辑表,按行分散到 N 个物理库、M 张物理表。比如 order_db0 与 order_db1 两个库,每个库里 t_order_0~t_order_3 四张表,逻辑上仍是一张 t_order。水平拆分后单表数据量可控、索引树高度下降、备份恢复也能并行。
三、水平分片的路由策略
分片的核心是”拿到分片键,算出落哪个库哪张表”。常见策略各有取舍:
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 取模分片(MOD) | 数据均匀、实现极简 | 扩容需迁移大量数据 | 数据量平稳、分片数固定 |
| 范围分片(RANGE) | 扩容方便、冷热易分离 | 易产生热点,新数据压最新分片 | 按时间/ID 递增的日志、订单 |
| 哈希分片 | 分布更均匀、抗热点 | 扩容仍需全量重分布 | 通用高并发读写 |
| 一致性哈希 | 扩容仅影响相邻节点、迁移量最小 | 实现复杂、需虚拟节点防倾斜 | 节点频繁变动的缓存/分片集群 |
四、ShardingSphere-JDBC 配置实战
ShardingSphere 是 Apache 顶级项目,其中 ShardingSphere-JDBC 以数据源代理方式嵌入应用,对业务代码零侵入。下面是一份按 user_id 分库、order_id 分表的 YAML 配置:
spring:
shardingsphere:
datasource:
names: ds0,ds1
ds0:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db0
username: app
password: ${DB_PWD}
ds1:
type: com.zaxxer.hikari.HikariDataSource
jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db1
username: app
password: ${DB_PWD}
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds$>{0..1}.t_order_$>{0..3}
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: db-mod
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: tbl-mod
sharding-algorithms:
db-mod:
type: MOD
props:
sharding-count: 2
tbl-mod:
type: MOD
props:
sharding-count: 4
绑定表与广播表:避免笛卡尔积
跨分片 JOIN 是分库分表最大的坑。订单表 t_order 与订单明细 t_order_item 都按 order_id 分片,若不声明绑定关系,一次 JOIN 会退化成”所有分片两两组合”的笛卡尔积,性能雪崩。用 binding-tables 把它们绑在一起,引擎就知道同一 order_id 的两张表落在同一分片,直接本地 JOIN。而字典表、配置表这类所有分片都要用的小表,配成 broadcast-tables 全量同步到每个库,避免跨库查询。
spring:
shardingsphere:
rules:
sharding:
binding-tables:
- t_order,t_order_item
broadcast-tables:
- t_dict
五、分片键怎么选(决定生死)
分片键选错,分库分表反而更慢。原则:选基数高、且查询必带的字段。以订单为例,用 order_id 当分片键,用户想查”我的订单”时只能带上 user_id——它不在分片键里,引擎被迫把 SQL 广播到全部 8 个分片再聚合,原本一次命中变八次。正确做法是优先用 user_id 分库,让”查某用户订单”只路由到一个库;order_id 只作表内分片键辅助打散。
-- 反例:WHERE 带 user_id 但分片键是 order_id,必须广播到所有分片
SELECT * FROM t_order WHERE user_id = 10086;
-- 正解:查询带上分片键 user_id,只路由到 1 个库 1 张表
SELECT * FROM t_order_1 WHERE user_id = 10086 AND order_id = 778899;
六、跨分片查询与分布式事务的坑
不是所有查询都能带分片键。分页、聚合(COUNT/SUM)、跨用户报表必然跨分片,ShardingSphere 会把 SQL 改写后发到各分片、再归并结果,深分页(LIMIT 100000,10)会拖垮全分片,务必改成游标或业务游标分页。同理,不带分片键的 COUNT/SUM 也会被改写成 N 条 SQL 发到各分片、再在内存里归并,跨分片越多延迟越高,能前置过滤就别让它在全分片扫一遍。分布式事务更棘手:跨库更新要么引入 Seata 做最终一致,要么把强一致逻辑收敛到同一分片。高并发写入还要注意跨库死锁与连接池打满,单分片上的锁竞争依然可能发生。
七、上线演进路线(别一上来就水平分片)
- 先做垂直拆分,按业务划库,缓解单库表过多。
- 加只读副本,主从复制读写分离扛住读放大。
- 热点读叠加Redis 缓存设计,挡掉大部分重复查询。
- 真到单表上亿、写成瓶颈,再上 ShardingSphere 做水平分片。
顺序错了,过早分片只会增加运维复杂度却换不回收益。很多团队在索引缺失、慢 SQL 横行时就急着分库,结果复杂度翻倍、性能却没本质改善。
八、上线前自检清单
- 分片键基数高且查询必带,避免广播 SQL。
- 绑定表/广播表声明完整,杜绝笛卡尔积。
- 跨分片事务用 Seata 或收敛到单分片,不强求 2PC。
- 深分页改用游标,禁用 LIMIT 大偏移。
- 扩容方案提前演练:一致性哈希或双写迁移,别临时抱佛脚。
- 监控每个物理分片的连接数、慢查询与磁盘水位。
分库分表不是银弹,它用”运维复杂度”换”写入与存储的横向扩展能力”。动手前先确认瓶颈真在单库容量,而非索引缺失或慢 SQL——不少”需要分库”的诉求,其实靠参数调优和索引优化就能再撑很久。一旦确定要分,ShardingSphere 的声明式配置能让你以最小改造成本平滑落地。




