分库分表

返回 数据库基础

当单表数据量超过千万、单库连接数和 IO 达到瓶颈时,分库分表是常见的扩展方案。核心原则:能不拆就不拆,先做索引优化、缓存、读写分离,最后才考虑分库分表。


什么时候需要分库分表

问题阈值参考解决方向
单表数据量过大,查询慢MySQL 单表 > 2000 万行(InnoDB + 合理索引)水平分表
单库连接数达上限MySQL max_connections 通常 1000~4000分库
单库写入 TPS 过高单机 InnoDB 写入约 1000~5000 TPS分库
表字段过多,数据冷热分离单表 > 60 列,部分字段极少访问垂直分表
不同业务模块在同一库互相影响垂直分库

垂直拆分

垂直分库

业务模块拆分:订单库、用户库、商品库各自独立部署:

单体数据库
├── user 表       →  用户库(user_db)
├── order 表      →  订单库(order_db)
├── product 表    →  商品库(product_db)
└── payment 表    →  支付库(pay_db)

优点:各模块独立扩展、故障隔离
缺点:跨库 JOIN 消失,需应用层聚合;分布式事务复杂度上升

垂直分表

宽表按字段冷热拆分:常用字段 + 大字段分离:

-- 原始表(60+ 列)
users(id, name, email, password, avatar, bio, settings_json, ...)
 
-- 拆分后
users(id, name, email, password, status)          -- 热数据,频繁查询
users_profile(user_id, avatar, bio, settings_json)  -- 冷数据,按需加载

水平拆分

同一张表的数据按规则分散到多个物理表(分片):

order 表(10亿行)
├── order_0(0~2.5亿行)
├── order_1(2.5~5亿行)
├── order_2(5~7.5亿行)
└── order_3(7.5~10亿行)

分片键(Sharding Key)选择

分片键决定每行数据去哪个分片,选择不当后患无穷:

原则说明
选择基数大的列保证数据均匀分布(如 user_id,不选 status)
选择查询高频列查询时带分片键才能精确路由,否则散查所有分片
避免热点若按时间分片,新数据集中在最新分片,产生写热点
业务含义清晰订单表用 user_id 或 order_id,确保同一用户的订单在同一分片

分片策略

Hash 取模

shard_id = user_id % shard_count

user_id=100 → 100 % 4 = 0 → order_0
user_id=101 → 101 % 4 = 1 → order_1

优点:数据分布均匀
缺点:扩容时几乎所有数据需要迁移(user_id % 4user_id % 8

范围分片

order_id 1~10000000    → order_0
order_id 10000001~20000000 → order_1

优点:扩容容易(新增分片即可)
缺点:写入热点(新数据都在最新分片)

一致性哈希

将所有分片节点分布在哈希环上,数据按 key 的哈希值顺时针找最近节点:

     0
    / \
  N3   N0
  |     |
  N2   N1
    \ /
    180

优点:扩容只需迁移相邻分片的少量数据(1/n)
缺点:实现复杂,需虚拟节点防止数据倾斜

分库 + 分表(常见组合)

16 个分库 × 每库 32 张分表 = 512 张分表

路由公式:
db_index   = user_id % 16
table_index = (user_id / 16) % 32

跨分片问题

分库分表带来一系列跨分片操作的复杂性:

跨分片查询 / JOIN

-- 原来:单库 JOIN
SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id WHERE o.status='paid';
 
-- 分库后:users 和 orders 在不同库,无法直接 JOIN
-- 解决方案:
-- 1. 冗余字段(将 user_name 冗余到 orders 表中)
-- 2. 应用层聚合(先查 users 再查 orders,内存中 JOIN)
-- 3. 全局表(维度小的表每个分库都复制一份,如地区、分类)

跨分片分页 / 排序

-- 分库后想按 created_at 排序取第 2 页
-- 必须查每个分片各取前 N 条,再在内存合并排序取第 2 页的 pageSize 条
-- → 越往后翻页,性能越差
 
-- 推荐方案:业务层避免深分页;或用游标 + 分片键保证查询只打一个分片

跨分片聚合

-- COUNT / SUM / AVG 需要每个分片各自计算后在应用层合并
-- AVG 不能简单相加(需要 SUM 和 COUNT 分别汇总再计算)

分布式事务

跨分片写入(如转账:A 分片扣款 + B 分片入账)需要分布式事务:
→ 参考 数据库事务 中的 TCC / SAGA 模式


全局唯一 ID

分库分表后,单库自增主键会重复,需要全局唯一 ID:

雪花算法(Snowflake)

64位 = 1位符号 + 41位时间戳(ms) + 10位机器ID + 12位序列号
  • 单机每毫秒可生成 4096 个 ID
  • 趋势递增,适合做分库分表的主键(B+ 树写入友好)
  • 时钟回拨问题:时间戳依赖系统时钟,时钟回调会生成重复 ID
// 常用实现:Hutool IdUtil
long id = IdUtil.getSnowflakeNextId();
 
// 或自定义(传入 datacenterId 和 workerId)
Snowflake snowflake = IdUtil.createSnowflake(1, 1);
long id = snowflake.nextId();

号段模式(Leaf-Segment)

从数据库批量取一段 ID(如每次取 1000 个),缓存在本地内存:

-- ID 分配表
CREATE TABLE id_alloc (
  biz_tag   VARCHAR(128) NOT NULL,
  max_id    BIGINT       NOT NULL,  -- 当前已分配到的最大 ID
  step      INT          NOT NULL,  -- 每次取的步长
  PRIMARY KEY (biz_tag)
);
 
-- 取号(CAS 更新)
UPDATE id_alloc SET max_id = max_id + step WHERE biz_tag = 'order';
SELECT max_id FROM id_alloc WHERE biz_tag = 'order';
-- 本地可用范围:[max_id - step + 1, max_id]

优点:ID 完全本地生成(号段用完前),性能高,不依赖时钟
实现:美团 Leaf、百度 UidGenerator

UUID

128 位随机字符串,全局唯一,但:

  • 无序,写入 InnoDB 聚簇索引时频繁页分裂,性能差
  • 存储占用大(36 字符 vs 8 字节 BIGINT)
  • 不推荐作为数据库主键,可用于幂等键

分库分表中间件

中间件形态特点
ShardingSphere-JDBC客户端 JAR代码内嵌,无额外网络跳转,性能好
ShardingSphere-Proxy独立代理进程,兼容 MySQL 协议应用无感知,支持异构语言
MyCat独立代理国内较早的分库分表中间件,功能全但社区活跃度下降
Vitess分布式 MySQL 集群YouTube 开源,K8s 友好,生产级

ShardingSphere-JDBC 配置示例

# application.yml
spring:
  shardingsphere:
    datasource:
      names: ds0, ds1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://db0:3306/order_db
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://db1:3306/order_db
    rules:
      sharding:
        tables:
          orders:
            actual-data-nodes: ds$->{0..1}.orders_$->{0..3}   # ds0.orders_0 ~ ds1.orders_3
            database-strategy:
              standard:
                sharding-column: user_id
                sharding-algorithm-name: db-inline
            table-strategy:
              standard:
                sharding-column: order_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: orders_$->{order_id % 4}
        key-generators:
          snowflake:
            type: SNOWFLAKE

迁移方案(存量数据迁移)

1. 双写阶段
   新数据同时写入老库(单表)和新库(分片)

2. 历史数据迁移
   DataX / 自研脚本将老库数据按分片键路由写入新库

3. 数据校验
   对比新旧库行数、checksum,确保一致

4. 切流
   读请求逐步切到新库(灰度放量)
   全量切流后,老库保留一段时间作为回滚方案

5. 停止双写,下线老库

相关