当前位置: 首页 > news >正文

mysql应用层分表(Application-Level Sharding)知识笔记

从 B+ 树到应用层分表:MySQL 海量数据架构解析

为什么单表超过千万行会变慢?InnoDB 页面分裂/合并如何影响性能?分库分表到底怎么选?五种分片策略各自的优劣?分表后如何解决分布式 ID、跨片查询、扩容迁移?


引言:为什么数据库越来越慢?

很多开发者都遇到过这样的场景:项目初期一切正常,随着用户量增长,某张表的数据从百万级冲向千万级,突然——查询变慢了,加索引要锁表半小时,备份恢复耗时越来越长。

这不是个例,而是 B+ 树结构 的固有特性决定的。理解 B+ 树,才能理解为什么分表是解决海量数据问题的终极手段。

本文将从底层原理出发,带你完整走一遍:B+ 树 → InnoDB 物理存储 → 页面分裂/合并 → 分库分表 → 分片策略 → 核心挑战


一、什么是应用层分表

应用层分表是指在应用程序代码中实现数据分片路由逻辑,而非依赖数据库中间件或数据库自身分片能力。应用程序根据分片键(Shard Key)计算目标分表,直接操作对应的物理表。

flowchart LRApp["应用程序<br/>(Lua/Java/C++/Go)"]App -->|"hash(Account) % 100 = 52"| T52["account_login52"]App -->|"hash(Account) % 100 = 7"| T7["account_login7"]App -->|"hash(Account) % 100 = 0"| T0["account_login0"]App -->|"hash(Account) % 100 = 99"| T99["account_login99"]

与中间件分片的区别:

维度 应用层分表 中间件分表(ShardingSphere-Proxy/MyCat)
路由位置 应用代码内 独立代理服务
性能开销 无额外网络跳转 多一跳网络转发
多语言支持 每种语言需各自实现 对应用透明,多语言通用
运维复杂度 分片规则分散在各应用 集中管理
适用场景 单一技术栈、性能要求极高 多语言、大规模集群

二、B+ 树:数据库性能的底层密码

InnoDB 存储引擎使用 B+ 树 作为索引结构。理解 B+ 树的工作原理,才能理解为什么单表数据量增长会导致性能下降。

2.1 为什么 MySQL 选择 B+ 树?

存储引擎是数据库的核心组件,定义数据如何组织、存储和检索。常见的存储数据结构有四种候选方案:

数据结构 单点查询 范围查询 写入性能 适用场景 代表
哈希表 O(1) 最优 不支持(只能全表扫描) O(1) Key-Value 精确查找 Redis、Memcached
平衡二叉树 O(log n) 支持,但 IO 次数多 O(log n) 内存中小数据集 AVL Tree、红黑树
B 树 O(1)~O(log n) 不稳定 支持,但随机 IO 多 O(log n) 早期文件系统 早期 MyISAM
B+ 树 O(log n) 稳定 高效(链表顺序扫描) O(log n) 关系型数据库索引 InnoDB、PostgreSQL

哈希表的致命缺陷:不支持范围查询和排序。

-- 这类查询哈希表只能全表扫描
SELECT * FROM commodity WHERE price > 10 AND price < 100;
SELECT * FROM commodity WHERE ORDER BY price DESC;

平衡二叉树的问题:每个节点只存一个数据,树高度大→磁盘 IO 次数多。磁盘读取时间远超内存比较时间,程序大部分时间阻塞在磁盘 IO 上。

B 树的范围查询问题(以查询"大于 10 小于 30 的数据"为例):

sequenceDiagramparticipant Q as 查询 >10 且 <30participant R as Root 节点participant L as 左子节点participant Ri as 右子节点Q->>R: 1. 遍历根节点,第一个元素 25 > 10R->>L: 2. 遍历左子节点,找到 11(>10)Q->>R: 3. 回溯根节点,发现无 <30 的数据R->>Ri: 4. 遍历右子节点,找到 27(<30)Note over Q,Ri: B 树范围查询需要 4 次随机 IO<br/>且 IO 次数不稳定

B+ 树的优势:叶子节点通过链表顺序连接,范围查询只需定位起点后沿链表顺序扫描,且磁盘预读机制会提前加载相邻页,IO 效率远高于 B 树。

B+ 树 vs B 树 IO 效率对比

B+ 树非叶节点只存索引(键值+指针),单页能存更多索引项,树更矮,IO 更少:

B+ 树非叶节点:key(8B) + pointer(6B) = 14B/条目
单页可存:16KB / 14B ≈ 1170 个索引项B 树非叶节点:key(8B) + pointer(6B) + data(约1KB) ≈ 1018B/条目
单页可存:16KB / 1018B ≈ 16 个索引项
指标 B+ 树 B 树
非叶节点单页索引项数 ~1170 ~16
3 层树可存行数 ~2190 万 ~4096
范围查询 IO 模式 顺序 IO(链表扫描) 随机 IO(回溯父节点)

(天呐,简直碾压~)

2.2 B+ 树结构详解

B+ 树是一种多路平衡查找树,核心设计围绕"减少磁盘 IO"展开。与 B 树的关键区别:

特性 B 树 B+ 树
数据存储位置 所有节点都存数据 只有叶子节点存数据,内部节点只存键值
叶子节点链接 叶子节点通过双向链表连接
查询路径 可能在中间节点就命中 必须走到叶子节点,查询路径稳定
范围查询 需要中序遍历整棵树 沿叶子节点链表顺序扫描即可

以阶数 m(每个节点最多 m 个子节点)为例:

节点规则

节点类型 存储内容 子节点/关键字范围
非叶子节点 仅存索引键值 + 子节点指针,不存数据 子节点数 k:⌈m/2⌉ ≤ k ≤ m;关键字数 k-1
叶子节点 键值 + 数据(或主键值),双向链表连接 关键字数 k:⌈m/2⌉-1 ≤ k ≤ m-1
根节点 同非叶子节点(或叶子节点,当数据很少时) 至少 2 个子节点(非叶时)

内部节点结构

[P1, K1, P2, K2, ..., Pn-1, Kn-1, Pn]P1 指向的子树:所有键值 < K1
Pi 指向的子树:所有键值 >= Ki-1 且 < Ki(1 < i < n)
Pn 指向的子树:所有键值 >= Kn-1

B+ 树性质

  1. 每个节点最多有 m 个子节点
  2. 每个非叶子节点(除根)至少有 ⌈m/2⌉ 个子节点
  3. 根节点至少有 2 个子节点(除非是叶子节点)
  4. 所有叶子节点在同一层(保证查询深度一致)
  5. 有 k 个子节点的非叶子节点包含 k-1 个键

B+ 树结构示意

graph TBsubgraph "第 1 层(Root)"R["Root 节点<br/>键值: 50, 100"]endsubgraph "第 2 层(Branch)"B1["Branch 节点<br/>键值: 10, 20, 30, 40"]B2["Branch 节点<br/>键值: 50, 60, 70, 80"]B3["Branch 节点<br/>键值: 100, 110, 120"]endsubgraph "第 3 层(Leaf)— 双向链表连接"L1["Leaf 页<br/>行数据<br/>1~40"]L2["Leaf 页<br/>行数据<br/>41~80"]L3["Leaf 页<br/>行数据<br/>81~120"]endR --> B1R --> B2R --> B3B1 --> L1B2 --> L2B3 --> L3L1 <-->|双向链表| L2L2 <-->|双向链表| L3

2.3 B+ 树的构建:插入与分裂

B+ 树通过逐条插入数据构建,当节点满时触发分裂。以阶数 m=3 为例:

flowchart TDA["插入 1"] -->|"叶子节点 [1]"| B["插入 2"]B -->|"叶子节点 [1,2] 未满"| C["插入 3"]C -->|"叶子节点满!分裂"| D["分裂为 [1,2] 和 [3]<br/>中间关键字 2 提升到父节点"]D --> E["根节点 [2]<br/>├─ 叶子 [1,2]<br/>└─ 叶子 [3]"]E -->|"插入 4"| F["叶子 [3,4] 未满"]F --> G["根节点 [2]<br/>├─ 叶子 [1,2]<br/>└─ 叶子 [3,4]"]

分裂对性能的影响

影响维度 说明
磁盘 IO 分裂需要写入新页 + 修改原页 + 修改父页,至少 3 次随机 IO
页碎片 分裂后两个页各约 50% 满,空间利用率下降
锁竞争 分裂期间持有页锁,阻塞并发访问

自增主键 vs 随机主键的插入差异

flowchart LRsubgraph "自增主键(顺序插入)"A1["始终在叶节点末尾追加"] --> A2["无需分裂,页填充率高"]A2 --> A3["IO 少,性能优"]endsubgraph "随机主键(如 UUID)"B1["随机位置插入"] --> B2["频繁触发页分裂"]B2 --> B3["页碎片多,IO 多,性能差"]end

2.4 B+ 树的维护:删除与合并

删除操作从叶子节点移除记录,若节点关键字数低于下限,触发借键合并

flowchart TDD1["从叶子节点删除记录"] --> D2{"节点关键字数<br/>≥ 下限?"}D2 -->|是| D3["删除完成,无需调整"]D2 -->|否| D4{"兄弟节点关键字数<br/>> 下限?"}D4 -->|是| D5["向兄弟节点借关键字<br/>更新父节点索引"]D4 -->|否| D6["与兄弟节点合并<br/>删除父节点中的索引键"]D6 --> D7{"父节点关键字数<br/>≥ 下限?"}D7 -->|是| D8["合并完成"]D7 -->|否| D9["递归向上合并<br/>可能降低树高"]

合并对性能的影响

影响维度 说明
磁盘 IO 合并需要读取兄弟页 + 修改两个页 + 修改父页
空间回收 合并后释放空页,提高空间利用率
触发频率 实际生产中删除操作较少触发合并,InnoDB 通过 MERGE_THRESHOLD(默认 50%)控制

另外,借键操作用 > 而不是 ≥,这不是写错,是 B+ 树删除算法的正确逻辑。原因如下:

B+ 树节点的关键字下限 为 ⌈m/2⌉ - 1 (m 为阶数)。

借键操作会从兄弟节点 拿走一个关键字 。如果兄弟节点恰好只有下限个关键字( 等于下限 ),借走一个后它会变成 下限 - 1 , 低于下限 ,违反 B+ 树性质。

假设阶数 m=5,关键字下限 = ⌈5/2⌉ - 1 = 2兄弟节点有 3 个关键字(> 下限)→ 借走 1 个,剩 2 个,≥ 
下限,合法 ✓
兄弟节点有 2 个关键字(= 下限)→ 借走 1 个,剩 1 个,< 
下限,违法 ✗所以只能用 >,不能用 ≥

兄弟节点关键字数 能否借键 原因 > 下限 能借 借走后兄弟仍 ≥ 下限 = 下限 不能借 借走后兄弟 < 下限,只能合并

简单说: > 保证借键后兄弟节点不会跌破下限, ≥ 会导致兄弟节点也违规 。

2.5 聚簇索引 vs 非聚簇索引:数据到底存在哪?

理解聚簇索引和非聚簇索引的区别,是理解 InnoDB 性能特征的关键。两者的核心差异只有一句话:数据行存在哪里?

聚簇索引(Clustered Index):数据就是索引

聚簇索引的 B+ 树叶子节点直接存储整行数据,索引和数据是同一棵树。就像一本字典按拼音排序,拼音索引的条目旁边直接印着释义——你找到了拼音,就找到了释义,不需要再去翻另一页。

flowchart TBsubgraph "聚簇索引(主键索引)"direction TBR["Root<br/>键值: 50, 100"]B1["Branch<br/>键值: 10, 30"]B2["Branch<br/>键值: 50, 80"]L1["Leaf 页<br/>id=1, name='张三', age=20<br/>id=5, name='李四', age=25<br/>id=30, name='王五', age=30"]L2["Leaf 页<br/>id=50, name='赵六', age=35<br/>id=80, name='钱七', age=40"]R --> B1R --> B2B1 --> L1B2 --> L2L1 <-->|"双向链表"| L2end

关键特征

  • 每张表只能有一个聚簇索引(因为数据只能按一种顺序物理存储)
  • InnoDB 中主键就是聚簇索引;若无主键,选第一个唯一非空索引;若都没有,InnoDB 隐式创建 6 字节 ROWID
  • 数据行按主键顺序物理存储,因此主键查询和范围查询极快

非聚簇索引(Non-Clustered Index / 二级索引):索引指向数据

非聚簇索引的 B+ 树叶子节点不存数据,存的是"指向数据的地址"。就像书末尾的关键词索引,关键词旁边印的是页码——你找到了关键词,还得翻到对应页码才能看到内容。

flowchart TBsubgraph "非聚簇索引(二级索引 idx_name)"direction TBR2["Root<br/>键值: '李四', '王五'"]L3["Leaf 页<br/>name='张三' → 主键 id=1<br/>name='李四' → 主键 id=5"]L4["Leaf 页<br/>name='王五' → 主键 id=30<br/>name='赵六' → 主键 id=50"]R2 --> L3R2 --> L4endsubgraph "聚簇索引(数据真正存储的地方)"direction TBL5["Leaf 页<br/>id=1, name='张三', age=20<br/>id=5, name='李四', age=25"]L6["Leaf 页<br/>id=30, name='王五', age=30<br/>id=50, name='赵六', age=35"]endL3 -->|"回表:用主键 id 再查聚簇索引"| L5L4 -->|"回表:用主键 id 再查聚簇索引"| L6

关键特征

  • 一张表可以有多个非聚簇索引(每个二级索引都是非聚簇索引)
  • InnoDB 的二级索引叶子节点存主键值(而非物理地址),MyISAM 的索引叶子节点存数据物理地址
  • 查询非索引列时需要回表:先查二级索引拿到主键值,再用主键值查聚簇索引获取整行数据

回表:非聚簇索引的性能代价

sequenceDiagramparticipant App as 查询participant SI as 二级索引 B+ 树participant CI as 聚簇索引 B+ 树App->>SI: 1. 查 name='王五'SI-->>App: 返回主键 id=30App->>CI: 2. 查 id=30(回表)CI-->>App: 返回整行数据Note over App,CI: 回表 = 多查一棵 B+ 树<br/>多 2~4 次磁盘 IO

回表的性能影响

场景 是否回表 IO 开销 性能
主键查询(走聚簇索引) 1 棵 B+ 树的 IO
二级索引查索引列(覆盖索引) 1 棵 B+ 树的 IO
二级索引查非索引列(需回表) 2 棵 B+ 树的 IO

覆盖索引:如果查询所需的所有列都包含在二级索引中,就不需要回表。例如索引 idx_name_age(name, age),查询 SELECT age FROM user WHERE name = '王五' 只需查二级索引即可,因为 name 和 age 都在索引里。

聚簇索引对性能的全面影响

维度 聚簇索引(InnoDB) 非聚簇索引(MyISAM)
主键等值查询 极快(1 棵树,数据就在叶节点) 快(1 棵树,叶节点存地址,需额外寻址)
主键范围查询 极快(叶节点链表顺序扫描,数据物理相邻) 慢(地址不连续,随机 IO)
二级索引查询 可能回表(多查 1 棵树) 无需回表(直接存地址)
插入性能 依赖主键类型(自增快,随机慢) 与主键类型关系不大
页分裂频率 高(数据在聚簇索引中,随机主键触发分裂) 低(索引与数据分离,分裂只影响索引树)
空间占用 二级索引存主键值(可能较大) 所有索引存物理地址(固定小)

为什么 InnoDB 二级索引存主键值而非物理地址?

聚簇索引会发生页分裂,数据行物理地址会变化。如果二级索引存物理地址,每次页分裂都要更新所有二级索引中受影响行的地址,维护成本极高。存主键值则页分裂不影响二级索引——代价是回表时多查一棵树,但维护成本大幅降低。

聚簇索引 + 自增主键 = 最佳拍档

自增主键保证数据按插入顺序追加到聚簇索引末尾,无需页分裂,页填充率高达 ~94%。如果用 UUID 做主键,随机插入会频繁触发页分裂,页填充率降至 ~50%,而且二级索引存的是 36 字节的 UUID 字符串(vs 8 字节的 BIGINT),索引体积膨胀 4~5 倍。

如何设置聚簇索引和非聚簇索引

SQL 中通过不同的关键字来设置:

类型 SQL 关键字 说明 每表数量
聚簇索引 PRIMARY KEY 主键即为聚簇索引,数据按主键物理存储 只能 1 个
非聚簇索引 KEY / INDEX 普通索引,叶节点存主键值 可以多个
唯一非聚簇索引 UNIQUE KEY 唯一索引,也是非聚簇索引 可以多个
-- 聚簇索引:PRIMARY KEY(只能有一个)
PRIMARY KEY (`IncrementNumber`) USING BTREE-- 非聚簇索引:KEY / INDEX(可以有多个)
KEY `User` (`UserName`,`ServerId`) USING BTREE-- 唯一非聚簇索引:UNIQUE KEY
UNIQUE KEY `uk_user_name` (`UserName`) USING BTREE

InnoDB 选择聚簇索引的优先级

1. 显式定义的 PRIMARY KEY → 聚簇索引
2. 第一个 UNIQUE NOT NULL 索引 → 聚簇索引
3. 都没有 → InnoDB 隐式创建 6 字节 ROWID → 聚簇索引

举个案例:用户表

| IncrementNumber | UserName | ServerId | FieldA | FieldB | ... |

PRIMARY KEY (`IncrementNumber`) USING BTREE,    -- 聚簇索引:数据按 IncrementNumber 物理存储
KEY `User` (`UserName`,`ServerId`) USING BTREE -- 非聚簇索引:叶节点存 IncrementNumber
  • IncrementNumber 是自增主键,天然就是聚簇索引,数据按自增顺序物理追加,页填充率高、分裂少
  • User 是复合二级索引,查 UserName 时先在二级索引树找到 IncrementNumber,再回表到聚簇索引取整行数据
  • 复合索引 (UserName, ServerId) 遵循最左前缀原则:查 UserNameUserName + ServerId 都能命中,但只查 ServerId 则索引失效

2.6 InnoDB vs MyISAM 的 B+ 树差异

flowchart TBsubgraph "InnoDB(聚簇索引)"CI["聚簇索引(主键)<br/>叶节点存储整行数据"]SI["二级索引(非主键)<br/>叶节点存储主键值"]CI ---|"回表查询"| SIendsubgraph "MyISAM(非聚簇索引)"MI["主键索引<br/>叶节点存储数据物理地址"]M2["二级索引<br/>叶节点存储数据物理地址"]MI ---|"直接定位"| M2end
对比维度 InnoDB(B+ 树) MyISAM(B+ 树)
索引类型 聚簇索引 + 二级索引 仅非聚簇索引,无聚簇索引
叶节点存储内容 聚簇索引存整行数据,二级索引存主键值 所有索引均存数据物理地址
数据与索引关系 数据与聚簇索引绑定,存储在一起 数据与索引分离,单独存储
查询性能 主键查询快,二级索引需回表,范围查询高效 无需回表,但无聚簇索引,范围查询性能差

InnoDB 聚簇索引规则:每张表有且只有一个聚簇索引——若显式定义主键,主键即为聚簇索引;若无主键,选择第一个唯一非空索引;若都没有,InnoDB 隐式创建 6 字节 ROWID。

为什么二级索引存主键值而非物理地址? 因为聚簇索引会发生页分裂,数据行物理地址会变化;若二级索引存物理地址,页分裂时需更新所有二级索引,维护成本极高;存主键值则页分裂不影响二级索引。

2.7 基于 B+ 树的实战优化技巧

优化 1:主键优先选择自增有序主键

B+ 树有序插入时仅需在叶节点末尾添加数据,无需分裂节点;UUID 等无序主键会导致频繁节点分裂,产生碎片。

推荐:bigint auto_increment 自增主键
避免:UUID、随机字符串主键

优化 2:复合索引遵循最左前缀原则

B+ 树复合索引按"先第一列、再第二列"排序,查询条件必须从最左列开始匹配。

-- 复合索引 idx_name_age (user_name, age)
SELECT * FROM user WHERE user_name = '张三';              -- 命中索引
SELECT * FROM user WHERE user_name = '张三' AND age = 20; -- 命中索引
SELECT * FROM user WHERE age = 20;                        -- 索引失效!未用第一列
SELECT * FROM user WHERE user_name LIKE '%三';            -- 索引失效!右模糊

优化 3:利用覆盖索引避免回表

覆盖索引 = 索引包含查询所需的所有字段,无需回表查聚簇索引,减少一次磁盘 IO。

-- 二级索引 idx_name_age (user_name, age),查询 phone 需回表
SELECT age, phone FROM user WHERE user_name = '张三';-- 覆盖索引 idx_name_age_phone (user_name, age, phone),无需回表
CREATE INDEX idx_name_age_phone ON user(user_name, age, phone);
SELECT age, phone FROM user WHERE user_name = '张三';

优化 4:避免索引失效

失效场景 示例 优化方案
对索引列使用函数 WHERE YEAR(create_time) = 2026 WHERE create_time >= '2026-01-01' AND create_time < '2027-01-01'
隐式类型转换 WHERE phone = 13800138000(varchar 列) WHERE phone = '13800138000'
前导模糊查询 WHERE name LIKE '%张三' WHERE name LIKE '张三%' 或用全文索引
索引列含 NULL WHERE col IS NULL 索引列设为 NOT NULL,用默认值替代

2.8 面试高频题速查

题目 核心答案
MySQL 为什么用 B+ 树不用 B 树? ① 非叶节点纯索引,树更矮,IO 更少;② 叶节点链表,范围查询高效;③ 查询路径固定,性能稳定;④ 数据集中存储,缓存利用率高
B 树与 B+ 树核心区别? ① 关键字:B 树分散所有节点,B+ 树仅叶节点;② 叶节点:B 树独立,B+ 树双向链表;③ 查询:B 树可在非叶节点返回,B+ 树必须到叶节点
InnoDB 和 MyISAM 的 B+ 树区别? InnoDB 有聚簇索引(数据与索引绑定),MyISAM 仅有非聚簇索引(索引与数据分离)
什么是回表?如何避免? 二级索引查到主键值后再查聚簇索引获取整行数据。避免方法:覆盖索引
为什么推荐自增主键? 有序插入无需节点分裂,UUID 无序导致频繁分裂和碎片
最左前缀原则? 复合索引查询必须从最左列开始匹配,跳过则索引失效

三、InnoDB 物理存储:从逻辑树到磁盘页

来源参考:InnoDB B+树页面分裂与合并:物理结构、机制与优化 - 华为云

上一章从逻辑层面介绍了 B+ 树的分裂与合并。本章深入到 InnoDB 的物理存储层面,详细分析页面分裂与合并的实现机制、性能影响和优化策略——这是理解"为什么分表能提升性能"的关键。

3.1 物理存储四层结构

InnoDB 的 B+ 树通过多层结构映射在磁盘上:

flowchart TBsubgraph "表空间(Tablespace)— .ibd 文件"subgraph "数据段(Leaf Segment)"E1["区 1<br/>页1 页2 ... 页64"]E2["区 2<br/>页65 页66 ... 页128"]F1["碎片区中的页<br/>页129 页130"]endsubgraph "索引段(Non-Leaf Segment)"E3["区 1<br/>页1 页2 ... 页64"]F2["碎片区中的页<br/>页65"]endend
层次 大小 说明
表空间(Tablespace) 不定 逻辑容器,每个表一个 .ibd 文件(独立表空间模式)
段(Segment) 不定 每棵 B+ 树使用 2 个段:索引段(非叶节点)+ 数据段(叶节点)
区(Extent) 1MB 64 个连续页,空间分配以区为单位
页(Page) 16KB 基础 IO 单元,B+ 树每个节点对应一个页

InnoDB 页内部结构

区域 大小 说明
File Header 38B 页号、前后页指针、校验和、LSN
Page Header 56B 页类型、行数、空闲空间起点
Infimum + Supremum 26B 虚拟最小/最大记录,构成页内链表哨兵
User Records 可变 实际数据行,按主键顺序组织为单向链表
Free Space 可变 未分配空间,插入时从此区域分配
Page Directory 可变 页内目录,二分查找加速页内搜索
File Trailer 8B 校验和,防止页写入不完整

3.2 段与页的关系和区别

段和页是 InnoDB 物理存储中不同层次的概念,容易混淆。核心区别:段是逻辑上的空间管理单位,页是物理上的 IO 操作单位

段(Segment)是什么

段是 InnoDB 为 B+ 树分配和管理空间的逻辑单位。每棵 B+ 树(即每个索引)使用 2 个段:

段类型 存储内容 为什么分开
数据段(Leaf Segment) B+ 树的所有叶子节点页 叶子节点存储行数据,访问频率高,集中存储有利于范围扫描的顺序 IO
索引段(Non-Leaf Segment) B+ 树的所有非叶子节点页 非叶子节点存储索引键值+指针,通常常驻 Buffer Pool,集中存储有利于缓存命中

段的核心职责:记录"哪些区/页属于我",并在需要时向表空间申请新的区或页。段本身不存储数据,数据存储在段所管理的页中。

页(Page)是什么

页是 InnoDB 磁盘 IO 和数据存储的最小单位。每页默认 16KB,B+ 树的每个节点对应一个页。页内存储实际的行数据或索引键值。

段与页的关系

flowchart LRsubgraph "归属关系"S["段"] -->|"管理"| E["区(64 个连续页)"]E -->|"包含"| P["页(16KB)"]endsubgraph "分配流程"A1["B+ 树需要新页"] --> A2["向所属段申请"]A2 --> A3{"段有空闲页?"}A3 -->|是| A4["从段的空闲链表取一个页"]A3 -->|否| A5{"段有碎片页可用?"}A5 -->|是| A6["从碎片区分配一个页"]A5 -->|否| A7["向表空间申请一个完整的区<br/>(64 个连续页)"]end
维度 段(Segment) 页(Page)
本质 逻辑空间管理单位 物理数据存储单位
大小 不固定,动态增长 固定 16KB
数量关系 一个段管理多个区(每个区 64 页)+ 碎片页 一个页属于一个段(通过段的区或碎片区)
存储内容 不直接存数据,只管理空间分配 存储行数据或索引键值
IO 层面 不参与 IO 操作 是磁盘 IO 的最小单位
生命周期 随索引创建而创建,随索引删除而删除 随段分配而创建,随段释放而归还
分配粒度 以区(1MB)为单位向表空间申请 以页(16KB)为单位向段申请

为什么需要段这个中间层?

没有段的话,每个页都直接向表空间申请,会产生严重的管理开销和碎片。段作为中间层提供三个关键能力:

  1. 局部性:段优先从自己已有的区中分配页,保证同一 B+ 树的页在物理上尽量连续,提升范围扫描性能
  2. 批量分配:段以区(1MB = 64 页)为单位申请空间,减少频繁向表空间申请的开销
  3. 碎片页管理:段初期不需要完整区时,可以从碎片区借用单个页,避免浪费空间
flowchart TDsubgraph "没有段(每个页直接向表空间申请)"N1["页A"] --- N2["...其他索引的页..."] --- N3["页B"] --- N4["...其他索引的页..."] --- N5["页C"]N6["物理位置分散,范围扫描退化为随机 IO"]endsubgraph "有段(段管理区的分配)"Y1["区1: 页A 页B 页C ... 页64<br/>物理连续"] --- Y2["区2: 页D 页E ... 页64<br/>物理连续"]Y3["同一 B+ 树的页尽量集中,范围扫描高效"]end

碎片区的作用

当段刚创建、数据量很少时,分配一个完整区(1MB)会浪费空间。InnoDB 的解决方案是碎片区(Fragment Extent)

flowchart TDsubgraph "碎片区(共享区)"FE["区(64 页)"]FE --> P1["页 → 数据段 A"]FE --> P2["页 → 索引段 B"]FE --> P3["页 → 数据段 C"]FE --> P4["页 → 索引段 A"]FE --> P5["空闲页"]end
阶段 分配策略 原因
段数据量 < 32 页 从碎片区借用单个页 避免分配完整区浪费空间
段数据量 ≥ 32 页 申请专属的完整区 32 页 = 半个区,此时分配完整区不再浪费,且专属区保证物理连续

3.3 页面分裂机制

当页达到填充上限时触发分裂,主要场景:

  • 插入满页(INSERT):新记录需按序插入但页面空闲空间不足
  • 更新扩容(UPDATE):更新存量记录,数据长度增加导致页无法容纳(如 VARCHAR 填充)

分裂详细流程

flowchart TDS1[1 满页判定<br/>目标页(叶/内部页)无法容纳新记录] --> S2[2 新页分配<br/>从表空间对应段分配空闲页<br/>优先使用碎片区,必要时分配新区]S2 --> S3[3 分裂点选择<br/>按键值顺序选择记录<br/>目标为近似均分数据]S3 --> S4[4 记录重分配<br/>分裂点后记录迁移至新页<br/>触发分裂的记录按序插入原页或新页]S4 --> S5[5 指针更新<br/>页指针:新页插入双向链表,调整 PREV/NEXT<br/>父页指针:在父页插入新页最小键值及页指针]S5 --> S6[6 父页是否已满?]S6 -->|否| S7[ 分裂完成]S6 -->|是| S8[递归触发父页分裂,直至根页]

顺序插入 vs 随机插入的分裂差异

维度 顺序插入(AUTO_INCREMENT PK) 随机插入(UUID / Hash PK)
插入位置 始终在最左页或最右页 随机发生在任意页面位置
分裂时记录迁移 仅迁移少量记录,原页维持满页 页面中部分裂,约 50% 记录需移动
页填充率 ~15/16(约 94%) ~50%
分裂频率 高,且进一步加剧碎片化
物理有序性 有序 无序

关键结论:插入模式从根本上决定了页分裂频率、存储密度(页填充率)以及索引碎片化程度。顺序键插入(如 AUTO_INCREMENT)在降低分裂频率与维持高存储密度方面具有显著优势。

3.4 页面合并机制

页面合并是页面分裂的互补操作,通过合并可以释放低填充页占用的物理存储,提升页内有效数据占比。

触发条件:页内记录占比低于 MERGE_THRESHOLD(默认 50%)时触发,主要场景:

  • 删除(DELETE):物理删除记录并释放页面内空间
  • 更新缩容(UPDATE):更新导致记录物理长度降低,降低页面填充率

合并详细流程

flowchart TDM1[1 低填充判定<br/>DML 操作后检测页填充率<br/>是否低于 MERGE_THRESHOLD(默认 50%)] --> M2[2 相邻页检查<br/>通过 FIL_PAGE_PREV/NEXT 定位相邻页]M2 --> M3[3 合并条件验证<br/>当前页填充率 < 50%<br/>且邻居页有足够空间?]M3 -->|不满足| M4[不触发合并]M3 -->|满足| M5[4 记录迁移<br/>目标页记录按序迁移至邻居页]M5 --> M6[5 页释放<br/>空页归还表空间,更新 XDES 位图]M6 --> M7[6 指针更新<br/>页指针:调整 PREV/NEXT 绕过释放页<br/>父页指针:移除父页中对释放页的指针]M7 --> M8[7 父页填充率<br/>是否低于 MERGE_THRESHOLD?]M8 -->|否| M9[合并完成]M8 -->|是| M10[递归触发父页合并]

页面合并的效果与局限

维度 说明
优势 减少内部碎片,提升页密度,降低磁盘空间占用,使缓冲池缓存更多有效页面
局限 1 内部碎片清理不彻底——仅当页面填充率 < 50%(默认)时触发,填充率长期大于 50% 的碎片无法回收
局限 2 无法解决外部碎片化(物理存储无序),页面合并后释放页再复用时,可能导致外部碎片化加剧

3.5 分裂/合并的性能影响

影响维度 详细说明
索引碎片化(内部碎片) 页面分裂和合并都会带来页内空间浪费:随机插入触发分裂导致 50% 填充率;合并阈值限制导致删除操作的残留碎片不能马上回收
索引碎片化(外部碎片) 页面分裂时新页随机分配会破坏物理连续性;页面合并时释放页会加剧物理离散性。逻辑连续的页在磁盘物理位置上离散,范围扫描性能劣化
I/O 操作增加 读放大:内部碎片化导致需要更多页来存储相同数据。随机 I/O:外部碎片化打破页面物理连续性,影响 InnoDB 预读效率,顺序扫描退化为随机 IO
缓冲池效率下降 碎片页与高密度页占用相同的缓冲池槽位,碎片页过多时缓冲池内大量空间空闲,命中率降低
锁竞争 分裂/合并需短暂持有闩锁(Latch),高并发写入时可能引发竞争

外部碎片化示意

flowchart LRsubgraph "逻辑顺序"L1["页A"] --> L2["页B"] --> L3["页C"]endsubgraph "磁盘物理位置(外部碎片化后)"P1["...页X..."] --- P2["页C"] --- P3["...页Y..."] --- P4["页A"] --- P5["...页Z..."] --- P6["页B"]endL1 -.->|"物理位置不连续"| P4L2 -.->|"物理位置不连续"| P6L3 -.->|"物理位置不连续"| P2

3.6 四大优化策略

策略 1:主键设计优化(最重要)

主键类型 页填充率 分裂频率 碎片化程度 推荐度
AUTO_INCREMENT(INT/BIGINT) ~94% 最推荐
有序 UUID(UUIDv6/v7) 中等 中等 中等 妥协方案
随机 UUID(UUIDv4)/ Hash ~50% 严重 避免使用

分布式系统必须使用 UUID 时,采用时序 UUID(UUIDv1/v6/v7)+ BINARY(16) 存储 + 字节重排(时间戳前置),效果显著优于完全随机键,但仍无法达到纯自增主键的性能水平。

策略 2:调整 innodb_fill_factor

参数 作用 代价 适用场景
innodb_fill_factor 控制索引创建/重建过程中的页填充百分比 页初始密度低,范围扫描初始性能下降,空间膨胀 预期重建后存在高频随机 DML 操作的表,可延缓页面首次分裂

默认行为:未指定时叶页初始化填充率为 100%,存储密度最大化,仅保留 InnoDB 默认强制预留的 1/16 空间。适用于以顺序插入为主或重建后只读/低频更新的表。

策略 3:表重组(定期重建)

对于有随机主键或高频 DML 的表,长期运行会导致碎片化严重,需定期重建:

OPTIMIZE TABLE table_name;
ALTER TABLE table_name ENGINE=InnoDB;
ALTER TABLE table_name FORCE;
效果 说明
消除内部碎片 按序生成紧凑页
消除外部碎片 尽可能保证新的 .ibd 文件内页面物理有序
注意 大表重建可能导致持锁时间较长,可使用 pt-online-schema-change 或 gh-ost 降低锁影响

策略 4:行格式选择

行格式 大字段处理方式 分裂影响 推荐
DYNAMIC / COMPRESSED 大字段完全存储到溢出页,B+ 树页仅存 20 字节指针 减少因更新大字段引发的页面分裂 推荐(MySQL 8 默认)
COMPACT / REDUNDANT B+ 树页存储大字段前缀(≤768 字节)+ 溢出页指针 字段前缀频繁更新时易触发分裂 避免用于大字段频繁更新场景

3.7 为什么分表能解决这些问题

理解 InnoDB 页面分裂/合并机制,就能理解为什么分表能有效提升性能

flowchart TBsubgraph "不分表:1 亿行单表"A1["B+ 树 4 层<br/>随机插入频繁触发页分裂<br/>页填充率 ~50%<br/>内部+外部碎片严重<br/>缓冲池效率低"]endsubgraph "分表后:100 张表各 100 万行"A2["每张表 B+ 树 3 层<br/>分裂频率大幅降低<br/>页填充率更高<br/>碎片可控<br/>缓冲池命中率提升"]endA1 -->|"分表"| A2
维度 不分表(1 亿行) 分表后(100 × 100 万行)
B+ 树层级 4 层(多 1 次磁盘 IO) 3 层
页分裂频率 高(随机插入命中满页概率大) 低(单表数据少,满页概率小)
页填充率 ~50%(随机插入) ~94%(AUTO_INCREMENT 顺序插入)
碎片化程度 严重(分裂频繁+合并不彻底) 轻微
缓冲池效率 低(碎片页浪费缓存槽位) 高(紧凑页缓存更多有效数据)
DDL 耗时 加索引可能 30 分钟+ 每张表数秒,可分批执行

四、单表到底能存多少行?

4.1 B+ 树层级与行数计算

计算推导

3 层 B+ 树能存储的最大行数:第 1 层(Root): 1 个页 = 1170 个指针第 2 层(Branch): 1170 个页 × 1170 个指针 = 1,368,900 个指针第 3 层(Leaf): 1,368,900 个页 × 16 行/页 ≈ 2190 万行4 层 B+ 树能存储的最大行数:第 4 层(Leaf): 1170 × 1170 × 1170 × 16 ≈ 256 亿行
B+ 树层级 最大行数(理论值) 磁盘 IO 次数
2 层 ~1.8 万行 2 次
3 层 ~2190 万行 3 次
4 层 ~256 亿行 4 次

注意:实际行数取决于单行大小。如果单行更大(如含 varchar(207)),每页存储的行数更少,3 层能容纳的行数也会相应减少。1000 万~5000 万行处于 3 层到 4 层的过渡区间。

4.2 多一层就多一次磁盘 IO

B+ 树的每一层对应一次页读取。查询过程:

sequenceDiagramparticipant App as 应用程序participant BP as Buffer Pool(内存缓存)participant Disk as 磁盘Note over App,Disk: 3 层 B+ 树查询过程(3 次 IO)App->>BP: 1. 读取 Root 页BP-->>App: 命中缓存,返回 Branch 页号App->>BP: 2. 读取 Branch 页BP-->>App: 命中缓存,返回 Leaf 页号App->>BP: 3. 读取 Leaf 页BP-->>App: 返回目标行数据Note over App,Disk: 4 层 B+ 树查询过程(4 次 IO)App->>BP: 1. 读取 Root 页BP-->>App: 返回 Branch1 页号App->>BP: 2. 读取 Branch1 页BP-->>App: 返回 Branch2 页号App->>BP: 3. 读取 Branch2 页(新增的层!)BP-->>App: 返回 Leaf 页号App->>BP: 4. 读取 Leaf 页BP-->>App: 返回目标行数据

关键点

  1. Root 页通常常驻内存(Buffer Pool 命中率高),第 1 次 IO 通常是内存读取
  2. Branch 页在热数据场景下也有较高缓存命中率
  3. Leaf 页数量巨大(百万级),缓存命中率低,大概率需要磁盘 IO
  4. 每多一层,就多一次可能需要磁盘 IO 的页读取。磁盘随机 IO 一次约 10ms(机械硬盘)或 0.1ms(SSD)

4.3 性能影响量化

场景 B+ 树层数 磁盘 IO 次数 机械硬盘延迟 SSD 延迟
< 1000 万行 3 层 2~3 次 20~30ms 0.2~0.3ms
1000 万 ~ 5000 万行 3~4 层 3~4 次 30~40ms 0.3~0.4ms
> 5000 万行 4 层+ 4+ 次 40ms+ 0.4ms+

Buffer Pool 命中时为内存读取(微秒级),未命中时才需要磁盘 IO。数据量越大,Buffer Pool 命中率越低,磁盘 IO 越频繁。

4.4 单表的其他瓶颈

  • DDL 操作困难:单表加索引、改结构可能耗时数小时并长时间锁表
  • 备份恢复慢:单表过大,备份/恢复极为耗时
  • 写入瓶颈:单机 CPU/磁盘 IO 有上限
  • 连接数限制:MySQL 默认 max_connections=151

4.5 适用边界:什么时候该分表

  • 不需要分表:单表 < 1000 万行,通过索引优化、读写分离、缓存即可解决
  • 必须分表:单表 > 5000 万行,TPS/QPS 超过单库承载上限

五、分库与分表:概念、痛点与选型

5.1 分库 vs 分表

分库:将原本存储在一个数据库实例中的数据,按一定规则分散到多个数据库实例中。每个数据库实例独立部署,拥有独立的连接池、Buffer Pool、CPU 和磁盘 IO。

分表:将原本存储在一张表中的数据,按一定规则分散到同一数据库内的多张物理表中。这些表结构相同,只是表名不同(如 user_0user_1user_2)。

flowchart TBsubgraph "分库(拆数据库实例)"DB1["db_1 实例<br/>独立连接池/Buffer Pool/CPU/磁盘"]DB2["db_2 实例<br/>独立连接池/Buffer Pool/CPU/磁盘"]endsubgraph "分表(拆表,同库内)"T0["user_0<br/>同库同实例"]T1["user_1<br/>同库同实例"]T2["user_2<br/>同库同实例"]end
维度 分库 分表
拆分对象 数据库实例 物理表
资源隔离 完全隔离(连接池、Buffer Pool、CPU、磁盘各自独立) 共享资源(同一实例)
解决瓶颈 连接数瓶颈、IO 瓶颈、CPU 瓶颈 单表数据量过大导致的查询慢
网络开销 跨库查询需网络通信 同库查询,无额外网络开销
事务支持 跨库事务复杂(需分布式事务) 同库内可用本地事务

5.2 分库解决的四大痛点

痛点 1:连接数瓶颈

单库 3000 连接上限
50 台应用服务器 × 50 连接/台 = 2500 连接 → 接近上限
100 台应用服务器 × 50 连接/台 = 5000 连接 → 超限!必须分库

痛点 2:磁盘 IO 瓶颈 — 单实例磁盘 IO 吞吐量有上限,分库后每个库实例使用独立磁盘。

痛点 3:CPU 瓶颈 — 单实例 CPU 处理复杂查询可能满载,分库后每个库实例独立 CPU。

痛点 4:Buffer Pool 竞争 — 单实例 Buffer Pool 被所有表共享,分库后每个库实例独立 Buffer Pool,业务模块间互不干扰。

5.3 分表解决的四大痛点

痛点 说明 分表效果
单表数据量过大 B+ 树层级从 3 层增长到 4 层,每次查询多一次磁盘 IO 1 亿行拆为 100 张表,每张表仅 100 万行,B+ 树稳定在 3 层
DDL 操作耗时过长 单表 5000 万行加索引可能耗时 30 分钟以上 每张表 50 万行,加索引仅需数秒,且可分批执行
索引维护成本高 单表索引体积大,插入/删除时维护开销大 每张表的索引更小,维护更快
数据备份恢复慢 单表过大导致 mysqldump 备份耗时过长 可并行备份/恢复各分表,速度大幅提升

5.4 什么时候分库,什么时候分表

flowchart TDA{"遇到性能瓶颈?"} -->|"否"| Z["无需拆分"]A -->|"是"| B{"瓶颈类型?"}B -->|"单表查询慢<br/>数据量过大"| C["分表"]B -->|"连接数不够<br/>IO/CPU 满载<br/>Buffer Pool 竞争"| D["分库"]B -->|"两者都有"| E["分库 + 分表"]C --> F{"单库能扛住<br/>并发和连接数?"}F -->|"是"| G["只分表即可"]F -->|"否"| H["需分库分表"]D --> I{"单表数据量<br/>是否过大?"}I -->|"否"| J["只分库即可"]I -->|"是"| K["需分库分表"]
场景 瓶颈表现 方案 示例
用户表 5000 万行,查询变慢但 QPS 不高 单表数据量过大 只分表 user_0~user_99
10 个业务模块共用 1 个库,连接数不够 连接数瓶颈 只分库 user_dborder_dbproduct_db
订单表日增百万,查询慢且写入 QPS 高 数据量 + 并发双重瓶颈 分库分表 order_db_0.order_0~order_db_3.order_24
日志表只写不读,写入 QPS 极高 写入瓶颈 分库分表 或换 LSM 日志库 + 按时间分表
配置表几百行,查询频繁 无瓶颈 不拆分 保持单表

量化参考标准

指标 只分表 只分库 分库分表
单表行数 > 1000 万 < 500 万 > 1000 万
单库 QPS < 2000 > 2000 > 2000
单库连接数 < 3000 > 3000 > 3000
单库数据量 < 100GB > 100GB > 100GB

5.5 分库分表带来的代价

代价 分库 分表 说明
跨库/跨表 JOIN 严重 中等 分库后无法直接 JOIN,需应用层组装
分布式事务 严重 轻微 分库后跨库事务需 XA/TCC/Saga
运维复杂度 多实例监控、备份、升级
全局唯一 ID 需要 需要 自增主键不再全局唯一
数据迁移 复杂 复杂 扩容时需重新分布数据
跨库/跨表查询 全库扫描 全表扫描 不带分片键的查询性能极差

六、拆分维度

6.1 垂直拆分 vs 水平拆分

垂直拆分:按 列/业务模块 切分
水平拆分:按 行/数据记录 切分

垂直分表(按列拆分)

flowchart TBWide["user 表(宽表,100+ 字段)<br/>行大小 > 4KB"]Hot["user_basic(热数据)<br/>id, name, phone<br/>频繁查询,常驻内存"]Cold["user_extra(冷数据)<br/>id, address, bio<br/>偶尔查询,可压缩归档"]Wide -->|垂直拆分| HotWide -->|垂直拆分| Cold

适用场景:单行数据过大(含 TEXT/BLOB),热冷字段访问频率差异大。

垂直分库(按业务模块拆分)

flowchart TBMono["单体数据库<br/>user | order | product | payment"]UDB["user_db"]ODB["order_db"]PDB["product_db"]Mono -->|垂直分库| UDBMono -->|垂直分库| ODBMono -->|垂直分库| PDB

适用场景:业务模块耦合严重,单库连接数/IO/CPU 瓶颈。

水平分表(按行拆分,单库内)

flowchart TBBig["user 表<br/>1 亿行"]U0["user_0<br/>2500 万行"]U1["user_1<br/>2500 万行"]U2["user_2<br/>2500 万行"]U3["user_3<br/>2500 万行"]Big -->|"user_id % 4 = 0"| U0Big -->|"user_id % 4 = 1"| U1Big -->|"user_id % 4 = 2"| U2Big -->|"user_id % 4 = 3"| U3

适用场景:单表数据量过大,但单库连接数/IO 尚可承受。

水平分库分表(按行拆分,多实例)

适用场景:数据量 + 并发双重瓶颈。

6.2 四种拆分方式对比

方式 解决问题 复杂度 典型场景
垂直分表 单行数据过大 热/冷字段分离
垂直分库 单库连接数/IO 瓶颈 业务模块解耦
水平分表 单表数据量过大 同库内拆行
水平分库分表 数据量 + 并发双重瓶颈 海量数据高并发

七、分片策略(水平拆分核心)

7.1 分片键选择原则

分片键(Shard Key)是分表成败的关键,必须遵循:

  1. 高频查询覆盖:分片键必须覆盖 90%+ 的查询场景,避免全分片扫描
  2. 数据均匀分布:分片键取值必须均匀,避免用性别、状态等低基数字段
  3. 不可变更:分片键值一旦写入不可修改,否则需跨分片迁移数据

常见分片键user_idorder_idaccountshop_id

7.2 五种分片策略

策略一:Hash 取模分片

table_index = hash(shard_key) % N示例:hash("player_abc") % 100 = 52 → 写入 account_login52
优点 缺点
数据分布非常均匀 扩容极其复杂(N 变化后所有数据需重新分布)
实现简单直观 不支持范围查询
查询路由快速(O(1) 计算) 扩容时数据迁移量大(几乎全量迁移)

扩容问题:原来 4 张表 hash(key) % 4,扩容到 8 张表 hash(key) % 8,约 75% 的数据需要迁移。

适用场景:分表数量固定、不需要频繁扩容的场景(如游戏按区服分表)。


策略二:范围分片(Range Sharding)

user_id 1 ~ 1000 万       → user_0
user_id 1000 万 ~ 2000 万 → user_1
user_id 2000 万 ~ 3000 万 → user_2
优点 缺点
扩容简单(新增范围即可,无需迁移历史数据) 容易数据倾斜和热点(最新分片承担 90% 读写)
支持范围查询 数据分布不均匀
便于数据归档 分片键必须是递增型

适用场景:日志表、订单流水表等时间序列数据。


策略三:一致性哈希分片(Consistent Hashing)

将分片键和分片节点都映射到 0~2^32 的哈希环上,数据顺时针路由到最近的节点。引入虚拟节点解决数据倾斜。

优点 缺点
扩容时只需迁移相邻节点数据(影响范围小) 实现复杂
数据分布较均匀(配合虚拟节点) 范围查询不支持
平滑扩缩容 节点数少时可能分布不均

扩容对比:Hash 取模 4→8 张表需迁移 ~75% 数据;一致性哈希 4→5 张表只需迁移 ~25%。

适用场景:缓存分片(Redis Cluster)、需要频繁扩缩容的场景。


策略四:枚举/查表分片(Directory-Based)

维护一张分片映射表,记录每条数据属于哪个分片。查询时先查映射表,再路由到目标分片。

优点 缺点
灵活度最高,可任意调整映射关系 多一次查询(映射表查询),有性能开销
扩容只需修改映射表 映射表本身成为单点和瓶颈

适用场景:按地域、按业务线分片,分片规则不规则。


策略五:复合分片

组合多种分片策略,先按一个维度粗分,再按另一个维度细分。

第一层:按 ZoneId 分库(枚举分片)
第二层:按 UserId 分表(Hash 取模)最终:db_1.table_3, db_2.table_7 ...
优点 缺点
精细控制数据分布 实现和运维复杂度最高
可同时满足多种查询模式 跨分片查询更复杂

适用场景:超大规模系统(如游戏多区服 + 多分表)。

7.3 五种策略总对比

策略 数据均匀性 扩容难度 范围查询 实现复杂度 典型场景
Hash 取模 高(几乎全量迁移) 不支持 游戏分表、用户表
范围分片 差(易倾斜) 低(新增区间) 支持 日志、订单流水
一致性哈希 低(相邻迁移) 不支持 缓存、弹性扩缩容
枚举/查表 可控 低(改映射) 取决于规则 地域分片、业务线分片
复合分片 可控 部分支持 最高 超大规模多维度

八、分表后的核心挑战

8.1 分布式 ID 生成

分表后自增主键不再全局唯一,需要分布式 ID 方案:

方案 原理 优点 缺点
UUID 随机生成 128 位 简单,无需协调 无序,索引性能差,存储大
Snowflake 雪花算法 时间戳+机器ID+序列号 有序,高性能 依赖时钟,时钟回拨有问题
号段模式 数据库预分配号段 简单可靠 依赖数据库,有号段浪费
Redis INCR Redis 原子自增 高性能 依赖 Redis 可用性

8.2 跨分片查询

不带分片键的查询会触发全分片扫描,性能急剧下降:

-- 带分片键:O(1) 定位
SELECT * FROM account_login52 WHERE Account = 'xxx';-- 不带分片键:全分片扫描,100 张表全查一遍
SELECT * FROM account_login* WHERE UserId = 12345;

解决方案

方案 说明
建立反向索引表 维护 UserId → Account 的映射,先查映射再查分表
冗余宽表 将常用查询字段冗余到 ES 或汇总表
强制带分片键查询 业务层限制查询必须带分片键

8.3 跨分片事务

分表后无法使用本地事务保证跨表一致性:

方案 一致性 性能 复杂度
XA 两阶段提交 强一致 低(锁等待长)
TCC(Try-Confirm-Cancel) 最终一致
本地消息表 最终一致
Saga 长事务 最终一致

8.4 扩容与数据迁移

Hash 取模扩容是最复杂的场景,常见方案:

方案 停机时间 复杂度
双倍扩容(停机迁移)
预分片(一次性建好 1024 张表,映射到少量物理库)
一致性哈希平滑迁移
成倍翻倍法(4→8→16,每次翻倍只迁移一半)

预分片方案详解(推荐):

逻辑分表:1024 张(一次性建好)
物理库:4 个(初期每个库承载 256 张逻辑表)扩容时:4 库 → 8 库
只需将每个库的 128 张逻辑表迁移到新库,无需重新计算 Hash
因为 hash(key) % 1024 的结果不变,只是逻辑表到物理库的映射变了

九、分表方案选型

9.1 三类实现方案对比

方案 代表 优点 缺点 适用场景
客户端分片(应用层) ShardingSphere-JDBC 无额外网络开销,性能最高 分片规则分散,多语言需各自实现 单一技术栈、性能要求极高
代理层分片 ShardingSphere-Proxy、MyCat 多语言透明接入,集中管理 多一跳网络转发,代理需高可用 多语言、大规模集群
分布式数据库 TiDB、OceanBase 完全透明,自动分片 成本高,厂商绑定 不想维护分片架构

9.2 选型决策树

flowchart TDA{"是否需要分表?"}A -->|"否(单表 < 1000 万)"| B["优化索引/读写分离/缓存"]A -->|是| C{"单库能扛住并发?"}C -->|是| D["只分表(水平分表)"]C -->|否| E["分库分表"]E --> F{"技术栈单一?"}F -->|是| G["应用层分片(性能最优)"]F -->|否| H{"多语言?"}H -->|是| I["代理层分片"]H -->|否| J["分布式数据库"]

十、最佳实践总结

  1. 能不分就不分:优先索引优化、读写分离、缓存,分表是最后手段
  2. 分片键决定成败:覆盖 90%+ 查询、数据均匀、不可变
  3. 预分片优于后分片:一次性建好足够多的逻辑表,避免扩容时重新 Hash
  4. 代理键做主键:用无业务含义的自增 int 做聚簇主键,业务查询走二级索引
  5. 避免跨分片查询:建立反向索引、冗余宽表、强制带分片键
  6. 扩容方案提前设计:Hash 取模用成倍翻倍法或预分片,范围分片直接加区间
  7. 监控数据倾斜:定期检查各分片数据量,发现倾斜及时调整
http://www.jsqmd.com/news/988327/

相关文章:

  • 2026年6月市场专业的悬臂焊接机器人供应商哪家专业,埋弧焊机器人/电力焊接机器人,悬臂焊接机器人厂家找哪家 - 品牌推荐师
  • MySQL字段里存了‘a,b,c’?教你用SUBSTRING_INDEX和REPLACE函数搞定拆分与精准查询
  • 五条超级智能实现路径的技术可行性分析框架
  • 多维聚合中的数据操纵:从OLAP立方体到CEO驾驶舱的四层解剖
  • 从OpenJudge一道题出发,聊聊C++里处理字符串输入的那些“坑”与技巧
  • 不止是列表:用RimWorld的Def系统设计你的第一个原创事件(IncidentDef实战)
  • 告别手动造数据:用SystemVerilog的$fscanf和$fwrite自动化你的测试平台
  • 告别AP直连:用华为AC+交换机搭建可扩展的无线办公网(隧道转发详解)
  • 2026年6月最新版宿迁第三方CMACNAS甲醛检测治理机构口碑名单:万清CMA检测中心等5家公司深度测评万清CMA检测中心TOP1推荐 - 一休咨询
  • 全国头部项目代建公司排行及收费标准实测对比 - 起跑123
  • 告别卡顿:用tiffslide和OME-TIFF金字塔优化你的病理图像查看体验
  • 保姆级教程:用STM32G431RB一块板子搞定编码器T法测速全流程测试(含CubeMX配置)
  • 别再只会用插值了!用PyTorch的PixelShuffle层实现更自然的图像超分辨率
  • 机器人电子皮肤:工业级触觉感知系统设计与落地实践
  • ggplot2分面进阶:用ggh4x包的facetted_pos_scales函数优雅定制每个面板的坐标轴
  • SAP CO-PA实战:手把手教你用KE32给获利能力报告新增自定义维度Z003
  • 工业视觉选型笔记:为什么我们项目最终选了MIL而不是Halcon?聊聊安装配置那些事
  • 上海企业搬迁公司推荐:主流厂商对比参考 - 资讯快报
  • 2026年6月伺服冲床企业选哪家,25吨伺服模切冲床/片材伺服模切冲床/小吨位伺服冲床,伺服冲床厂家哪家权威 - 品牌推荐师
  • 别再被‘Command not found’卡住!手把手教你为ZYNQ开发板安装arm-linux-gnueabihf-gcc交叉编译器
  • 2026年条码扫描器经销商/厂家推荐榜:斑马、摩托罗拉、霍尼韦尔、新大陆等品牌手持/无线/工业扫描器深度测评与选购指南 - 品牌发掘
  • 从‘流感传染’到‘图搜索’:用C++队列优化算法,带你吃透NOI/OpenJudge经典题
  • 省内寄快递省钱攻略:怎么收费、哪家便宜、怎么寄更划算 - 快递物流资讯
  • VScode插件失效?IAR工程识别不了?手把手教你排查iar-vsc.json与setting.json配置问题
  • 生产级多维聚合:从Pandas groupby到业务语义建模
  • 别再只懂Deployment了!用K8S探针(Liveness/Readiness/Startup)和优雅停机,给你的Spring Boot应用上双保险
  • 用Presto时间函数搞定业务报表:周环比、月同比、季度初计算实战
  • 从论文到代码:手把手复现2022年顶会PolyWorld建筑提取模型(附数据集下载)
  • 当LabVIEW遇上MATLAB分类模型:手把手教你用DLL封装SVM/决策树并可视化结果
  • AI伦理使用四重校验法:从提示到署名的责任实践框架