Skip to content

MySQL 分库分表 ​

分库分表不是“SQL 慢了就上”的万能药。它是当单库、单表、单机已经接近容量、写入、连接、IO 或运维边界时,用更复杂的架构换更大容量和吞吐的治理手段。

先记住一句话:

分库分表的本质是:把原来集中在一个库或一张表里的数据,按规则拆到多个物理库或物理表里;收益是容量和吞吐提升,代价是查询、事务、分页、唯一约束、扩容和运维都会变复杂。

学习目标 ​

学完本章你要能回答:

  1. 为什么分库分表不是普通慢 SQL 的第一选择。
  2. 垂直拆分、水平拆分、分库、分表分别解决什么问题。
  3. 分片键为什么决定后续查询和扩容成本。
  4. 按用户、按订单、按时间、按租户分片分别有什么取舍。
  5. 分库分表后为什么 Join、事务、唯一约束、分页排序会变难。
  6. 全局 ID、路由、广播表、绑定表、读写分离、数据迁移怎么设计。
  7. 商业订单系统和医疗数据采集平台应该如何落地。
  8. 面试中如何把“为什么这样做,不这样会怎样”讲清楚。

什么时候才考虑分库分表 ​

很多慢查询并不需要分库分表。先判断问题属于哪一类。

问题优先方案为什么不是先分库分表
SQL 没有合适索引先设计联合索引、改 SQL分表后每个分片仍然会慢
深分页慢改游标分页、缩小查询范围分片后还要跨分片归并,可能更慢
历史数据太多冷热归档、分区、历史库老数据很少访问,没必要增加全链路复杂度
读多写少加缓存、读写分离、优化报表分库会让读路径和一致性更复杂
单表写入极高评估水平分表写入热点可能需要拆散
单库连接、CPU、IO 到瓶颈评估分库单实例资源不够,需要横向扩展
单机容量不够分库分表、归档或分布式数据库需要明确长期容量模型

判断顺序:

mermaid
flowchart TD
    A["发现 MySQL 压力大"] --> B{"是单条 SQL 慢吗"}
    B -- "是" --> C["先看 EXPLAIN、索引、扫描行数"]
    B -- "否" --> D{"是历史数据太多吗"}
    D -- "是" --> E["先做归档、分区、冷热分离"]
    D -- "否" --> F{"单库资源是否到瓶颈"}
    F -- "否" --> G["继续优化 SQL、缓存、读写分离"]
    F -- "是" --> H{"是否有明确分片键和迁移方案"}
    H -- "否" --> I["先补齐分片键、ID、路由、迁移设计"]
    H -- "是" --> J["评估分库分表"]

如果没有明确分片键就分库分表,后续大概率会出现:高频查询跨分片、分页无法承受、事务补偿复杂、扩容要重做路由。

分库、分表、垂直拆分、水平拆分 ​

这几个词经常混在一起,先分清。

mermaid
flowchart TD
    A["拆分方式"] --> B["垂直拆分"]
    A --> C["水平拆分"]
    B --> D["按业务模块拆库"]
    B --> E["按字段冷热拆表"]
    C --> F["按行拆到多表"]
    C --> G["按行拆到多库"]
类型拆什么例子解决什么
垂直分库按业务域拆库用户库、订单库、支付库降低模块耦合,减少单库对象和连接压力
垂直分表按字段冷热拆表user 与 user_profile减少宽表,降低高频查询读页成本
水平分表同库多表按行拆orders_00 到 orders_15降低单表数据量、索引高度和写热点
水平分库多库按行拆order_db_00.orders_00分散连接、CPU、IO 和存储压力

垂直拆分 ​

业务域拆分:

text
user_db     用户、登录、地址
order_db    订单、订单明细
pay_db      支付单、支付流水
inventory_db 库存、库存流水

字段冷热拆分:

text
product
- id
- sku_no
- title
- status
- price
- updated_at

product_detail
- product_id
- description
- rich_text
- images
- long_attributes

为什么要垂直分表?因为宽字段会让每页能放的行变少。高频列表页只需要标题、价格、状态,却被大文本、图片 JSON、扩展字段拖累,Buffer Pool 会被低频大字段占用。

水平拆分 ​

水平拆分是把同一种数据按行分散:

text
order_db_00.orders_00
order_db_00.orders_01
order_db_01.orders_00
order_db_01.orders_01

适合数据持续增长、写入高、单表索引和单实例资源都接近瓶颈的场景。

分片键是最核心的设计 ​

分片键决定数据落在哪个分片,也决定查询能不能精准路由。

订单表常见分片键:

分片键优点缺点适合场景
user_id用户订单列表单分片查询按 order_no 查需要路由映射C 端用户订单列表高频
order_id写入较均匀,订单号直查方便用户订单列表可能跨分片内部系统按订单号处理更多
tenant_id租户隔离清晰大租户可能成为热点SaaS 多租户系统
created_at归档和冷热分离方便当前时间段热点明显日志、流水、历史归档
hospital_id医疗机构数据隔离自然大医院热点,跨院统计复杂医疗平台按机构运营

分片键选择原则:

  1. 高频查询必须尽量带分片键。
  2. 分片键的取值要尽量分散,避免热点。
  3. 分片键不能频繁变化。
  4. 分片键要能支撑扩容迁移。
  5. 强事务边界最好落在同一分片内。

反例:

text
订单按 created_at 分表,但用户订单列表按 user_id 查询。

这样每次查用户订单都要扫多个时间分片,再归并排序。短期看能跑,长期数据增长后会很难维护。

常见分片算法 ​

取模分片 ​

java
public final class OrderSharding {

    private static final int DB_COUNT = 4;
    private static final int TABLE_COUNT_PER_DB = 16;

    public static Shard routeByUserId(long userId) {
        int totalTableCount = DB_COUNT * TABLE_COUNT_PER_DB;
        long shard = Math.floorMod(userId, totalTableCount);
        int dbIndex = (int) (shard / TABLE_COUNT_PER_DB);
        int tableIndex = (int) (shard % TABLE_COUNT_PER_DB);
        return new Shard("order_db_" + String.format("%02d", dbIndex),
                "orders_" + String.format("%02d", tableIndex));
    }

    public record Shard(String database, String table) {
    }
}

优点:

  1. 简单直观。
  2. 数据通常比较均匀。
  3. 路由计算快。

缺点:

  1. 从 64 片扩到 128 片时,大量数据需要迁移。
  2. 老路由和新路由不兼容。
  3. 扩容期间要处理双写、回查和校验。

一致性哈希 ​

一致性哈希常用于节点扩缩容时降低迁移量,但在数据库分片中也不是银弹。原因是数据库分片不仅要路由,还要考虑范围查询、事务、迁移校验、连接池、DDL 和运维。

适合:节点扩容频繁、Key 访问比较独立的场景。

不适合:强依赖范围查询、跨分片统计、严格事务的核心订单表。

范围分片 ​

按月份分表:

text
collect_result_202601
collect_result_202602
collect_result_202603

适合:

  1. 日志、流水、采集结果。
  2. 数据天然按时间归档。
  3. 查询大多带时间范围。

问题:

  1. 当前月写入会集中,形成热点。
  2. 跨月查询要查多张表。
  3. 修改时间范围查询很容易遗漏分片。

字典映射分片 ​

用路由表记录业务 ID 到分片的映射:

sql
create table order_route (
  order_no varchar(64) primary key,
  user_id bigint not null,
  db_index int not null,
  table_index int not null,
  created_at datetime not null,
  unique key uk_user_order (user_id, order_no)
) engine = InnoDB default charset = utf8mb4;

适合:

  1. 查询入口既有 user_id,又有 order_no。
  2. 订单号本身无法直接计算分片。
  3. 需要兼容历史路由和迁移路由。

代价:

  1. 多一次路由表查询。
  2. 路由表也要高可用和缓存。
  3. 创建订单时要保证路由记录和订单记录一致。

分库分表后的 SQL 怎么执行 ​

分库分表前:

sql
select *
from orders
where user_id = 1001
order by created_at desc
limit 20;

如果按 user_id 分片,且 SQL 带 user_id,可以精准路由:

mermaid
flowchart TD
    A["SQL 带 user_id"] --> B["路由算法计算分片"]
    B --> C["order_db_02.orders_09"]
    C --> D["单分片执行 SQL"]
    D --> E["返回结果"]

如果 SQL 不带分片键:

sql
select *
from orders
where status = 'PAID'
order by created_at desc
limit 20;

就可能变成广播查询:

mermaid
flowchart TD
    A["SQL 不带分片键"] --> B["发送到所有分片"]
    B --> C["每个分片各查 TopN"]
    C --> D["应用或中间件归并排序"]
    D --> E["取全局 Top20"]

广播查询的问题:

  1. 分片越多,查询放大越严重。
  2. 每个分片都消耗连接和 CPU。
  3. 全局排序分页需要在应用层归并。
  4. 深分页会非常昂贵。

因此分库分表后,接口设计要尽量要求带分片键。例如用户订单列表必须带 user_id,租户资产查询必须带 tenant_id 或 hospital_id。

全局唯一 ID ​

单库单表可以用自增主键。分库分表后,如果每张表都从 1 自增,会出现全局重复。

常见方案:

方案优点缺点
UUID简单,不依赖中心服务太长、无序,索引和存储成本高
数据库号段趋势递增,性能好需要号段服务高可用
雪花算法本地生成,趋势递增依赖时钟,机器号管理复杂
Redis 自增简单Redis 可用性和持久化要考虑
业务编码 + 序列可读性好规则复杂,可能泄露业务信息

雪花 ID 简化结构:

mermaid
flowchart TD
    A["64 位 ID"] --> B["时间戳"]
    A --> C["机房/机器号"]
    A --> D["序列号"]

Java 简化 Demo:

java
public class SnowflakeId {
    private final long workerId;
    private long lastTimestamp = -1L;
    private long sequence = 0L;

    public SnowflakeId(long workerId) {
        if (workerId < 0 || workerId > 1023) {
            throw new IllegalArgumentException("workerId must be 0..1023");
        }
        this.workerId = workerId;
    }

    public synchronized long nextId() {
        long now = System.currentTimeMillis();
        if (now < lastTimestamp) {
            throw new IllegalStateException("clock moved backwards");
        }
        if (now == lastTimestamp) {
            sequence = (sequence + 1) & 4095;
            if (sequence == 0) {
                while (now <= lastTimestamp) {
                    now = System.currentTimeMillis();
                }
            }
        } else {
            sequence = 0;
        }
        lastTimestamp = now;
        return ((now - 1700000000000L) << 22) | (workerId << 12) | sequence;
    }
}

生产上要补齐:

  1. workerId 分配和冲突检测。
  2. 时钟回拨处理。
  3. ID 服务监控。
  4. 压测单毫秒序列耗尽行为。

跨分片 Join 怎么处理 ​

分库分表前:

sql
select o.order_no, u.username
from orders o
join users u on u.id = o.user_id
where o.user_id = 1001;

分库分表后,如果 orders 和 users 不在同一个分片,数据库层 Join 就很难直接做。

常见处理方式:

方式说明适合
冗余字段订单表冗余用户名、手机号摘要读多写少、字段变更不频繁
绑定表同一分片键的表放同一分片订单和订单明细
应用层 Join先查订单,再批量查用户结果集小
数据同步到搜索视图同步到 ES 或宽表复杂检索和列表
禁止跨分片强 Join接口设计要求带分片键核心交易链路

订单和订单明细适合绑定表:

text
orders 按 order_id 分片
order_items 也按 order_id 分片

这样一个订单和它的明细在同一分片,查询订单详情、修改订单明细可以落在同一个库表里。

跨分片事务怎么处理 ​

单库事务:

text
更新订单 + 写订单流水 + 写支付状态

如果这些表都在一个库里,数据库本地事务能保证原子性。

分库后,如果订单在 order_db,支付在 pay_db,就变成分布式事务问题。

常见方案:

方案适合代价
避免跨库事务通过同分片、业务拆分减少跨库写需要建模提前设计
最终一致本地事务 + 消息/补偿要有重试、幂等、对账
TCC强业务一致且可拆 Try/Confirm/Cancel代码复杂
Saga长流程、多步骤业务补偿逻辑复杂
Seata AT常见 Java 微服务场景对 SQL、性能、运维有要求
XA/2PC强一致要求阻塞、性能和可用性成本高

核心原则:

分库分表设计时,要尽量让一个事务内必须强一致的数据落在同一个分片。跨分片强事务越多,系统越难稳定。

更多看:分布式事务。

分页排序为什么变难 ​

单表分页:

sql
select *
from orders
where status = 'PAID'
order by created_at desc
limit 20 offset 1000;

多分片全局分页:

mermaid
flowchart TD
    A["查询所有分片"] --> B["每个分片查局部结果"]
    B --> C["应用层归并排序"]
    C --> D["跳过 offset"]
    D --> E["取 limit"]

问题:

  1. 每个分片都要查更多数据。
  2. 全局 offset 越大,归并成本越高。
  3. 结果可能因为数据变化出现重复或漏数。
  4. 中间件和应用内存压力增加。

优化方向:

需求方案
用户自己的订单按 user_id 精准路由,单分片分页
后台全局订单搜索同步到 ES 或搜索宽表
时间流水翻页Seek 分页,带上上一页最后时间和 ID
报表统计离线数仓或预聚合

Seek 分页示例:

sql
select order_no, user_id, status, created_at
from orders_09
where user_id = 1001
  and (created_at < '2026-07-06 10:00:00'
       or (created_at = '2026-07-06 10:00:00' and id < 90001))
order by created_at desc, id desc
limit 20;

广播表、绑定表和影子表 ​

广播表 ​

每个分片都复制一份的小表,例如字典、地区、状态码。

text
order_db_00.dict_status
order_db_01.dict_status
order_db_02.dict_status

适合:

  1. 数据量小。
  2. 更新少。
  3. 很多业务 SQL 需要 Join。

风险:

  1. 多分片数据要保持一致。
  2. 更新要广播到所有库。
  3. 更新失败要补偿。

绑定表 ​

使用同一个分片键的表。例如订单主表和订单明细都按 order_id 分片:

text
orders_03
order_items_03

好处是同一个订单相关数据落在同一分片,Join 和事务成本低。

影子表 ​

用于迁移、压测、灰度:

text
orders      线上表
orders_new  新分片规则或新结构表

迁移期间可以双写老表和新表,逐步校验一致性,再切读流量。

中间件和实现方式 ​

常见方式:

方式说明适合
应用代码封装路由自己计算库表名简单场景、强控制
ShardingSphere-JDBCJava 应用内分片中间件Java 项目常见
ShardingSphere-Proxy独立代理,应用像连 MySQL多语言、集中治理
MyCAT 等代理代理层分库分表老项目或特定生态
云厂商分布式数据库云服务托管团队不想自建复杂中间件

ShardingSphere 不只是配置库表取模,它会负责 SQL 解析、路由、改写、执行和归并,还涉及绑定表、广播表、读写分离、事务、分布式 ID、迁移与 Proxy 运维。完整内容见 Apache ShardingSphere 原理与实战。

自己拼接表名要非常谨慎:

java
String tableName = route.tableName();
String sql = "select id, order_no from " + tableName + " where user_id = ?";

表名不能用 ? 参数化,所以必须保证 tableName 只能来自白名单路由结果,不能来自用户输入。

商业场景一:订单系统按用户分片 ​

业务特点:

  1. 用户查自己的订单高频。
  2. 订单详情按订单号查询也高频。
  3. 支付回调按支付单或订单号处理。
  4. 后台全局查询和报表相对低频。

设计:

text
orders 按 user_id 分片
order_items 按 user_id 或 order_id 绑定分片
order_route 保存 order_no -> user_id -> 分片
后台搜索同步 ES

建表示例:

sql
create table orders_00 (
  id bigint primary key,
  order_no varchar(64) not null,
  user_id bigint not null,
  status varchar(20) not null,
  total_amount decimal(12, 2) not null,
  created_at datetime not null,
  updated_at datetime not null,
  unique key uk_order_no (order_no),
  key idx_user_status_created (user_id, status, created_at)
) engine = InnoDB default charset = utf8mb4;

创建订单流程:

mermaid
flowchart TD
    A["生成全局订单 ID"] --> B["根据 user_id 计算分片"]
    B --> C["写 orders_xx"]
    C --> D["写 order_route"]
    D --> E["提交本地事务"]
    E --> F["发订单创建事件"]
    F --> G["同步搜索视图或报表"]

按订单号查询:

mermaid
flowchart TD
    A["输入 order_no"] --> B["查 order_route"]
    B --> C["得到 user_id 和分片"]
    C --> D["路由到具体 orders_xx"]
    D --> E["查询订单详情"]

如果没有 order_route,按 order_no 查可能要广播所有分片。

商业场景二:医疗采集结果按医院和时间治理 ​

医疗采集平台常见特点:

  1. 采集结果写入量大。
  2. 查询通常带医院、任务、时间范围。
  3. 历史结果用于追溯,但在线高频看最近状态。
  4. 复杂检索可能同步到 ES。

一种设计:

text
asset_current_status        当前状态表,不分或按 hospital_id 分
collect_result_202607       按月分表保存采集结果
collect_error_202607        按月分表保存异常明细
collect_result_archive      历史归档库

为什么不直接所有表按医院取模?

  1. 采集结果天然按时间增长,历史归档很重要。
  2. 大医院可能成为写入热点。
  3. 查询“最近失败”通常带时间范围。
  4. 当前状态和历史流水的访问模式不同,应该分开设计。

当前状态查询:

sql
select asset_no, collect_status, last_collect_time, last_error_type
from asset_current_status
where hospital_id = 1001
  and collect_status = 'FAILED'
order by last_collect_time desc
limit 20;

历史结果查询:

sql
select task_id, asset_no, collect_time, result_status
from collect_result_202607
where hospital_id = 1001
  and collect_time >= '2026-07-01'
  and collect_time < '2026-07-07'
order by collect_time desc
limit 100;

这里体现一个原则:不是所有表都用同一种分片规则,应该根据表的访问模式设计。

扩容迁移怎么做 ​

分库分表最怕“上线后才发现分片数不够”。扩容通常比首次拆分更难。

扩容流程:

mermaid
flowchart TD
    A["确定新分片规则"] --> B["创建新库表"]
    B --> C["历史数据迁移"]
    C --> D["双写老分片和新分片"]
    D --> E["增量数据同步"]
    E --> F["数据校验"]
    F --> G{"校验通过吗"}
    G -- "否" --> H["修复差异继续校验"]
    G -- "是" --> I["灰度切读"]
    I --> J["全量切换"]
    J --> K["保留回滚窗口"]

迁移必须回答:

  1. 老数据怎么搬?
  2. 迁移期间新增数据怎么同步?
  3. 怎么校验行数、金额、状态、哈希?
  4. 出错怎么回滚?
  5. 切换期间如何避免重复写、漏写?
  6. 业务如何识别老路由和新路由?

校验示例:

sql
select count(*) as cnt,
       sum(total_amount) as amount_sum,
       min(created_at) as min_time,
       max(created_at) as max_time
from orders_00
where created_at >= '2026-07-01'
  and created_at < '2026-07-02';

还可以按主键范围计算摘要,避免只看总数掩盖数据错位。

生产排查 ​

现象可能原因排查方向处理
某个分片特别慢分片键倾斜、大租户热点看各分片 QPS、数据量、慢 SQL大租户单独拆、热点隔离
广播查询拖垮系统SQL 不带分片键看 SQL 路由日志接口强制分片键,搜索同步 ES
跨分片分页慢全局排序归并看每个分片扫描量改精准路由、Seek 分页、搜索视图
写入失败不一致跨库写部分成功看本地事务、消息、补偿表幂等、重试、对账、分布式事务
扩容数据不一致迁移漏数据或双写失败对比行数、金额、哈希、binlog 位点暂停切流,补偿差异
某表索引膨胀分片仍然太大或冷热混合看单表大小、索引大小、Buffer 命中归档、分区、再拆分
连接池耗尽分片多导致连接倍增看每库连接数控制分片数、连接池隔离、代理层

排查流程:

mermaid
flowchart TD
    A["分库分表系统异常"] --> B{"是单分片问题吗"}
    B -- "是" --> C["查该分片数据量、慢 SQL、锁、资源"]
    B -- "否" --> D{"是否广播或跨分片查询"}
    D -- "是" --> E["查路由日志和 SQL 是否带分片键"]
    D -- "否" --> F{"是否迁移或双写期间"}
    F -- "是" --> G["查数据校验、消息补偿、路由版本"]
    F -- "否" --> H["查连接池、代理、中间件、整体资源"]

常见坑 ​

坑后果正确做法
为了慢 SQL 直接分表复杂度上升,慢 SQL 仍慢先 EXPLAIN、索引、归档、分页优化
分片键不在高频查询里大量广播查询按核心访问路径选分片键
分片数拍脑袋很快再次扩容做 3 到 5 年容量估算
忽略全局 ID主键冲突或排序混乱提前设计雪花、号段或路由 ID
跨库事务太多一致性和补偿复杂同事务数据尽量同分片
后台查询直接扫分片拖垮在线库搜索视图、数仓、报表库
扩容无回滚切换失败难恢复双写、校验、灰度、回滚窗口
表名来自用户输入SQL 注入风险表名只来自白名单路由

面试标准回答 ​

分库分表是什么?
分库分表是把一个库或一张表的数据,按业务或分片规则拆到多个库表中,用复杂度换容量、吞吐和隔离能力。它不是普通慢 SQL 的第一选择,应该先优化索引、SQL、分页、归档、缓存和读写分离;当单表数据量、单库资源或写入吞吐接近瓶颈,并且有明确分片键和迁移方案时,再考虑分库分表。

分片键怎么选?
分片键要围绕最高频、最核心的查询和事务边界选择。订单系统如果用户订单列表最高频,可以按 user_id 分片;如果按订单号处理最高频,可以按 order_id 或建立订单路由表。好的分片键要分布均匀、查询常带、基本不变、能减少跨分片事务。不好的分片键会导致大量广播查询、跨分片分页和热点分片。

分库分表后为什么事务变难?
单库事务由数据库本地保证原子性;分库后,一个业务操作可能同时写多个库,本地事务无法覆盖所有分片,就变成分布式事务或最终一致问题。解决方式包括同事务数据尽量放同一分片、本地消息表、事务消息、TCC、Saga、Seata、XA 等。实际项目要根据一致性要求、性能和复杂度选型。

分库分表后分页排序为什么慢?
如果查询不能精准路由到单个分片,就要多个分片各自查一批数据,再在应用或中间件做全局归并排序。分片越多、offset 越大,中间结果越多,成本越高。优化方式是接口带分片键、使用 Seek 分页、同步搜索视图或使用报表/数仓承载全局查询。

分库分表扩容怎么做?
扩容要先确定新路由规则,创建新库表,迁移历史数据,再处理迁移期间增量数据。常见做法是双写、增量同步、数据校验、灰度切读、保留回滚窗口。不能只复制数据就切换,因为切换期间会有新增和修改,必须保证不漏、不重、可校验、可回滚。

关联知识点 ​

知识点说明
大表覆盖索引仍然慢判断什么时候先归档、分页优化,什么时候才考虑拆分
主从复制分库前常见的读扩展方案
MySQL EXPLAIN分库前必须先证明 SQL 慢在哪里
MySQL 事务本地事务边界
分布式事务跨库事务解决方案
Apache ShardingSphereJDBC/Proxy 选型、路由内核、配置、事务、迁移与排障
缓存一致性分库分表后缓存和数据库一致性
Elasticsearch 一致性全局搜索视图同步和补偿

本章小结 ​

分库分表的核心不是“把表名改成很多张”,而是重新设计数据路由、事务边界、查询方式和运维流程。它能解决容量和吞吐问题,但会带来跨分片查询、跨库事务、全局 ID、分页排序、扩容迁移和一致性补偿。真正能在商业项目里用好分库分表的人,必须先能证明为什么普通优化不够,再能说明分片键怎么选、数据怎么迁、出错怎么补、线上怎么查。