分库分表模式(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 分片,因为大部分查询都是「查某个用户的订单」。
  • 均匀分布:数据要散得开。别用性别这种只有两个值的字段做分片键,否则就两个库忙死,其他库闲死。
  • 不可变:分片键一旦确定,尽量不要改。改分片键意味着数据要迁移,那是一场噩梦。
避坑指南:我曾经在一个项目中用了时间戳作为分片键,结果每个月的数据都往新表里写,旧表几乎没查询。这就导致了「热点问题」——新表忙到爆,旧表闲得慌。后来改成了 user_id 取模,才解决了。

常见路由策略

路由策略就是「怎么算出来数据在哪张表」。我常用的有这几种:

  1. 取模路由:shard_id = user_id % 16。简单粗暴,适合数据均匀的场景。
  2. 范围路由:按时间范围或 ID 范围划分。比如 1-1000 万在表1,1001-2000 万在表2。适合有明确区间查询的场景。
  3. 哈希路由:对分片键做哈希,再取模。比直接取模更均匀,但计算成本略高。
  4. 一致性哈希:解决扩容时数据迁移量大的问题。每次加节点只影响相邻节点,迁移量只有 1/n。

我个人习惯用取模路由,简单可靠。除非你预估未来会频繁扩容,否则一致性哈希带来的复杂度不值得。

分库分表带来的挑战

嗯,这里要泼点冷水。分库分表不是银弹,它引入的问题可能比解决的问题还多。

  • 跨库查询:不能 join 了,得在应用层做聚合。代码复杂度直线上升。
  • 分布式事务:跨库的事务一致性很难保证。TCC、Saga 这些模式,写起来很痛苦。
  • 全局主键:自增 ID 不能用了。得用雪花算法、UUID 或者发号器。
  • 数据迁移:扩缩容时,数据要重新分布。这个操作必须在线完成,非常考验架构能力。
我的建议:能不拆就不拆。先做读写分离、加缓存、优化索引。实在扛不住了,再考虑分片。很多项目其实连单库 5000 万都跑不满,根本不需要分库分表。

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

总结一下

分库分表这个模式,说白了就是「用复杂度换容量和性能」。垂直分片适合解耦,水平分片适合扩容。选好分片键,定好路由策略,再用 ShardingSphere 这样的工具落地,基本就能搞定大部分场景。

但我要再强调一遍:分库分表是最后的手段。先看看能不能用缓存、读写分离、冷热分离来解决。如果非拆不可,那就做好数据迁移和分布式事务的预案。我曾经见过一个项目,拆了 32 个库,结果一个简单的分页查询要查 32 次再合并,比不拆还慢。嗯,这就是典型的「过度设计」。

好了,今天的内容就到这里。记住:架构没有银弹,只有权衡。

公众号:蓝海资料掘金营,微信deep3321