MySQL(六):日志、主从复制与高可用
MySQL(六):日志、主从复制与高可用
导语:三大日志(Redo / Undo / Binlog)是 InnoDB 保证事务特性与主从复制的底层基石。本篇覆盖每种日志的作用与差异、两阶段提交、长事务对 undo 的危害,以及主从复制的原理、复制模式、GTID、读写分离与数据恢复。共 13 题。
一、三大日志
1. Redo Log 是什么?为什么需要它(WAL)?
答: Redo Log 是 InnoDB 引擎层的物理日志,记录"某个数据页发生了什么修改"(页号 + 偏移量 + 新值)。
核心思想:WAL(Write-Ahead Logging,预写日志)
- 事务提交时,先把 redo log 写入磁盘并 fsync,而数据页(脏页)可以之后异步刷盘;
- 为什么需要它:InnoDB 的读写都以页为单位,而一页 16KB。若每次提交都直接把随机分布的脏页写回磁盘,会产生大量随机 IO,性能极差;
- WAL 把"随机写数据页"变成了"顺序追加写日志",磁盘顺序写比随机写快几个数量级。
保证的特性:持久性(Durability)——即使事务提交后数据还在内存中未刷盘就宕机,重启后 InnoDB 也能根据 redo log 把已提交的修改重放出来,数据不丢。
文件组织(版本差异,需注意):
| 版本 | 组织方式 |
|---|---|
| MySQL 5.7 及之前 / 8.0.30 之前 | 固定数量的 ib_logfile0、ib_logfile1,循环写(写满就覆盖最旧的),大小由 innodb_log_file_size × innodb_log_files_in_group 决定 |
| MySQL 8.0.30 起 | 改为 #innodb_redo 目录下的 32 个文件,由 innodb_redo_log_capacity 统一控制总容量(默认 100MB),自动管理、循环写 |
面试要点:redo log 是循环写、固定大小的,这是它和 binlog(追加写、可长期保留)最本质的区别之一。循环写意味着不能无限回溯——redo 只用于"崩溃恢复",不用于"历史恢复"(那是 binlog 的职责)。
2. Undo Log 是什么?有什么作用?
答: Undo Log 是回滚日志,记录数据的「修改前的旧值」。它承担两大职责:
| 作用 | 说明 |
|---|---|
| 1. 原子性(回滚) | 事务失败或执行 ROLLBACK 时,按 undo log 反向恢复到事务开始前的状态 |
| 2. MVCC 快照读 | 多个 undo 记录通过回滚指针串成版本链,供快照读按 ReadView 找到可见的历史版本 |
两类 undo 与清理策略:
| 类型 | 产生于 | 能否立即删除 |
|---|---|---|
insert undo | INSERT | 可以——事务提交后即可删除(插入的记录对其他事务而言"此前不存在",没有更早的 ReadView 需要它) |
update undo | UPDATE / DELETE | 不可以——可能还有更早的 ReadView 需要看旧版本,必须由后台 purge 线程在确认无人需要后清理 |
关键理解:undo log 是"逻辑日志",它记录的是"如何撤销这次修改"(如"把 id=1 的 name 改回 '旧值'"),而不是物理页的字节变化。这与 redo log 的物理性正相反。
3. Binlog 是什么?与 Redo Log 有什么区别?
答: Binlog(归档日志)是 MySQL Server 层的逻辑日志,记录所有更改数据的操作(不含 SELECT),用于主从复制与按时间点恢复(PITR)。
四组核心区别(高频):
| 维度 | Redo Log | Binlog |
|---|---|---|
| 所属层 | InnoDB 引擎层 | MySQL Server 层(所有引擎通用) |
| 日志类型 | 物理日志(记录"某页某偏移改成了什么") | 逻辑日志(记录 SQL 语句或行的前后值) |
| 写入方式 | 循环写,固定大小,写满覆盖最旧 | 追加写,文件写满后切新文件,可长期保留 |
| 主要用途 | 崩溃恢复(保证持久性) | 主从复制、数据恢复/审计 |
| 记录时机 | 事务执行过程中持续写 | 事务提交时一次性写入(一个事务的 binlog 不可拆分) |
两个易错点:
① 只有 InnoDB 有 redo/undo log,而 binlog 是所有引擎通用的(MyISAM 也有 binlog,只是它没有 redo/undo,因此不支持事务与崩溃恢复);
② binlog 不记录SELECT和SHOW之类无数据变更的语句,所以它不能用于"审计谁查了什么"(那需要 general log 或审计插件)。
4. Binlog 有哪几种格式?如何选择?
答: binlog_format 有三种取值:
| 格式 | 记录内容 | 优点 | 缺点 |
|---|---|---|---|
STATEMENT | 记录原始 SQL 语句 | 日志体积小,可读性好 | 某些函数导致主从不一致:NOW()、RAND()、UUID()、USER()、带 LIMIT 的 UPDATE 等在从库重放结果可能不同 |
ROW(5.7+ 默认) | 记录每一行数据的前后值 | 精确、强主从一致;可做数据回溯与闪回;DDL 之外不受函数影响 | 日志体积大(批量更新会写大量行记录) |
MIXED | 自动切换:一般语句用 STATEMENT,不确定的(含上述函数)自动改用 ROW | 折中 | 仍存在 STATEMENT 的不确定性风险,判断依赖优化器 |
如何选择:
- 默认用
ROW(MySQL 8.0 的默认值)——主从一致性与数据恢复的可靠性优先; - 磁盘/网络带宽紧张时,可考虑
MIXED,但要注意核查; STATEMENT在 RC 隔离级别下是危险组合:RC 允许"半一致读",某些UPDATE/DELETE在主库和从库上锁定的行可能不同,导致主从数据不一致——因此若使用 RC,必须用ROW格式(这也是很多公司改用 RC 的同时必须确认binlog_format=ROW的原因)。
补充:
ROW格式下可通过binlog_row_image控制记录范围(FULL记录前后完整值 /MINIMAL只记录被改的列 /NOBLOB),MINIMAL能显著减小日志体积,但会影响某些恢复与闪回工具的使用。
二、日志协同与长事务
5. 什么是两阶段提交(2PC)?为什么需要?
答: 为什么需要:一份数据变更要写两个日志——redo log(InnoDB 层,用于崩溃恢复)和 binlog(Server 层,用于主从复制与恢复)。两者必须保持一致:否则会出现"主库恢复后数据正确但从库少了一条"或反之。
两阶段提交的流程:
| 阶段 | 动作 |
|---|---|
| 1. Prepare 阶段 | InnoDB 写 redo log,并将该事务的 redo 标记为 prepare 状态,fsync 落盘 |
| 2. Write Binlog | Server 层写 binlog 并 fsync 落盘 |
| 3. Commit 阶段 | InnoDB 把 redo log 的状态改为 commit,事务完成 |
崩溃恢复规则(重启后 InnoDB 如何处理"半途而废"的事务):
| 崩溃时的状态 | 处理方式 |
|---|---|
redo 处于 prepare,且 binlog 完整(有 XID 的 commit 标记) | 提交(说明 binlog 已经写好,主从不会丢) |
redo 处于 prepare,但 binlog 不完整/不存在 | 回滚(说明 binlog 没写完,若提交会导致主从不一致) |
| redo 里没有 prepare 记录 | 说明事务还没走到第一步 → 回滚 |
一句话结论:两阶段提交让"redo log 与 binlog"要么都成功、要么都回滚,从而保证主库恢复后与从库的数据一致。
面试深挖:为什么不是"先写 binlog 再写 redo"?因为 binlog 写完就不能撤回(它可能已经被从库拉走并重放),而 redo 处于 prepare 状态时还"可回滚"——把"可回滚的一方"放在前面,把"不可撤回的一方"放在后面,才能让崩溃恢复有明确的裁决依据。
6. 三大日志如何协同保障事务?
答: 一个事务的完整生命周期:
| 时点 | 动作 |
|---|---|
| 修改前 | 把旧值写入 undo log(供回滚 + 供 MVCC 版本链) |
| 修改中 | 在 Buffer Pool 中更新数据页(产生脏页);同时把变更写入 redo log(prepare) |
| 提交时 | 写 binlog 并 fsync → 把 redo log 置为 commit |
| 提交后 | 脏页由后台线程异步刷盘(checkpoint) |
| 崩溃恢复 | redo 重放确保已提交事务不丢;undo 回滚未提交事务;binlog 用于重建从库 / PITR |
| 事务结束 | undo 由 purge 线程在无人需要后清理 |
三者分工一句话总结:
- Undo Log → 原子性 + MVCC
- Redo Log → 持久性
- Binlog → 主从复制 + 数据恢复
7. 什么是 undo 的 purge?长事务有什么危害?
答: Purge 是 InnoDB 的后台清理机制:由 purge 线程根据版本链,删除那些"已经没有任何活跃 ReadView 可能需要"的 undo 记录,并回收 undo 表空间。
为什么不能立即删除:假设事务 T1 的 ReadView 生成于版本 v1 之后,那么即使 v1 已被 v2 取代,purge 也必须保留 v1,因为 T1 的快照读可能还要读它。所以 purge 的准则是:"早于当前所有活跃 ReadView 的版本才能清理"。
长事务的五大危害(高频):
| 危害 | 说明 |
|---|---|
| 1. undo 无法清理、空间膨胀 | 版本链一直保留 → undo 表空间持续增长(ibdata1 / undo 文件暴涨),甚至撑满磁盘 |
| 2. 版本链变长,查询变慢 | 后续快照读要沿更长的版本链向前找可见版本,每次读都变慢 |
| 3. 长时间持锁 | 两阶段锁下锁一直持有到提交 → 阻塞其他写操作、ALTER TABLE(Waiting for table metadata lock)、造成连锁阻塞 |
| 4. 主从延迟 | 长事务在从库 replay 时耗时长,且会造成并行复制失效(长事务无法被拆到多线程) |
| 5. purge 落后 | History list length 持续升高,整个实例的清理能力被拖垮 |
监控与治理:
-- 查看运行中的事务与时长
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS seconds, trx_query
FROM information_schema.INNODB_TRX ORDER BY seconds DESC;
-- History list length(越大说明 purge 越落后)
SHOW ENGINE INNODB STATUS; -- 关注 "History list length"治理手段:拆分大事务(分批提交)、缩短事务边界(不要在事务里做 RPC/IO/计算)、设置 max_execution_time(限制单语句执行时间,对只读语句有效)、配置长事务告警。
一句话:"长事务是 MySQL 性能问题的万恶之源"——它同时伤害 undo 清理、版本链查询、锁并发、主从复制四个方面。
三、主从复制与高可用
8. MySQL 主从复制的原理是什么?
答: 主从复制基于 binlog,涉及三个线程:
| 线程 | 所在位置 | 职责 |
|---|---|---|
| binlog dump 线程 | 主库 | 读取主库的 binlog,发送给从库的 I/O 线程 |
| I/O 线程 | 从库 | 连接主库、接收 binlog,写入本地的 relay log(中继日志) |
| SQL 线程 | 从库 | 读取 relay log 并在从库重放(再执行一遍),使数据与主库一致 |
完整流程:
- 主库把数据变更写入 binlog;
- 从库 I/O 线程连接主库,主库的 dump 线程把 binlog 推送过来;
- I/O 线程把收到的内容写入本地 relay log;
- 从库 SQL 线程读取 relay log 并重放。
为什么要有 relay log 这个中间层:让"接收 binlog"与"重放 binlog"解耦——主库推送慢/网络抖动时不影响重放,重放慢也不会阻塞接收,同时 relay log 落盘后即使从库重启也能续上进度(记录在 relay_log_info 中)。
应用场景:
- 读写分离(主写从读,分散读压力);
- 数据备份与容灾(从库做备份不干扰主库);
- 高可用基础(主库故障时提升从库);
- Canal 等工具也模拟该机制,解析 binlog 同步到 ES / 缓存 / 数仓。
9. MySQL 有哪些复制模式?异步、半同步与组复制的区别?
答:
| 模式 | 机制 | 数据一致性 | 性能 |
|---|---|---|---|
| 异步复制(默认) | 主库提交后立即返回,不等待从库 | 可能丢数据:主库宕机时,尚未发送出去的 binlog 会丢失 | 最好 |
| 半同步复制 | 主库等至少一个从库确认"已收到 binlog 并写入 relay log"才返回成功 | 大幅降低丢数据风险(但写入 relay log 不等于已重放,极端情况仍可能丢) | 略降(等待网络往返) |
| 组复制(MGR) | 基于 Paxos 多数派共识,事务需多数节点确认 | 强一致(支持单主/多主模式) | 最差(多数派确认开销大) |
| 全同步 | 等所有从库确认 | 最强 | 最差(实际很少用) |
半同步复制的两个关键参数:
rpl_semi_sync_master_wait_for_slave_count:需要确认的从库数量(默认 1);rpl_semi_sync_master_timeout:等待超时(默认 10 秒),超时后自动降级为异步复制——这是半同步的"兜底",避免从库故障导致主库不可写。
版本差异:MySQL 8.0.26 起半同步插件更名,主库侧为 rpl_semi_sync_source、从库侧为 rpl_semi_sync_replica(旧名 rpl_semi_sync_master / rpl_semi_sync_slave 已废弃)。
选型建议:金融、账务等不允许丢数据的场景用半同步或 MGR;普通业务用默认异步 + 监控延迟即可。注意半同步只保证"从库收到了 binlog",不保证"已重放完成",因此仍可能读到旧数据(这是主从延迟问题,不是复制模式能解决的)。
10. 什么是 GTID?相比传统复制有什么好处?
答: GTID(Global Transaction Identifier,全局事务标识符) 为每个已提交事务分配一个全局唯一编号,格式为:
source_id:transaction_id
例:3E11FA47-71CA-11E1-9E33-C80AA9429562:23其中 source_id 是产生该事务的实例 UUID,transaction_id 是该实例上单调递增的序号。
相比传统"基于 binlog 文件名 + 位点(file + position)"复制的优势:
| 优势 | 说明 |
|---|---|
| 自动定位 | 从库只需开启 MASTER_AUTO_POSITION = 1,无需手动指定 binlog 文件和位点,自动找出缺失的事务 |
| 主从切换更简单 | 主库故障后提升某从库为新主,其他从库只需指向新主即可续上,无需逐个计算位点(传统复制下这一步极易出错) |
| 天然幂等 | 从库会自动跳过已经执行过的事务(GTID 已在 gtid_executed 中),避免重复执行 |
| 便于运维与校验 | 可用 gtid_executed 对比不同实例的事务集合,做一致性校验、判断数据是否同步完整 |
| 与并行复制配合更好 | GTID 记录顺序与组提交信息,便于并行重放判断依赖关系 |
启用与使用:
-- 主库 my.cnf
gtid_mode = ON
enforce_gtid_consistency = ON
log_bin = ON
-- 从库指向主库时开启自动定位
CHANGE REPLICATION SOURCE TO
SOURCE_HOST='...', SOURCE_USER='...', SOURCE_PASSWORD='...',
SOURCE_AUTO_POSITION = 1;注意事项:GTID 要求"一个事务对应一个 GTID",因此不支持部分语句(如 CREATE TABLE ... SELECT 在开启 enforce_gtid_consistency 时受限);某些 mysqlbinlog 恢复操作需要跳过 GTID(--skip-gtids)或手动 SET GTID_NEXT。
11. 主从延迟(数据不一致)怎么产生?如何应对?
答: 现象:从库的数据落后于主库,应用"写主读从"时可能读到旧数据。
产生原因:
| 原因 | 说明 |
|---|---|
| 从库单线程/并行度不足 | 早期从库只有单 SQL 线程重放,主库并发写多时必然积压 |
| 大事务 | 一个几百万行的 UPDATE 在主库是一个事务,从库必须整体重放,期间其他事务无法并行 |
| 大表 DDL | 慢 DDL 在从库执行时间长 |
| 从库负载高 | 从库承担大量读查询,资源被抢占,重放变慢 |
| 网络延迟 / 主库写入过猛 | binlog 传输或生成速度超过重放速度 |
查看延迟:SHOW REPLICA STATUS(8.0.22+)/ SHOW SLAVE STATUS 的 Seconds_Behind_Master(基于"从库当前时间 - 正在重放事务的时间戳"估算,有误差,如长时间无写入时会不准)。更可靠的方式是对比 GTID 集合(WAIT_FOR_EXECUTED_GTID_SET)。
应对手段:
| 层面 | 手段 |
|---|---|
| 架构 | 关键业务读写都走主库;对延迟敏感的读强行走主库 |
| 复制能力 | 开启并行复制(replica_parallel_workers / replica_parallel_type = LOGICAL_CLOCK,MySQL 8.0 默认按组提交并行);升级从库硬件 |
| 减少延迟源 | 避免大事务(分批提交)、避开业务高峰做 DDL、用 gh-ost/pt-osc 做在线 DDL |
| 业务兜底 | 写后短时间内的读走主库("写后读一致性");用 WAIT_FOR_EXECUTED_GTID_SET 等待指定事务同步完成 |
| 异步解耦 | 用 Canal 解析 binlog 异步更新缓存/下游,把"实时一致性"降级为"最终一致" |
| 监控 | 对延迟设阈值告警,超过阈值自动降级为读主库 |
一句话:主从延迟只能减小、无法消除(因为复制本质是异步的)。工程上的核心思路是"按一致性要求分级路由"——强一致读走主库,弱一致读走从库。
12. 读写分离如何实现?主从不一致的场景如何兜底?
答:
实现方式:
| 方式 | 说明 |
|---|---|
| 应用层多数据源路由 | 用 AOP + 注解(如 @ReadOnly)或 Spring 的 AbstractRoutingDataSource 动态切换数据源;灵活但需自己处理事务与强制走主库 |
| 中间件代理 | ShardingSphere-JDBC(客户端,无额外部署)、ShardingSphere-Proxy / MyCat / ProxySQL(服务端代理,对应用透明) |
| 框架内置 | 部分 ORM/中间件支持读写分离配置 |
主从不一致的典型场景与兜底:
| 场景 | 兜底方案 |
|---|---|
| 写后立刻读(下单后跳转详情页) | 同一请求/同一事务内的读强制走主库(用 ThreadLocal 标记);或写后的若干毫秒内走主库 |
| 读到自己刚写的数据 | 记录写入的 GTID,读之前 WAIT_FOR_EXECUTED_GTID_SET(gtid, timeout) 等从库追上 |
| 延迟超过阈值 | 监控 Seconds_Behind_Master,超过阈值时自动把读切到主库 |
| 强一致业务(支付、库存校验、账号余额) | 读写全部走主库 |
| 最终一致可接受(列表页、统计、推荐) | 走从库,接受短暂延迟 |
设计原则:不要把"读写分离"当成默认正确的架构,而要按接口的一致性要求分类——绝大部分查询(列表、详情、统计)可以走从库;涉及"写后读"和金额的关键路径必须走主库。这也是为什么很多团队只在少数高读 QPS 的接口上启用从库。
13. 如何用 binlog 做数据恢复(PITR)?
答: PITR(Point-In-Time Recovery,按时间点恢复) = 全量备份 + binlog 增量重放。
前提条件:
- 开启 binlog(
log_bin = ON),且binlog_format = ROW(便于精确重放与闪回); - 有定期全量备份(
mysqldump、xtrabackup/xtrabackup物理备份); - binlog 文件保留足够长时间(
binlog_expire_logs_seconds,默认 30 天),并异地备份(否则误删表后连 binlog 也可能被清理)。
恢复流程:
# 1. 用全量备份恢复到一个临时实例(假设备份时间点为 T0)
mysql -uroot -p < full_backup.sql # 逻辑备份
# 或用 xtrabackup --prepare + --copy-back # 物理备份
# 2. 用 binlog 重放从 T0 到"误操作之前"的增量
mysqlbinlog --start-datetime="2026-06-09 00:00:00" \
--stop-datetime="2026-06-09 10:29:59" \
/var/lib/mysql/binlog.000123 | mysql -uroot -p误删数据的典型处理(如 10:30 误执行了 DELETE/DROP):
- 恢复到 10:29:59(误操作前一秒);
- 跳过误操作语句继续重放之后的 binlog(用
--start-position精确定位,或--stop-position分段); - 校验数据后,把误删的数据导回线上库(而不是直接替换线上)。
GTID 环境下的差异:mysqlbinlog 重放时需要 --skip-gtids=true(避免重复的 GTID 被跳过),或用 SET GTID_NEXT='...' 手动控制。
闪回工具(更精准的恢复方式):
| 工具 | 作用 |
|---|---|
binlog2sql | 解析 binlog 生成"反向 SQL"(INSERT 变 DELETE),可精确回滚误操作 |
MyFlash(美团开源) | 解析并翻转 binlog,生成回滚文件,性能高,适合大事务场景 |
实战教训:真正可靠的不是"出事能恢复",而是"出事前有备份 + 演练过恢复流程"。建议定期(每季度)做恢复演练——很多团队的全量备份 + binlog 链路在真正需要时才发现断裂(binlog 被清理、备份损坏、权限丢失)。另外,生产库上应禁止执行无
WHERE的DELETE/UPDATE,或开启sql_safe_updates。
