3891 字
19 分钟
MySQL 高可用详解(1)主从复制与分库分表

本文行文:什么是主从结构,为什么需要主从结构 → 主从复制方法 → 复制延迟怎么处理 → 单表过大怎么处理

读写分离#

MySQL 高可用的第一块基石是主从结构:一个主库(Primary / Master)承担写入,一个或多个从库(Replica / Slave)持续从主库同步数据。

需要主从结构的原因有三条:

目标说明
可用性主库故障时,从库上有全量数据,可以顶上来(见第二篇《故障转移》)
读扩展多数业务读多写少,从库横向扩展读能力,比升级单机更划算
数据安全从库、异地从库本身就是一份在线副本,也是备份的落地点

只有单机时,读写都压在一个实例上:QPS 上不去、慢查询和 DDL 互相干扰、磁盘故障即全量丢数据。读写分离把流量按”写主读从”分流,写路径保持单点串行(避免冲突),读路径可以水平扩展。

分流方式#

方式典型实现优点代价
客户端/连接池路由多数据源、@ReadOnly 注解、动态数据源无额外组件,改动小路由逻辑散在业务代码里
代理层ProxySQL、MySQL Router、HAProxy应用无感,可做健康检查与自动摘除多一跳网络,需要单独运维
数据库中间件ShardingSphere、Vitess、MyCat分库分表与读写分离一起解决复杂度最高,SQL 兼容性有约束

主从、主主、多主结构#

  • 一主多从:写只有一个入口,不存在写冲突,是最省心的形态;缺点是写节点仍是单点(靠切换解决)。
  • 双主(主主):两边都开 binlog 且互为主从,可以做写冗余,但同一行同时被两边写入会产生冲突,通常配合 VIP 只让一个节点对外写。
  • 多主 / 组复制:由集群层做写冲突检测(MGR、Galera),见第二篇。

主从复制#

binlog 方式#

MySQL 的复制是基于 binlog(binary log)的逻辑复制:binlog保存了数据库的增删改事务操作(SQL语句),主库把数据变更写进 binlog,从库拉取后在本地重放。

复制的三条线程#

线程位置职责
Binlog Dump Thread主库读取 binlog 并推送给从库
Replica I/O Thread从库接收 binlog 事件,写入本地 relay log,并记录复制连接信息
(8.0 起存在 mysql.slave_master_info 等表里)
Replica SQL Thread从库读取 relay log 并在从库重放

因此从库的磁盘上会多出 relay log;重放完成后 relay log 会被清理(relay_log_purge=ON),卡住时它会持续增长,是排查复制问题的第一现场。

主从复制流程:#

  1. 连接:Slave 的 IO 线程连接 Master 的 binlog dump 线程,请求获取指定日志文件的指定位置之后的 binlog 日志;
  2. 主库推送:Master 的 binlog dump 线程将指定的 binlog 信息(日志信息+文件名称+位置信息)发给 Slave;
  3. 从库同步:Slave 的 IO 线程接收到信息后将日志内容写入 relay log 文件的末端,并将读取到的 Master 端的 binlog 的文件名和位置记录到master-info,以便下一次读取时能告诉 Master 从何处开始同步日志。
  4. 从库重放:Slave 的 SQL 线程检测到 relay log 新增了内容后解析 Log 文件生成对应的 sql 语句,并应用到数据库。

配置流程#

1. binlog 配置

/etc/my.cnf(主库)
[mysqld]
server_id = 1 # 集群内必须唯一,0 会被拒绝
log_bin = mysql-bin # 开启 binlog
binlog_format = ROW # 高可用场景一律用 ROW
binlog_row_image = FULL # 记录变更前后完整行镜像
sync_binlog = 1 # 每次提交都刷盘,不丢已提交事务
innodb_flush_log_at_trx_commit = 1 # 双 1 配置,崩溃不丢数据
expire_logs_days = 7 # 或用 binlog_expire_logs_seconds

binlog 三种格式的选择:

格式记录内容特点
STATEMENTSQL 原文日志小,但 NOW()、RAND()、LIMIT 无 ORDER BY 等语句主从不一致
ROW每行变更前后镜像确定性强,是复制与数据恢复的推荐格式;日志量大
MIXED由 MySQL 自行选择兼顾体积,但问题定位时不够直观

binlog_format 是会话级可改的,但为了复制链路一致,生产环境统一设为 ROW。

2. 建复制账号

-- 主库执行,最小权限即可
CREATE USER 'repl'@'10.0.0.%' IDENTIFIED BY 'StrongPassw0rd!';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'10.0.0.%';
FLUSH PRIVILEGES;

3. 取起始位点并配置从库

-- 主库:记录当前 binlog 位点(传统方式)
SHOW MASTER STATUS;
-- +------------------+----------+--------------+------------------+
-- | File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
-- +------------------+----------+--------------+------------------+
-- | mysql-bin.000003 | 1571 | | |
-- +------------------+----------+--------------+------------------+
/etc/my.cnf(从库)
[mysqld]
server_id = 2
relay_log = relay-bin
read_only = ON # 普通账号只读(super 与复制线程不受限)
super_read_only = ON # 连 super 也禁写,防止误操作
log_replica_updates = ON # 双主级联时必须开,保证 binlog 连续
-- 从库:8.0 起 CHANGE MASTER TO 已更名为 CHANGE REPLICATION SOURCE TO
CHANGE REPLICATION SOURCE TO
SOURCE_HOST = '10.0.0.11',
SOURCE_PORT = 3306,
SOURCE_USER = 'repl',
SOURCE_PASSWORD = 'StrongPassw0rd!',
SOURCE_LOG_FILE = 'mysql-bin.000003',
SOURCE_LOG_POS = 1571;
START REPLICA;
SHOW REPLICA STATUS\G

从库状态里最需要看懂的几个字段:

字段含义
Replica_IO_RunningIO 线程是否在拉取 binlog,应为 Yes
Replica_SQL_RunningSQL 线程是否在重放 relay log,应为 Yes
Seconds_Behind_Master只表示”当前重放事件的时间戳与主库当前时间差”,会撒谎,见下文
Retrieved_Gtid_Set / Executed_Gtid_Set已拉取 / 已执行的 GTID 集合,判断延迟更可靠
Last_IO_Error / Last_SQL_Error复制中断的直接原因

GTID 方式#

binlog 位点复制的痛点是位点错一下就全错:首次同步、切换、重建从库、级联拓扑变更时都要人工找 File + Position。

GTID(Global Transaction Identifier,全局事务ID)给每个已提交事务发一个全局唯一编号,格式为 source_uuid:transaction_id,例如 3f2b1c8e-...-9a01:1-100。

  • source_uuid:标识事务最初提交的源服务器。通常是 MySQL 实例唯一的 server_uuid,在实例首次启动时生成并保存。
  • transaction_id:一个从 1 开始的自增序列号,代表该事务在源服务器上的提交顺序。

流程:

  • 从库同步:从库连接主库,告知已执行的 GTID 集合。
  • 差集计算:主库计算主库 GTID 集合和从库 GTID 的差集,推送差集事务给从库。
  • 事务应用:从库接收事务并应用,同时记录GTID。

因此,从库只需知道”我执行过哪些事务”,这种机制使复制不再依赖易变的文件名和位置,简化了主从同步和故障切换操作。

配置方式#

/etc/my.cnf(所有节点)
[mysqld]
gtid_mode = ON
enforce_gtid_consistency = ON
log_replica_updates = ON
-- 从库:不再需要 File/Pos,直接自动定位
CHANGE REPLICATION SOURCE TO
SOURCE_HOST = '10.0.0.11',
SOURCE_USER = 'repl',
SOURCE_PASSWORD = 'StrongPassw0rd!',
SOURCE_AUTO_POSITION = 1;
START REPLICA;
-- 观察 GTID 复制进度
SELECT @@GLOBAL.gtid_executed; -- 本机已执行
SELECT @@GLOBAL.gtid_purged; -- 已清理、不再需要的历史
SHOW REPLICA STATUS\G -- Retrieved / Executed / Auto_Position

实际收益#

  • 切换免定位:新主提升后,其他从库改指新主即可,不用去新主上找位点。
  • 可校验一致性:gtid_executed 的集合差异就是”缺了哪些事务”,比对比行数靠谱。
  • 多源复制更清晰:FOR CHANNEL 'ch1' 每个通道各自维护 GTID 进度。

使用约束#

  • 只支持事务型引擎(InnoDB);CREATE TABLE ... SELECT 需拆成 CREATE TABLE + INSERT ... SELECT 两步,显式 CREATE TEMPORARY TABLE 需在 transaction_isolation = READ COMMITTED 的会话中执行(enforce_gtid_consistency=WARN 时会放宽)。
  • gtid_purged 只能追加、不能回退;从库重建时用备份里的 GTID 集合补位:
RESET MASTER;
SET GLOBAL gtid_purged = '3f2b1c8e-...-9a01:1-1000';
CHANGE REPLICATION SOURCE TO ... SOURCE_AUTO_POSITION = 1;
START REPLICA;

复制中断的常见错误#

错误码现象处理思路
1062Duplicate entry 从库已有该行先查清是主从数据漂移还是业务重复写;确认可跳过后用 SET GTID_NEXT='...'; BEGIN; COMMIT; 空事务跳过
1032Can't find record 从库缺行说明从库被人工改过数据或被过滤过,宜用 pt-table-sync 修复或重建从库
1236Could not find first log file name in binary log index file——请求的位点/文件已不存在主库 binlog 被清理过早(expire_logs_days 太小),只能重建从库
2003 / 2013连不上主库网络或主库 max_connections 被打满

跳过事务的正确姿势(GTID 模式):

STOP REPLICA;
SET GTID_NEXT = '3f2b1c8e-...-9a01:1001';
BEGIN; COMMIT;
SET GTID_NEXT = 'AUTOMATIC';
START REPLICA;

主从延迟#

主从复制默认是异步的:主库提交成功即可返回客户端,从库可能还没重放完。

延迟测量#

Seconds_Behind_Master 的取值为”从库 SQL 线程正在重放的事件时间戳”与”从库当前时间”的差值,因此:

  • 主库空闲时它必然为 0,即使从库已经落后了几小时没重放(IO 线程断了它反而变 NULL)。
  • 主从机器时钟不同步时数值无意义。
  • 粒度为整秒,小于1s的延迟均记为0。

更可靠的做法:

方案说明
pt-heartbeat主库定时写心跳表,从库据此算出真实延迟(亚秒级),最常用
GTID 差值比较 Retrieved_Gtid_Set 与 Executed_Gtid_Set,只能定性看”是否落后”
performance_schema.replication_applier_status_by_worker看每个并行 worker 正在处理的事务与耗时
SHOW REPLICA STATUS 的 Relay_Log_Space / Relay_Log_Posrelay log 持续膨胀说明重放追不上

延迟来源#

  1. 天然串行:传统复制只有一个 SQL 线程,主库并发写入会在这里排队堆积。
  2. 大事务:一次性 DELETE 百万行、ALTER TABLE 重建表,重放耗时长且无法拆分。
  3. DDL 与锁:从库重放时如果被长查询/长事务阻塞,整个重放链路停住。
  4. 从库自身压力:从库同时扛着大量读请求,CPU、IO 被抢占。
  5. 硬件与网络:binlog 传输带宽不足,或磁盘 fsync 能力弱于主库。
  6. 行锁竞争:READ COMMITTED 之外的隔离级别下行锁竞争更重,重放更慢。

治理方式#

手段做法效果
并行复制replica_parallel_type=LOGICAL_CLOCK、
replica_parallel_workers=8、
replica_preserve_commit_order=ON
利用主库组提交信息在从库并行重放,是目前最有效的手段
拆大事务大批量操作拆成小批次提交(每批几千行),配合 --limit 循环避免长时间阻塞重放
DDL 变更窗口用 gh-ost / pt-online-schema-change,或先在从库执行再切主减少锁表时间
业务削峰写请求排队、合并批量写入降低主库瞬时并发
提升从库规格从库往往要同时承担读流量和重放,规格不应低于主库直接提升重放能力
半同步rpl_semi_sync_source_timeout + rpl_semi_sync_source_wait_for_replica_count=1控制数据丢失窗口,但换来延迟上升
读写策略写后读走主、会话粘性、容忍度高的报表走从库让业务对延迟不敏感

并行复制相关参数的取舍:

[mysqld]
replica_parallel_type = LOGICAL_CLOCK
replica_parallel_workers = 8 # 建议 4~16,与从库 CPU 核数相关
replica_preserve_commit_order = ON # 保证提交顺序与主库一致,避免并行导致的可见性异常

延迟的兜底思路#

即使做了上述优化,异步复制在极端情况下仍会延迟,因此必须承认:延迟是”降低”而不是”消除”的。工程上的兜底是:

  • 业务侧定义可接受的最大延迟(例如 1 秒),超过就降级为读主库。
  • 监控告警盯住延迟,而不是等业务报错。
  • 切换时校验 GTID,避免”延迟中切换”造成部分事务永丢(详见第二篇)。

分库分表#

主从解决的是”读扩展与可用性”,但写能力和单表数据量仍在单实例上。当单表行数到千万级、单库磁盘或写入 QPS 触顶时,才需要考虑分库分表。

垂直拆分#

按业务域把不同表拆到不同库(用户库、订单库、商品库)。

  • 优点:业务边界清晰、单库压力下降、后续可独立扩容。
  • 代价:跨库 JOIN 消失,需要改为多次查询或数据冗余。

水平拆分#

把同一张表按规则拆成多张(同库多表或分到多库)。

分片方式规则适用
Range按 id 段或时间(按月)日志、订单等有自然时序,便于归档删除;易热点
Hashid % N 或一致性哈希分布均匀;扩容时需迁移数据(一致性哈希可减少迁移量)
查表映射独立路由表记录 key → 分片灵活,可做定制路由;多一次查询

选定分片键的原则:高频查询条件里必须带分片键,否则就是全分片扫描。

中间件选型#

方案定位
ShardingSphere-JDBC客户端 JDBC 增强,无代理层,性能好
ShardingSphere-Proxy独立代理,多语言通用
Vitess云原生 MySQL 分片,K8s 友好,YouTube 出品
TiDB / OceanBase原生分布式数据库,把分片对应用透明

引入分库分表后的新问题#

  • 分布式事务:跨分片写入无法用本地事务,要么 2PC/TCC,要么用”本地消息表 + 补偿”走向最终一致。
  • 全局唯一 ID:AUTO_INCREMENT 不再全局唯一,需要雪花算法、号段模式或 Redis INCR。
  • 跨分片分页与排序:LIMIT 100000, 10 变成每个分片取 100010 行再归并,性能极差,应改为”按游标/上次最大 ID 翻页”。
  • 扩容迁移:分片数翻倍时的数据搬迁与双写切换,是分库分表最痛的一步,最好在设计时就把分片数放大(例如分成 1024 个逻辑分片再映射到物理库)。
  • 运维复杂度:备份、DDL、监控、故障排查都要按”分片 × 副本”维度放大。

小结#

  • 主从结构用一份数据换来了读扩展、数据冗余和故障接管的可能,是 MySQL 高可用的地基。
  • 复制本身很简单(binlog 三段线程),难的是位点管理(用 GTID 解决)和延迟治理(并行复制 + 业务削峰 + 读写策略)。
  • 异步复制注定存在数据丢失窗口,延迟只能降低不能消除;因此”复制正常”不等于”高可用”,还需要第二篇的故障检测与自动切换、第三篇的备份与容灾演练共同兜底。
  • 读写分离和分库分表会引入一致性与复杂度成本,能用索引、缓存和架构优化解决的,不要用分片解决。
Global Transaction IdentifierGlobal Transaction IdentifierGlobal Transaction IdentifierGlobal Transaction IdentifierGlobal Transaction IdentifierGlobal Transaction IdentifierGlobal Transaction IdentifierGlobal Transaction IdentifierGlobal Transaction IdentifierGlobal Transaction IdentifierGlobal Transaction IdentifierGlobal Transaction IdentifierTwo-Phase CommitTry-Confirm-CancelGlobal Transaction Identifier
MySQL 高可用详解(1)主从复制与分库分表
https://blog-l7wd3.pages.dev/posts/ha/mysql-replication/
作者
L7WD3-Xiao
发布于
2026-09-02
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取