分库分表模式(Sharding):垂直分片、水平分片、分片键、路由策略
各位同学,今天我们来聊聊分库分表。说实话,这个模式在面试里被问烂了,但在实际项目中,真正把它用好的人并不多。我见过太多团队,一上来就搞 64 个库、1024 张表,结果查询慢得还不如单库。嗯,这里面的门道,咱们今天一次说透。
为什么需要分库分表?
先问个问题:单库单表到底能扛多大?
以 MySQL 为例,单表数据量超过 500 万行,索引深度超过 3 层,B+ 树的 IO 次数就开始明显增加。到了 2000 万行,很多查询已经需要 1 秒以上了。我有个项目,订单表到了 3000 万行,一个简单的 count(*) 跑了 8 秒——那感觉,就像在等一个永远不来的公交车。
说白了,分库分表就是为了解决两个问题:
- 容量瓶颈:单库磁盘有限,单表数据太多
- 性能瓶颈:连接数、IOPS、CPU 都扛不住了
垂直分片 vs 水平分片
这两个概念,我建议你从「切的方向」去理解。
垂直分片(Vertical Sharding)
垂直分片是按「列」来切。把一张宽表拆成多张窄表,或者把一个库拆成多个库,每个库放不同的业务模块。
举个例子:用户表原来有 30 个字段,我把头像、签名这种大文本字段拆到另一张表。这就是垂直分表。再比如,把用户库、订单库、商品库拆成三个独立的数据库,这就是垂直分库。
我在项目中遇到过最典型的场景:一个 CRM 系统,所有表都在一个库里,结果一次全表扫描把 IO 打满了,连登录都卡。后来把日志表单独拆到一个库,问题立刻缓解。说白了,垂直分片就是「把不相关的数据隔离开」。
水平分片(Horizontal Sharding)
水平分片是按「行」来切。把同一张表的数据,按照某个规则分散到多个结构相同的表中。
比如订单表,按用户 ID 取模,分到 16 张表里。每张表只存 1/16 的数据。这才是真正解决数据量大的手段。
我记得有个支付系统,日订单量 500 万,单表根本扛不住。水平分片后,每张表只有 300 万行,查询基本在 10ms 以内。效果立竿见影。
| 维度 | 垂直分片 | 水平分片 |
|---|---|---|
| 切分对象 | 列(字段) | 行(记录) |
| 解决痛点 | 表太宽、业务耦合 | 数据量太大 |
| 复杂度 | 较低 | 较高 |
| 典型场景 | 用户信息拆分、日志分离 | 订单、消息、流水 |
分片键与路由策略
分片键,就是用来决定数据去哪张表的那个字段。选对了,系统跑得飞起;选错了,天天加班修 Bug。
分片键的选择原则
- 高频查询条件:分片键必须是查询中最常用的字段。比如订单表按 user_id 分片,因为大部分查询都是「查某个用户的订单」。
- 均匀分布:数据要散得开。别用性别这种只有两个值的字段做分片键,否则就两个库忙死,其他库闲死。
- 不可变:分片键一旦确定,尽量不要改。改分片键意味着数据要迁移,那是一场噩梦。
常见路由策略
路由策略就是「怎么算出来数据在哪张表」。我常用的有这几种:
- 取模路由:shard_id = user_id % 16。简单粗暴,适合数据均匀的场景。
- 范围路由:按时间范围或 ID 范围划分。比如 1-1000 万在表1,1001-2000 万在表2。适合有明确区间查询的场景。
- 哈希路由:对分片键做哈希,再取模。比直接取模更均匀,但计算成本略高。
- 一致性哈希:解决扩容时数据迁移量大的问题。每次加节点只影响相邻节点,迁移量只有 1/n。
我个人习惯用取模路由,简单可靠。除非你预估未来会频繁扩容,否则一致性哈希带来的复杂度不值得。
分库分表带来的挑战
嗯,这里要泼点冷水。分库分表不是银弹,它引入的问题可能比解决的问题还多。
- 跨库查询:不能 join 了,得在应用层做聚合。代码复杂度直线上升。
- 分布式事务:跨库的事务一致性很难保证。TCC、Saga 这些模式,写起来很痛苦。
- 全局主键:自增 ID 不能用了。得用雪花算法、UUID 或者发号器。
- 数据迁移:扩缩容时,数据要重新分布。这个操作必须在线完成,非常考验架构能力。
ShardingSphere 实践
讲完了理论,咱们看看实际怎么落地。ShardingSphere 是目前 Java 生态里最成熟的分库分表中间件。它有两种模式:JDBC 模式和 Proxy 模式。
JDBC 模式是嵌入在应用里的,相当于一个增强版的 DataSource。Proxy 模式是一个独立的服务,应用通过 JDBC 连接它,它再连接真实的数据库。
我个人更推荐 JDBC 模式,因为少了一层网络开销,性能更好。但如果你需要多语言支持,或者不想改代码,那就用 Proxy。
核心配置示例
下面是一个水平分片的配置,按 user_id 取模分到 4 张表:
spring:
shardingsphere:
datasource:
names: ds0,ds1
ds0:
url: jdbc:mysql://localhost:3306/db0
username: root
password: 123456
ds1:
url: jdbc:mysql://localhost:3306/db1
username: root
password: 123456
sharding:
tables:
t_order:
actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}
table-strategy:
inline:
sharding-column: user_id
algorithm-expression: t_order_$->{user_id % 2}
database-strategy:
inline:
sharding-column: user_id
algorithm-expression: ds$->{user_id % 2}
这段配置的意思是:先按 user_id 取模决定去哪个库(ds0 或 ds1),再取模决定去哪个表(t_order_0 或 t_order_1)。总共 2 个库 × 2 张表 = 4 个分片。
你想想看,如果 user_id = 7,7 % 2 = 1,所以去 ds1;7 % 2 = 1,所以去 t_order_1。最终数据落在 ds1.t_order_1 这张表里。
分片键的绑定
ShardingSphere 还有一个很实用的功能:绑定表。如果订单表和订单明细表都按 user_id 分片,可以配置成绑定表,这样 join 查询就不会跨库了。
spring:
shardingsphere:
sharding:
binding-tables:
- t_order, t_order_item
这个配置告诉 ShardingSphere:这两张表的分片规则一样,join 时直接在同一个分片内完成,避免笛卡尔积式的跨库查询。性能提升非常明显。
知识体系总览
下面这张图,我把分库分表的核心知识点串起来了。你可以把它当作一个思维导图来用:
总结一下
分库分表这个模式,说白了就是「用复杂度换容量和性能」。垂直分片适合解耦,水平分片适合扩容。选好分片键,定好路由策略,再用 ShardingSphere 这样的工具落地,基本就能搞定大部分场景。
但我要再强调一遍:分库分表是最后的手段。先看看能不能用缓存、读写分离、冷热分离来解决。如果非拆不可,那就做好数据迁移和分布式事务的预案。我曾经见过一个项目,拆了 32 个库,结果一个简单的分页查询要查 32 次再合并,比不拆还慢。嗯,这就是典型的「过度设计」。
好了,今天的内容就到这里。记住:架构没有银弹,只有权衡。