# 分库分表

作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)
代码参考:[https://github.com/YangYingmeng/learning_shardingJDBC)


# 一、MySQL 数据库架构的演变

# 1. 数据库的演变升级

随着业务规模的扩大,数据库需要不断演进,以解决性能瓶颈、可用性不足、扩展性受限等问题。典型的演进路径如下:

# 单机数据库

特点:

  • 所有读写操作都集中在一个数据库实例上
  • 架构简单、部署成本低

问题:

  • 高并发下查询慢:CPU、内存、IO 都会成为瓶颈
  • 单点故障(SPOF):一旦数据库宕机,业务完全不可用
  • 无法横向扩展:性能提升只能依赖硬件升级(Scale Up)

# 主从架构(读写分离)

改进点:

  • 引入从库,从库通过主库 异步/半同步 复制数据
  • 主库负责写,从库实现 读扩展

优点:

  • 提升整体读吞吐量
  • 降低主库压力
  • 一定程度上提升可用性(从库可作为备库)

但仍存在问题:

  • 一个主库依然承担所有写压力 → 写成瓶颈
  • 主库仍然是最终的 单点
  • 主从同步延迟,读到旧数据的风险(读写一致性难保证)
  • 增加从库无法解决核心瓶颈:TPS、IO、内存仍受限于主库机器

# 双主架构(多主写入)

为了解决单主写瓶颈,引入双主甚至多主架构。

改进点:

  • 两个 Master 都可以处理写请求
  • 互相同步 binlog,实现数据同步

优点:

  • 写入能力提升
  • 可在不同机房部署,提高可用性

但带来新的更大的挑战:

数据一致性极难保证(多主同步复杂)

  • 主主之间要互相同步写操作,冲突检测难度大
  • 可能出现写冲突、重复写、循环同步等问题
  • 最终一致性难以保证,需要额外的机制(唯一键冲突、全局事务 ID 等)

架构复杂度显著提升

  • 维护成本高
  • 故障恢复流程困难
  • 网络抖动可能导致严重的数据不一致

因此,多主更多用于:

  • 跨地域多活
  • 高可用容灾
  • 对一致性要求不特别高的业务场景

# 分库分表(水平拆分)

当读写压力继续增大,单节点性能已无法满足需求,即使双主也不够时,数据库需要进行 水平扩展(Scale Out)

核心思想: 将数据按照一定规则拆分成多个库(Sharding)和多个表(物理分库),使得:

  • 多台数据库共同承担读写压力
  • 单库数据量变小,查询速度更快
  • 单表数据量可控,减少索引膨胀、加快扫描

优点:

  • 真正实现数据库横向扩展
  • 写能力、存储能力、读能力均可线性提升

挑战:

  • SQL 路由(需要中间层)
  • 跨库事务(CAP 限制)
  • 跨库分页、排序复杂
  • 数据迁移与扩容难度较高

分库分表是大规模业务的必经之路,但复杂度也随之提升,因此一般配合:

  • 中间件如 ShardingSphere、MyCAT
  • 自研分布式 ID
  • 分布式事务补偿机制
  • 缓存 / 搜索引擎协同

# 2. 分库分表的优缺点

分库分表(Sharding)的核心目的是突破单机数据库存储容量和并发性能的极限,实现数据库层面的水平扩展(Scale Out)。 它既能带来性能上的巨大收益,同时也会引入分布式系统的典型复杂性。

# 分库分表的优势

  1. 解决数据库连接数与并发瓶颈

    单机 MySQL 默认最大连接数为 100,理论最大值 16384。当请求量大时,容易出现:

    ERROR 1040 (HY000): Too many connections
    
    1

    分库后,多个数据库实例共同承接连接与请求压力,从根本上提升整体系统的并发能力。

  2. 缓解磁盘、网络、CPU 等硬件瓶颈

    随着数据量增大,数据库会遇到三大硬件瓶颈:

    • 磁盘 IO 瓶颈

      • 数据量过大导致磁盘随机读写变慢

      • 热点数据无法全部放入内存,导致大量 IO 访问

      • SQL 大表扫描会引起严重 IO 冲击

    • 网络 IO 瓶颈

      • 返回数据量大

      • 同一数据库承载大量网络请求

      • 链路延迟增高

    • CPU 瓶颈

      复杂 SQL(排序、聚合、函数、跨表计算)会大量占用 CPU。

      常见 CPU 高占用 SQL 状态:

      • Sending data
      • Copying to tmp table
      • Sorting result
      • Using filesort
      • tmp table on disk

      单库承担所有 SQL 运算时 CPU 容易打满。

# 分库分表带来的问题

  1. 跨节点 JOIN 查询受限

    拆库后:

    • 关联表不在同一库
    • 无法直接执行 SQL JOIN(例如 JOIN、子查询)
  2. 跨库事务问题

    数据不在同一个库时,原本依赖单机事务(ACID)的流程将受到影响,例如:

    库 A:扣减库存
    库 B:创建订单
    
    1
    2

    这会引发:

    • 2PC/3PC 成本高
    • 全局锁与阻塞
    • 数据不一致
    • 分布式中间件参与
  3. 跨节点排序、分页、聚合问题

    拆库后执行以下操作会变得复杂:

    • ORDER BY
    • LIMIT 分页
    • GROUP BY
    • COUNT、SUM、MAX、MIN 等函数

    尤其是:

    排序字段不是分片键时,所有节点需要先局部排序 → 再聚合 → 再全局排序。

  4. 全局主键唯一性问题

    单库依赖自增 ID 时不会冲突,但分库后会有冲突。

  5. 分片扩容困难(数据迁移)

    分库分表后的二次扩容通常面临:

    • 是否需要扩容(2 库扩 4 库)
    • 老数据如何导出迁移
    • 分片键需不需要变更
    • 如何保证迁移过程不停机

    扩容属于最复杂的场景之一,需要工具和运维体系配合。

  6. 分库分表技术选型问题

这些问题的解决方案都会在后续章节中一一解答。

# 二、MySQL 常见的分库分表

# 1. 分库分表的常见方式

# 垂直分表

表字段过多、行数据宽度过大、存在大字段(text/blob)或访问频率差异明显 时,会将一张表拆分成多张。

拆分原则:

  1. 将不常用字段拆分到扩展表(减少主表宽度,提高缓存命中率)。
  2. 将大字段(text/blob)独立存储(减少 I/O 占用)。
  3. 将业务上经常一起查询的字段放在同一张表(避免跨表 join)。

主要目的:减小单表宽度、提升查询性能。

# 垂直分库

当某个数据库实例 CPU 长期高负载(>90%)、连接数频繁耗尽、单库成为瓶颈 时,将业务按功能拆分进不同数据库。

例:订单库、用户库、商品库分离 —— 对应微服务数据库拆分策略。

主要目的:分散压力,提高吞吐量。

# 水平分表

单张表数据量达到千万级甚至更高,影响查询与维护时,根据某种规则将数据拆到多个表中,但仍在同一个数据库里。

常见规则:

  • 按创建时间分表(create_time)
  • 按自增 ID 分片
  • 按业务字段(如用户ID)分片

优点:

  • 减少单表体积,提高查询效率
  • 降低锁竞争时间

缺点:

  • 数据仍在一个库,数据库 I/O、CPU 仍存在瓶颈

# 水平分库

在完成水平分表后,如果 一个数据库的I/O、CPU依然是瓶颈,需要将表组分布到不同物理数据库。

例:

  • user_00 表放 DB1
  • user_01 表放 DB2

主要目的:从根本上解决单库瓶颈,实现读写扩展能力。

# 2. 分库分表策略

# 水平分库分表的常见策略

  1. 基于 Range(范围)路由

    根据某一字段的范围将数据分片。

    常见规则:

    • 按 ID 范围(如 0–1000w, 1000w–2000w)
    • 按时间范围(按月、按季度、按年)
    • 按地域范围(如:华东、华南 分库)

    特点:

    • 数据有明显区分
    • 懒加载表、按需扩容容易
    • 查询直观

    缺点: 热点数据可能集中在某个区间,引发 分片热点问题

  2. 基于 Hash / 取模(Mod)路由

    适用于 ID 离散性较好、数据均匀分布的场景。

    规则示例:

    table_index = user_id % 4
    
    1

    对于非数字:

    table_index = hash(username) % 4
    
    1

    优点:分片数据分布比较均匀,不容易产生热点。

    缺点:

    • 一旦要扩容(从4张表扩到8张表),数据迁移成本大
    • 历史数据需要重算 hash 并迁移

# 三、ShardingJDBC介绍

# 1. 常见术语与概念

# 数据节点

数据分片的最小执行单元,由 数据源名称 + 真实表 名称 组成。例如:

ds_0.product_order_0
1

每个 DataNode 对应一个具体的物理表。

# 真实表

在分片后 真正存在于数据库中的物理表,是最终 SQL 执行的目标表。

例如,同一个逻辑订单表拆分后:

product_order_0
product_order_1
product_order_2
1
2
3

# 逻辑表

多个真实表的统一抽象称为逻辑表,拥有一致的结构和业务含义。例如:

真实表 逻辑表
product_order_0 product_order
product_order_1 product_order
product_order_2 product_order

开发者写 SQL 时只需要访问逻辑表,ShardingJDBC 会根据规则路由真实表。

# 绑定表

分片规则相同 的一组主从关联表。 例如订单主表和订单商品表都按照 order_id 分片:

product_order
product_order_item
1
2

绑定表的 JOIN 不会产生笛卡尔积,可直接精准路由,提高关联查询效率。

# ⼴播表

在所有数据源中都存在且内容一致的表。

常用于:

  • 字典表
  • 配置表
  • 轻量、不会频繁更新的小表

存在于每个数据库中,可避免跨库 JOIN 提升性能。

# 2. 常见分片算法与策略

分片由 分片键(sharding column) + 分片策略(sharding strategy) 两部分组成。

# 分片键

用于决定数据库或数据表如何拆分的核心字段。

示例:订单表 product_order 根据订单号 out_trade_no 取模:

out_trade_no % 8 → 决定表路由
1

ShardingSphere 也支持 多字段联合分片,用于复杂场景下的路由控制。

# 分片策略

  1. 行表达式分片策略(InlineShardingStrategy)

    特点:

    • 仅支持单分片键
    • 使用 Groovy 表达式
    • 简单、高效、不需要 Java 代码实现
    • 支持 SQL 中的 =IN

    示例:

    product_order_$->{user_id % 8}
    
    1

    表示:

    根据 user_id % 8 结果,路由到 product_order_0 ~ product_order_7
    
    1

    适合结构简单、按规则分片的场景。

  2. 标准分片策略(StandardShardingStrategy)

    特点:

    • 仅支持单分片键
    • 需要实现两个核心算法:
      • PreciseShardingAlgorithm(必选,用于 =IN
      • RangeShardingAlgorithm(可选,用于 BETWEEN AND

    如果未配置 RangeShardingAlgorithm,且 SQL 使用区间查询:

    BETWEEN AND
    
    1

    则会触发 全库路由,性能会显著下降。

    适合需要自定义复杂逻辑但只有一个分片键的场景。

  3. 复合分片策略(ComplexShardingStrategy)

    特点:

    • 支持多分片键
    • 开发者自行实现分片逻辑
    • 灵活度最高
    • 支持处理 =, IN, BETWEEN AND 等多种条件

    适用于:

    • 订单按 user_idcreate_time 双字段分片
    • 高度定制化的业务场景
  4. Hint 分片策略(HintShardingStrategy)

    特点:

    • 不依赖 SQL 中的分片键
    • 分片信息由外部手动指定(HintManager)
    • 完全绕过 SQL 解析
    • 性能更高(尤其适用于复杂 SQL)

    常见场景:

    • 一个 SQL 通过某些业务逻辑决定只查某个分片
    • 后台脚本批处理
    • 某些 SQL 无法通过字段判断目标分片

    示例:

    HintManager.getInstance().setDatabaseShardingValue(1);
    
    1

    此时 SQL 会只在分片 1 中执行。