本文行文:什么是主从结构,为什么需要主从结构 → 主从复制方法 → 复制延迟怎么处理 → 单表过大怎么处理
读写分离
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),卡住时它会持续增长,是排查复制问题的第一现场。
主从复制流程:
- 连接:Slave 的 IO 线程连接 Master 的 binlog dump 线程,请求获取指定日志文件的指定位置之后的 binlog 日志;
- 主库推送:Master 的 binlog dump 线程将指定的 binlog 信息(日志信息+文件名称+位置信息)发给 Slave;
- 从库同步:Slave 的 IO 线程接收到信息后将日志内容写入 relay log 文件的末端,并将读取到的 Master 端的 binlog 的文件名和位置记录到master-info,以便下一次读取时能告诉 Master 从何处开始同步日志。
- 从库重放:Slave 的 SQL 线程检测到 relay log 新增了内容后解析 Log 文件生成对应的 sql 语句,并应用到数据库。
配置流程
1. binlog 配置
[mysqld]server_id = 1 # 集群内必须唯一,0 会被拒绝log_bin = mysql-bin # 开启 binlogbinlog_format = ROW # 高可用场景一律用 ROWbinlog_row_image = FULL # 记录变更前后完整行镜像sync_binlog = 1 # 每次提交都刷盘,不丢已提交事务innodb_flush_log_at_trx_commit = 1 # 双 1 配置,崩溃不丢数据expire_logs_days = 7 # 或用 binlog_expire_logs_secondsbinlog 三种格式的选择:
| 格式 | 记录内容 | 特点 |
|---|---|---|
STATEMENT | SQL 原文 | 日志小,但 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 | | |-- +------------------+----------+--------------+------------------+[mysqld]server_id = 2relay_log = relay-binread_only = ON # 普通账号只读(super 与复制线程不受限)super_read_only = ON # 连 super 也禁写,防止误操作log_replica_updates = ON # 双主级联时必须开,保证 binlog 连续-- 从库:8.0 起 CHANGE MASTER TO 已更名为 CHANGE REPLICATION SOURCE TOCHANGE 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_Running | IO 线程是否在拉取 binlog,应为 Yes |
Replica_SQL_Running | SQL 线程是否在重放 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。
因此,从库只需知道”我执行过哪些事务”,这种机制使复制不再依赖易变的文件名和位置,简化了主从同步和故障切换操作。
配置方式
[mysqld]gtid_mode = ONenforce_gtid_consistency = ONlog_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;复制中断的常见错误
| 错误码 | 现象 | 处理思路 |
|---|---|---|
| 1062 | Duplicate entry 从库已有该行 | 先查清是主从数据漂移还是业务重复写;确认可跳过后用 SET GTID_NEXT='...'; BEGIN; COMMIT; 空事务跳过 |
| 1032 | Can't find record 从库缺行 | 说明从库被人工改过数据或被过滤过,宜用 pt-table-sync 修复或重建从库 |
| 1236 | Could 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_Pos | relay log 持续膨胀说明重放追不上 |
延迟来源
- 天然串行:传统复制只有一个 SQL 线程,主库并发写入会在这里排队堆积。
- 大事务:一次性
DELETE百万行、ALTER TABLE重建表,重放耗时长且无法拆分。 - DDL 与锁:从库重放时如果被长查询/长事务阻塞,整个重放链路停住。
- 从库自身压力:从库同时扛着大量读请求,CPU、IO 被抢占。
- 硬件与网络:binlog 传输带宽不足,或磁盘
fsync能力弱于主库。 - 行锁竞争:
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_CLOCKreplica_parallel_workers = 8 # 建议 4~16,与从库 CPU 核数相关replica_preserve_commit_order = ON # 保证提交顺序与主库一致,避免并行导致的可见性异常延迟的兜底思路
即使做了上述优化,异步复制在极端情况下仍会延迟,因此必须承认:延迟是”降低”而不是”消除”的。工程上的兜底是:
- 业务侧定义可接受的最大延迟(例如 1 秒),超过就降级为读主库。
- 监控告警盯住延迟,而不是等业务报错。
- 切换时校验 GTID,避免”延迟中切换”造成部分事务永丢(详见第二篇)。
分库分表
主从解决的是”读扩展与可用性”,但写能力和单表数据量仍在单实例上。当单表行数到千万级、单库磁盘或写入 QPS 触顶时,才需要考虑分库分表。
垂直拆分
按业务域把不同表拆到不同库(用户库、订单库、商品库)。
- 优点:业务边界清晰、单库压力下降、后续可独立扩容。
- 代价:跨库 JOIN 消失,需要改为多次查询或数据冗余。
水平拆分
把同一张表按规则拆成多张(同库多表或分到多库)。
| 分片方式 | 规则 | 适用 |
|---|---|---|
| Range | 按 id 段或时间(按月) | 日志、订单等有自然时序,便于归档删除;易热点 |
| Hash | id % N 或一致性哈希 | 分布均匀;扩容时需迁移数据(一致性哈希可减少迁移量) |
| 查表映射 | 独立路由表记录 key → 分片 | 灵活,可做定制路由;多一次查询 |
选定分片键的原则:高频查询条件里必须带分片键,否则就是全分片扫描。
中间件选型
| 方案 | 定位 |
|---|---|
| ShardingSphere-JDBC | 客户端 JDBC 增强,无代理层,性能好 |
| ShardingSphere-Proxy | 独立代理,多语言通用 |
| Vitess | 云原生 MySQL 分片,K8s 友好,YouTube 出品 |
| TiDB / OceanBase | 原生分布式数据库,把分片对应用透明 |
引入分库分表后的新问题
- 分布式事务:跨分片写入无法用本地事务,要么 2PC/TCC,要么用”本地消息表 + 补偿”走向最终一致。
- 全局唯一 ID:
AUTO_INCREMENT不再全局唯一,需要雪花算法、号段模式或 RedisINCR。 - 跨分片分页与排序:
LIMIT 100000, 10变成每个分片取 100010 行再归并,性能极差,应改为”按游标/上次最大 ID 翻页”。 - 扩容迁移:分片数翻倍时的数据搬迁与双写切换,是分库分表最痛的一步,最好在设计时就把分片数放大(例如分成 1024 个逻辑分片再映射到物理库)。
- 运维复杂度:备份、DDL、监控、故障排查都要按”分片 × 副本”维度放大。
小结
- 主从结构用一份数据换来了读扩展、数据冗余和故障接管的可能,是 MySQL 高可用的地基。
- 复制本身很简单(binlog 三段线程),难的是位点管理(用 GTID 解决)和延迟治理(并行复制 + 业务削峰 + 读写策略)。
- 异步复制注定存在数据丢失窗口,延迟只能降低不能消除;因此”复制正常”不等于”高可用”,还需要第二篇的故障检测与自动切换、第三篇的备份与容灾演练共同兜底。
- 读写分离和分库分表会引入一致性与复杂度成本,能用索引、缓存和架构优化解决的,不要用分片解决。
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 54 天
部分内容可能已过时