分库分表方案

举报
yd_94907100 发表于 2026/11/03 14:23:02 2026/11/03
【摘要】 从拆分时机讲到分片键选择、路由算法与扩容,附 ShardingSphere 配置示例

单表超过 2000 万行就该分表——这个说法流传很广,但真正的问题不是行数,而是"查询能否稳定在索引可控的范围内"。分库分表是一次架构级手术,代价是跨库 JOIN、分布式事务、扩容、运维复杂度全面上升。这篇文章讲清楚什么时候该拆、怎么选分片键、ShardingSphere 如何落地,以及扩容时的数据迁移。面向已有一定数据量压力的后端开发与架构设计者。

一、什么时候该拆

先排除"不该拆"的情况。下面这些手段优先级都高于分库分表:

  1. 优化索引与 SQL:多数慢查询是索引问题,不是数据量问题。
  2. 归档历史数据:订单、日志类表按时间归档到冷库,主表只留热数据。
  3. 读写分离:读多写少的场景,加从库往往能撑很久。
  4. 缓存:热点数据前置到 Redis。
  5. 升级硬件:SSD、更大内存,单位成本常常低于改造。

判断是否真的要拆,看两个信号:

  • 单表数据量超过 5000 万行,且索引优化后仍有大量慢查询。
  • 单库的 QPS/TPS 或磁盘 IO 达到实例上限,垂直扩容不划算。

行数本身不是标准。一张只有主键查询的表,1 亿行也能保持毫秒级;一张有 10 个二级索引、频繁范围查询的表,500 万行就可能撑不住。

二、垂直拆分与水平拆分

垂直分库:按业务拆成独立库,如用户库、订单库、商品库。本质是微服务的数据库隔离,风险低、收益明确,应优先做。

垂直分表:把大字段(TEXT、BLOB)或低频列拆到扩展表。减少主表行宽,提高页内行数,间接提升 Buffer Pool 效率。

水平分表:同一张表按分片键拆成多张结构相同的表。这是通常说的"分表"。

水平分库:在水平分表基础上,把表分散到多个实例,突破单机 IO 与连接数上限。

实践中常见组合是"先垂直分库,再对订单类大表做水平分库分表"。

三、分片键的选择

分片键决定了路由,也决定了哪些查询会变成"广播查询"。选错分片键,后续几乎无法补救。

选择原则:

  1. 高基数:值要足够分散,避免数据倾斜。status、type 这类只有几个值的列不能用。
  2. 查询必带:绝大多数查询的 WHERE 条件里都有它。否则每次查询都要扫所有分片。
  3. 分布均匀:避免热点。用 user_id 取模通常均匀;用 created_at 按月分片,当前月一定是热点。
  4. 不可变:分片键一旦确定,修改意味着数据迁移。不要用可能变化的字段。

常见选择:

| 业务 | 分片键 | 说明 |

| --- | --- | --- |

| 订单 | user_id | 用户维度查询为主;商家维度需异构索引表 |

| 用户 | user_id | 天然均匀 |

| 日志 | created_at + 哈希 | 按时间范围查询友好 |

| 消息 | conversation_id | 会话维度聚合 |

多维度查询的解法:订单既要按用户查、又要按商家查。方案是主表按 user_id 分片,另建一张按 merchant_id 分片的索引表(只存 order_id 与定位信息),写入时双写。

四、路由算法

取模:shard = hash(key) % N。分布均匀,但扩容时 N 变化会导致几乎所有数据重新分布。

范围:按 ID 区间或时间区间。扩容方便(加新分片即可),但容易热点。

一致性哈希:解决扩容时的数据迁移量,但实现复杂,且在节点少时分布不均。

双倍扩容 + 基因法:先按 2 的幂扩容(2 → 4 → 8),每次只迁移一半数据。基因法把分片基因编码进 ID 低位,使 order_id 自带分片信息,避免二次路由。

// 基因法示意:user_id 决定分片,order_id 的低位携带同一基因
// 取 user_id 的低 4 位作为基因
long gene = userId & 0b1111;
long orderId = (snowflakeId << 4) | gene;
// 之后用 order_id 也能直接算出分片
int shard = (int) (orderId & 0b1111);

代价是 ID 变长、雪花算法需要改造,但换来"用订单号直接定位分片"的能力。

五、ShardingSphere 落地

以 ShardingSphere-JDBC 5.4 为例,Spring Boot 3.x + MySQL 8.0。

依赖:

<dependency>
  <groupId>org.apache.shardingsphere</groupId>
  <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
  <version>5.4.1</version>
</dependency>

配置(按 user_id 分 2 库 × 2 表):

spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db0?useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: root
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db1?useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: root
    rules:
      sharding:
        tables:
          t_order:
            actual-data-nodes: ds$->{0..1}.t_order_$->{0..1}
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: db-inline
            table-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: table-inline
        sharding-algorithms:
          db-inline:
            type: INLINE
            props:
              algorithm-expression: ds$->{user_id % 2}
          table-inline:
            type: INLINE
            props:
              algorithm-expression: t_order_$->{user_id % 2}
        props:
          sql-show: true

初始化分片表(两个库各建两张):

CREATE DATABASE IF NOT EXISTS order_db0 DEFAULT CHARACTER SET utf8mb4;
CREATE DATABASE IF NOT EXISTS order_db1 DEFAULT CHARACTER SET utf8mb4;

CREATE TABLE IF NOT EXISTS order_db0.t_order_0 (
  order_id   BIGINT UNSIGNED NOT NULL,
  user_id    BIGINT UNSIGNED NOT NULL,
  amount     DECIMAL(12,2)   NOT NULL,
  status     TINYINT         NOT NULL DEFAULT 0,
  created_at DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (order_id),
  KEY idx_user (user_id)
) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4;

CREATE TABLE IF NOT EXISTS order_db0.t_order_1 LIKE order_db0.t_order_0;
CREATE TABLE IF NOT EXISTS order_db1.t_order_0 LIKE order_db0.t_order_0;
CREATE TABLE IF NOT EXISTS order_db1.t_order_1 LIKE order_db0.t_order_0;

注意:分片表的主键不能用 AUTO_INCREMENT,否则每个分片各自自增会产生重复 ID。必须由应用生成分布式 ID(雪花算法、号段模式),或使用 ShardingSphere 内置的 SNOWFLAKE 主键生成器。

六、跨分片查询

分库分表后,以下几类操作会退化:

| 操作 | 后果 | 应对 |

| --- | --- | --- |

| 不带分片键的查询 | 广播到所有分片 | 建异构索引表、ES |

| 跨分片 JOIN | 内存归并,性能差 | 冗余字段、宽表、应用层拼装 |

| 跨分片 ORDER BY + LIMIT | 各分片取 N 条再归并 | 增大 N 或改用搜索索引 |

| 跨分片 COUNT/SUM | 各分片聚合再合并 | 计数表、定时任务 |

| 跨分片事务 | 需要分布式事务 | 本地消息表、Seata、尽量避免 |

绑定表(Binding Table):如果 t_order 和 t_order_item 都按 user_id 分片,且路由算法一致,ShardingSphere 可以把它们当作绑定表,JOIN 时只在同一分片内进行,避免跨库。

        binding-tables:
          - t_order,t_order_item

广播表:字典表、配置表数据量小、变更少,可在每个库都存一份,JOIN 时无需跨库。

        broadcast-tables:
          - t_dict

七、分布式事务

跨分片写操作没有单机事务保证。选型顺序:

  1. 避免跨分片事务:把同一业务实体的数据放在同一分片,是最彻底的解法。
  2. 本地消息表 + 最终一致:业务表与消息表同库,靠定时任务投递,实现简单、可靠。
  3. Seata AT 模式:对业务侵入小,但依赖全局锁,高并发下有争用。
  4. TCC:性能好,但要为每个操作写 try/confirm/cancel,开发成本高。

对订单这类核心链路,推荐"本地消息表 + 幂等消费",而不是强一致的分布式事务。

八、扩容与数据迁移

从 2 库扩到 4 库,取模算法从 % 2 变成 % 4,需要迁移一半数据。双写方案步骤:

  1. 新老库同时写入(双写),读仍走老库。
  2. 用 pt-archiver 或自研工具做存量数据迁移,校验行数与校验和。
  3. 开启新旧数据比对,确认一致。
  4. 灰度切读,按 user_id 白名单逐步放大。
  5. 全量切读,停止双写,下线老库。
# pt-archiver 迁移示例:按主键分批搬运
pt-archiver \
  --source h=127.0.0.1,D=order_db0,t=t_order_0 \
  --dest   h=127.0.0.1,D=order_db2,t=t_order_0 \
  --where "1=1" \
  --limit 1000 \
  --commit-each \
  --purge

--purge 会删除源数据,首次迁移务必先去掉该参数做演练。

九、常见坑 / 最佳实践

  1. 不要在数据量还没到瓶颈时拆。分库分表带来的复杂度是永久的,收益是临时的。先用归档、读写分离、缓存把问题压到极限,再考虑拆。
  2. 分片键必须覆盖 90% 以上的查询。如果大量查询不带分片键,等于每次全分片扫描,比单表还慢。选型前先统计线上 SQL 的 WHERE 分布。
  3. 分片主键不能用自增。用雪花算法或号段模式,且 ID 生成器要保证时钟回拨安全。
  4. 扩容方案在第一天就要设计好。取模扩容需要全量迁移,双倍扩容只需迁一半;如果预计会持续增长,直接用 2 的幂分片数并预留扩容路径。
  5. 跨分片分页要限制深度。LIMIT 10000, 20 会在每个分片都取 10020 条再归并,深度分页在分片场景下代价是分片数的倍数,必须限制或改用游标。

十、小结

  • 分库分表是最后手段,优先用索引优化、归档、读写分离、缓存解决。
  • 分片键要满足高基数、查询必带、分布均匀、不可变四个条件。
  • 取模扩容代价高,双倍扩容 + 基因法能显著降低迁移量。
  • 跨分片 JOIN、排序、聚合、事务都会退化,用绑定表、广播表、冗余字段、本地消息表应对。
  • 扩容必须走"双写 → 迁移 → 校验 → 灰度切读 → 下线"的完整流程,不能一步切换。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。