Skip to content

第35章 分区表与复制 ​

本章目标 ​

  1. 掌握 MySQL 四种分区类型(RANGE、LIST、HASH、KEY)的语法和应用场景
  2. 理解分区裁剪(Partition Pruning)的原理和 EXPLAIN 验证方法
  3. 掌握分区的日常管理操作(ADD、DROP、TRUNCATE、REORGANIZE PARTITION)
  4. 深入理解 MySQL 主从复制的架构和工作原理
  5. 掌握复制的配置步骤和常见问题的排查方法
  6. 理解 GTID 复制的优势和配置方法
  7. 掌握读写分离的架构和实现策略

前置知识 ​

  • 熟练掌握 CREATE TABLE 和 ALTER TABLE
  • 理解 InnoDB 存储引擎的基本原理
  • 了解二进制日志(Binlog)的基本概念和作用
  • 有一定的 Linux 运维经验

35.1 分区表 ​

35.1.1 分区的概念和优势 ​

概念:
  一个逻辑表物理上被分割为多个分区文件(.ibd),
  但对应用层来说仍然是一张表。

+------------------------------------------------+
|  逻辑表: orders                                  |
|  +--------------------------------------------+ |
|  | PARTITION p2022  | 2022年的数据              | |
|  | PARTITION p2023  | 2023年的数据              | |
|  | PARTITION p2024  | 2024年的数据              | |
|  +--------------------------------------------+ |
+------------------------------------------------+

检查分区文件:
$ ls /var/lib/mysql/mydb/orders#p#p2022.ibd
$ ls /var/lib/mysql/mydb/orders#p#p2023.ibd
$ ls /var/lib/mysql/mydb/orders#p#p2024.ibd

分区的三大优势:

sql
-- 1. 查询裁剪(Partition Pruning)
-- 只扫描相关分区,不扫描不需要的分区
SELECT * FROM orders WHERE created_at >= '2024-01-01';
-- 自动只扫描 p2024 分区

EXPLAIN SELECT * FROM orders WHERE created_at >= '2024-01-01';
-- partitions 列显示:p2024(而非 p2022,p2023,p2024)

-- 2. 数据生命周期管理
-- 删除 2022 年的数据(ALTER TABLE ... DROP PARTITION 是元数据操作,毫秒级完成)
ALTER TABLE orders DROP PARTITION p2022;
-- 对比:DELETE FROM orders WHERE YEAR(created_at) = 2022;
--     需要扫描、删除、产生 Undo Log、锁表,可能需要数小时!

-- 3. 分散 I/O
-- 不同分区的物理文件可放置在不同磁盘上
ALTER TABLE orders
  PARTITION BY RANGE (YEAR(created_at)) (
    PARTITION p2022 VALUES LESS THAN (2023) DATA DIRECTORY = '/ssd1',
    PARTITION p2023 VALUES LESS THAN (2024) DATA DIRECTORY = '/ssd2',
    PARTITION p2024 VALUES LESS THAN (2025) DATA DIRECTORY = '/ssd3'
  );

35.1.2 四种分区类型 ​

RANGE 分区(范围分区,最常用):

sql
-- 按创建年份分区
CREATE TABLE orders (
  id INT NOT NULL,
  user_id INT NOT NULL,
  total_amount DECIMAL(10,2),
  created_at DATE NOT NULL,
  PRIMARY KEY (id, created_at)  -- 注意:主键必须包含分区键!
)
PARTITION BY RANGE (YEAR(created_at)) (
  PARTITION p2021 VALUES LESS THAN (2022),
  PARTITION p2022 VALUES LESS THAN (2023),
  PARTITION p2023 VALUES LESS THAN (2024),
  PARTITION p2024 VALUES LESS THAN (2025),
  PARTITION p_future VALUES LESS THAN MAXVALUE  -- 兜底分区(超过2024的数据流入这里)
);

-- RANGE COLUMNS:可以直接使用 DATE 类型,不需要函数
CREATE TABLE orders_v2 (
  id INT NOT NULL,
  user_id INT NOT NULL,
  total_amount DECIMAL(10,2),
  created_at DATE NOT NULL,
  PRIMARY KEY (id, created_at)
)
PARTITION BY RANGE COLUMNS(created_at) (
  PARTITION p2024_q1 VALUES LESS THAN ('2024-04-01'),
  PARTITION p2024_q2 VALUES LESS THAN ('2024-07-01'),
  PARTITION p2024_q3 VALUES LESS THAN ('2024-10-01'),
  PARTITION p2024_q4 VALUES LESS THAN ('2025-01-01')
);

LIST 分区(列表分区):

sql
-- 按区域分片
CREATE TABLE regional_sales (
  id INT NOT NULL,
  region_id INT NOT NULL,
  amount DECIMAL(10,2),
  sale_date DATE NOT NULL,
  PRIMARY KEY (id, region_id)
)
PARTITION BY LIST (region_id) (
  PARTITION p_north VALUES IN (1, 2, 3),
  PARTITION p_south VALUES IN (4, 5, 6),
  PARTITION p_east VALUES IN (7, 8),
  PARTITION p_west VALUES IN (9, 10)
);

-- LIST COLUMNS:可以使用字符串类型
CREATE TABLE users_by_country (
  id INT NOT NULL,
  country_code CHAR(2) NOT NULL,
  name VARCHAR(100),
  PRIMARY KEY (id, country_code)
)
PARTITION BY LIST COLUMNS(country_code) (
  PARTITION p_asia VALUES IN ('CN', 'JP', 'KR', 'SG'),
  PARTITION p_europe VALUES IN ('GB', 'DE', 'FR', 'IT'),
  PARTITION p_americas VALUES IN ('US', 'CA', 'BR', 'MX')
);

HASH 分区(哈希分区,均匀分布):

sql
-- 按用户 ID 哈希,将数据均匀分布到 4 个分区
CREATE TABLE user_events (
  id BIGINT NOT NULL,
  user_id INT NOT NULL,
  event_type VARCHAR(50),
  event_data JSON,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id, user_id)
)
PARTITION BY HASH (user_id)
PARTITIONS 4;

-- HASH 分区的优势:
-- 1. 数据均匀分布(避免热点分区)
-- 2. 写入压力被分散到多个分区文件
-- 3. 指定 user_id 查询时可以裁剪到 1 个分区

-- 线性 HASH(LINEAR HASH):添加/合并分区时数据移动更少
PARTITION BY LINEAR HASH (user_id)
PARTITIONS 4;

KEY 分区(和 HASH 类似,但由 MySQL 计算哈希):

sql
-- KEY 分区适用于非整型列
CREATE TABLE sessions (
  session_id VARCHAR(128) NOT NULL,
  user_id INT,
  data TEXT,
  created_at DATETIME,
  PRIMARY KEY (session_id)
)
PARTITION BY KEY (session_id)
PARTITIONS 8;

-- KEY 分区的特点:
-- 1. MySQL 使用自己的哈希算法(不是用户可控的)
-- 2. 可以用于字符串等非整型列
-- 3. 如果未指定分区键,默认使用主键

35.1.3 分区表的核心限制 ​

sql
-- 限制 1:主键/唯一键必须包含所有分区键
-- 正确:
CREATE TABLE t (
  id INT,
  created_at DATE NOT NULL,
  PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (YEAR(created_at)) (...);

-- 错误:
CREATE TABLE t (
  id INT PRIMARY KEY,         -- 主键只有 id
  created_at DATE NOT NULL
) PARTITION BY RANGE (YEAR(created_at)) (...);
-- ERROR 1503: A PRIMARY KEY must include all columns in the table's partitioning function

-- 限制 2:不支持外键(外键引用的表不能是分区表)
-- 分区表不能有外键约束

-- 限制 3:不支持 FULLTEXT 索引

-- 限制 4:最大 8192 个分区(MySQL 8.0)

-- 限制 5:没有全局唯一索引(唯一索引必须包含分区键)
-- 这意味着不能创建不在分区键上的唯一列!

35.1.4 分区管理 ​

sql
-- 查看分区信息
SELECT
  PARTITION_NAME,
  PARTITION_ORDINAL_POSITION,
  TABLE_ROWS,
  DATA_LENGTH,
  INDEX_LENGTH,
  PARTITION_DESCRIPTION
FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA = 'mydb' AND TABLE_NAME = 'orders';

-- 添加分区
ALTER TABLE orders ADD PARTITION (
  PARTITION p2025 VALUES LESS THAN (2026)
);

-- 删除分区(数据也被删除!)
ALTER TABLE orders DROP PARTITION p2021;
-- 注意:DROP PARTITION 是即时操作(只修改元数据),不是逐行删除

-- 截断分区(删除数据但保留分区定义)
ALTER TABLE orders TRUNCATE PARTITION p2024;

-- 重组分区(合并/拆分分区)
-- 将 p2021 和 p2022 合并为 p_archive
ALTER TABLE orders REORGANIZE PARTITION p2021, p2022 INTO (
  PARTITION p_archive VALUES LESS THAN (2023)
);

-- 拆分分区
ALTER TABLE orders REORGANIZE PARTITION p2024 INTO (
  PARTITION p2024_h1 VALUES LESS THAN (20240701),
  PARTITION p2024_h2 VALUES LESS THAN (20250101)
);

-- 注意:
-- 1. REORGANIZE PARTITION 会锁表并复制数据,大分区耗时很长
-- 2. 如果有 MAXVALUE 分区,不能直接 ADD PARTITION,
--    需要用 REORGANIZE 拆分 MAXVALUE 分区

-- 重建分区(碎片整理)
ALTER TABLE orders REBUILD PARTITION p2024;

-- 修复分区
ALTER TABLE orders REPAIR PARTITION p2024;

-- 检查分区
ALTER TABLE orders CHECK PARTITION p2024;

-- 优化分区(等价于在分区上执行 OPTIMIZE TABLE)
ALTER TABLE orders OPTIMIZE PARTITION p2024;

35.1.5 分区策略选择指南 ​

+-------------------+---------------------+---------------------------+
| 分区类型            | 适合场景             | 典型例子                    |
+-------------------+---------------------+---------------------------+
| RANGE             | 时间序列数据          | 订单表按年/月分区            |
| LIST              | 离散分类数据          | 按国家/地区分区              |
| HASH              | 均匀分布(无业务语义)  | 用户表按 user_id 哈希分区    |
| KEY               | 字符串列的哈希分布     | 会话表按 session_id 分区    |
+-------------------+---------------------+---------------------------+

选择建议:
- 有时间维度的 → RANGE 分区(最常用)
- 有明确分类的 → LIST 分区
- 仅需要分散 I/O → HASH/KEY 分区
- 分区数不是越多越好:100-500 个分区通常足够
- 大多数业务用 RANGE + 时间维度即可覆盖 80% 的拆分需求

35.2 MySQL 主从复制 ​

35.2.1 复制架构 ​

+------------------+          +------------------+
|    Master        |          |    Slave         |
|   (主库/写库)    |          |   (从库/读库)    |
+------------------+          +------------------+
|                  |          |                  |
| Binary Log       |  ──────> | Relay Log        |
| (binlog)         |  网络复制  | (relay log)      |
|                  |          |                  |
| SQL Thread       |          | IO Thread        |
| (写binlog)       |          | (读binlog→relay) |  ← 从库IO线程
|                  |          |                  |
|                  |          | SQL Thread       |
|                  |          | (中继日志→数据)    |  ← 从库SQL线程
+------------------+          +------------------+

复制流程:
1. Master 将数据变更写入 Binary Log
2. Slave 的 IO Thread 连接到 Master,读取 Binary Log
3. Slave 的 IO Thread 将读取的 binlog 事件写入本地的 Relay Log
4. Slave 的 SQL Thread 读取 Relay Log,执行其中的事件
5. 循环往复,保持数据同步

35.2.2 复制模式 ​

异步复制(默认):

text
Master 写入 binlog 后立即返回客户端,不等 Slave 确认。

优点:性能好,Master 不受 Slave 状态影响
缺点:如果 Master 崩溃,未传输到 Slave 的 binlog 会丢失

半同步复制(Semi-Synchronous):

text
Master 写入 binlog 后,等待至少一个 Slave 确认收到后才返回客户端。

优点:减少了数据丢失的风险
缺点:增加了写入延迟(等待网络往返)

# 配置半同步
# Master 和 Slave 都需要安装插件
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';

SET GLOBAL rpl_semi_sync_master_enabled = ON;
SET GLOBAL rpl_semi_sync_slave_enabled = ON;

# 查看状态
SHOW STATUS LIKE 'Rpl_semi_sync%';

35.2.3 复制搭建步骤(基于日志位置) ​

在 Master 上:

sql
-- 1. 创建复制专用账号
CREATE USER 'repl'@'slave_ip' IDENTIFIED BY 'repl_password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'slave_ip';
FLUSH PRIVILEGES;

-- 2. 获取当前 binlog 位置
SHOW MASTER STATUS;
-- +-------------------+----------+--------------+------------------+
-- | File              | Position | Binlog_Do_DB | Binlog_Ignore_DB |
-- +-------------------+----------+--------------+------------------+
-- | binlog.000010     |      1234 |              |                  |
-- +-------------------+----------+--------------+------------------+

-- 3. 导出数据(如果 Slave 从零开始)
mysqldump -u root -p --single-transaction \
  --master-data=2 --all-databases > master_dump.sql

配置文件设置:

ini
# Master 的 my.cnf
[mysqld]
server-id = 1                          # 必须全局唯一
log_bin = /var/log/mysql/binlog        # 必须开启 binlog
binlog_format = ROW
binlog_expire_logs_seconds = 604800
sync_binlog = 1

# Slave 的 my.cnf
[mysqld]
server-id = 2                          # 必须与 Master 不同
relay_log = /var/log/mysql/relaylog    # 中继日志路径
read_only = ON                         # 从库只读(推荐)
log_bin = /var/log/mysql/binlog        # 可选:如果此 Slave 也将是其他 Slave 的 Master
log_slave_updates = ON                 # 可选:将复制的变更也写入自己的 binlog

在 Slave 上执行:

sql
-- MySQL 8.0.23+ 新语法
CHANGE REPLICATION SOURCE TO
  SOURCE_HOST = 'master_host',
  SOURCE_PORT = 3306,
  SOURCE_USER = 'repl',
  SOURCE_PASSWORD = 'repl_password',
  SOURCE_LOG_FILE = 'binlog.000010',
  SOURCE_LOG_POS = 1234;

-- MySQL 8.0.23 之前(旧语法,仍可用)
CHANGE MASTER TO
  MASTER_HOST = 'master_host',
  MASTER_PORT = 3306,
  MASTER_USER = 'repl',
  MASTER_PASSWORD = 'repl_password',
  MASTER_LOG_FILE = 'binlog.000010',
  MASTER_LOG_POS = 1234;

-- 启动复制
START REPLICA;  -- 新语法
-- START SLAVE;  -- 旧语法(仍可用)

-- 检查复制状态
SHOW REPLICA STATUS\G
-- 关键字段:
-- Slave_IO_Running: Yes      ← IO 线程正常
-- Slave_SQL_Running: Yes     ← SQL 线程正常
-- Seconds_Behind_Master: 0   ← 延迟(0=无延迟,理想状态)
-- Last_IO_Error: ''           ← IO 线程错误
-- Last_SQL_Error: ''          ← SQL 线程错误

35.2.4 复制状态监控 ​

sql
-- 综合监控查询
SELECT
  @@server_id AS server_id,
  @@read_only AS read_only,
  CONNECTION_NAME,
  CHANNEL_NAME,
  SERVICE_STATE,
  RECEIVED_TRANSACTION_SET,
  LAST_ERROR_NUMBER,
  LAST_ERROR_MESSAGE,
  LAST_ERROR_TIMESTAMP
FROM performance_schema.replication_connection_status;

-- 查看复制延迟
SHOW REPLICA STATUS\G
-- Seconds_Behind_Master: 0

-- 如果延迟增大,检查:
-- 1. 是否有大事务阻塞了 SQL 线程
-- 2. 网络是否正常(ping, iperf)
-- 3. Slave 磁盘 I/O 是否成为瓶颈
-- 4. Slave 是否也在承担大量读请求

-- 查看复制过滤规则
SHOW REPLICA STATUS\G
-- Replicate_Do_DB:
-- Replicate_Ignore_DB:

35.2.5 GTID 复制(全球事务 ID) ​

GTID 的优势:

传统复制(基于日志+位置):
  CHANGE MASTER TO MASTER_LOG_FILE='binlog.000010', MASTER_LOG_POS=1234;
  → 需要手动找到正确的位置
  → 切换 Master(failover)时容易出错

GTID 复制:
  CHANGE REPLICATION SOURCE TO SOURCE_AUTO_POSITION = 1;
  → MySQL 自动找到需要复制的下一个事务!
  → failover 时自动从正确位置开始

配置 GTID 复制:

ini
# Master 和 Slave 都需要配置
[mysqld]
server-id = 1
gtid_mode = ON
enforce_gtid_consistency = ON   # 确保所有语句都能安全地使用GTID
log_bin = /var/log/mysql/binlog
log_slave_updates = ON
sql
-- 查看 GTID 执行状态
SHOW MASTER STATUS\G
-- Executed_Gtid_Set: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:1-100

-- Slave 上配置(使用 GTID 自动定位)
CHANGE REPLICATION SOURCE TO
  SOURCE_HOST = 'master_host',
  SOURCE_PORT = 3306,
  SOURCE_USER = 'repl',
  SOURCE_PASSWORD = 'repl_password',
  SOURCE_AUTO_POSITION = 1;       -- 关键:自动位置

-- GTID 跳过一个错误的事务(当从库因某事务而出错时)
-- 方法1:注入空事务(最安全)
SET GTID_NEXT = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:101';
BEGIN; COMMIT;
SET GTID_NEXT = 'AUTOMATIC';

-- 方法2:直接设置(但不推荐)
STOP REPLICA;
SET GTID_NEXT = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:101';
BEGIN; COMMIT;
SET GTID_NEXT = 'AUTOMATIC';
START REPLICA;

35.2.6 读写分离策略 ​

架构方案1:应用层读写分离(代码实现)
  - 应用代码中使用 AOP/中间件识别读写
  - @Master → 走主库(INSERT/UPDATE/DELETE/SELECT ... FOR UPDATE)
  - @Slave → 走从库(普通 SELECT)
  - 需处理复制延迟(刚写入的数据可能还没复制到从库)

架构方案2:代理中间件
  - ProxySQL, MySQL Router, MaxScale
  - 对应用透明,通过端口区分读写
  - 支持负载均衡、故障转移、查询路由

架构方案3:数据库中间件
  - ShardingSphere, MyCat
  - 同时支持读写分离和分库分表
  - 功能最强大,但运维复杂度也最高
sql
-- 复制延迟处理的关键 SQL
-- 场景:用户刚下单,立刻跳转到订单详情页

-- 方案 1:关键读操作强制走主库(如订单详情页)
-- 在代码中标记此类查询为 @Master

-- 方案 2:从库读时检查是否可达(应用层实现)
-- SELECT * FROM orders WHERE id = 123;  -- 从库
-- if (result == null) { 查询主库 }

-- 方案 3:检查从库延迟,超过阈值则走主库
SHOW REPLICA STATUS;
-- if Seconds_Behind_Master > 1 → 走主库

常见错误 ​

错误 1:MaxValue 分区导致无法添加新分区 ​

问题:创建了 PARTITION p_max VALUES LESS THAN MAXVALUE 后,
     想再添加 2026 年的分区时:
     ALTER TABLE orders ADD PARTITION (PARTITION p2026 VALUES LESS THAN (2027));
     → 错误:MAXVALUE can only be used in last partition definition

解决:
  不能 ADD,需要用 REORGANIZE 拆分 maxvalue 分区:
  ALTER TABLE orders REORGANIZE PARTITION p_max INTO (
    PARTITION p2025 VALUES LESS THAN (2026),
    PARTITION p_max VALUES LESS THAN MAXVALUE
  );

错误 2:分区键不在主键/唯一键中 ​

问题:
  CREATE TABLE t (
    id INT AUTO_INCREMENT PRIMARY KEY,
    text VARCHAR(100),
    created_at DATE NOT NULL
  ) PARTITION BY RANGE (YEAR(created_at)) (...);
  → ERROR 1503: A PRIMARY KEY must include all columns in the partitioning function

解决:
  ALTER TABLE t DROP PRIMARY KEY, ADD PRIMARY KEY (id, created_at);
  -- 或者不使用分区

错误 3:复制延迟时强制读从库导致数据不一致 ​

问题:用户下单后跳转到订单详情页面,数据还没复制到从库,页面显示"订单不存在"。

解决:
  1. 刚写入后的读操作强制走主库
  2. 在应用层设置"刚写入的 Key"缓存到 Redis(TTL 几秒),读时先检查
  3. 复制延迟监控告警,延迟过高时暂时将所有读切回主库

错误 4:忘记分配唯一的 server-id ​

问题:搭建从库时忘记设置 server-id 或设为与主库相同的值,
     复制无法正常工作。

解决:
  每个 MySQL 实例必须有全局唯一的 server-id:
  -- 主库:server-id = 1
  -- 从库1:server-id = 2
  -- 从库2:server-id = 3
  -- ...
  -- 不能重复!

错误 5:使用 Replicate_Do_DB 导致的"意外" ​

问题:在 Slave 上设置 Replicate_Do_DB = 'mydb',
     但在 Master 上执行 USE otherdb; INSERT INTO mydb.orders ... 
     时,这个 INSERT 不会被复制!因为 Replicate_Do_DB 只根据
     当前的默认数据库(USE)判断,不检查被操作的表属于哪个库。

建议:使用 Replicate_Wild_Do_Table = 'mydb.%' 代替,
     它基于被操作的表名判断。

本章练习 ​

练习 1:RANGE 分区实践 ​

  1. 创建 orders 表,按年进行 RANGE 分区(2020-2025)
  2. 插入跨年份的测试数据
  3. 使用 EXPLAIN 验证分区裁剪是否生效
  4. 添加 2026 年分区
  5. 删除 2020 年分区(与 DELETE 操作对比耗时)
  6. 查看 information_schema.PARTITIONS 获取各分区的行数统计

练习 2:主从复制环境搭建 ​

  1. 在同一台机器上使用不同的端口和数据目录搭建 1 主 + 2 从
  2. 配置 GTID 复制
  3. 测试:在主库插入数据,验证从库同步
  4. 测试:在从库尝试写入(验证 read_only 效果)
  5. 模拟主库故障,手动将从库提升为主库
  6. 配置半同步复制,对比异步和半同步的写入延迟

练习 3:复制故障排查 ​

  1. 故意在从库上删除某行数据,然后在主库更新该行
  2. 观察复制中断(SQL 线程报错)
  3. 修复错误并重启复制
  4. 使用 GTID 跳过错误事务
  5. 记录排查过程和常用诊断命令

练习 4:分区类型对比 ​

  1. 分别创建 RANGE、LIST、HASH、KEY 分区的表
  2. 每种分区插入 100 万行测试数据
  3. 对比不同分区类型在以下场景的性能:
    • 按分区键精确查询
    • 跨分区范围查询
    • 插入性能
    • 删除整分区 vs 删除数据行

练习 5:复制延迟场景 ​

  1. 搭建主从复制环境
  2. 在主库执行大量 UPDATE 操作(模拟大事务)
  3. 观察从库的 Seconds_Behind_Master 的增长
  4. 实现应用层的读写分离逻辑(读从库 + 关键读主库)
  5. 测试复制延迟 > 1s 时自动切换读主库

练习 6:综合架构设计 ​

设计一个支持 100 万日活用户的数据库架构方案:

  1. 数据量:用户表 500 万、订单表 5 亿、订单明细表 20 亿
  2. 设计分区方案(订单表按时间分区)
  3. 设计分库分表方案(用户维度分库)
  4. 设计读写分离方案(1 主 + N 从)
  5. 画出完整的架构图,标注数据流向
  6. 估算硬件资源需求

把零散知识,整理成可以反复走的路。