数据库基础体系 · 第 18/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

MySQL 复制与高可用:Binlog、GTID、半同步、切换与一致性

一、先明确问题:复制解决什么,不能解决什么

MySQL 主从复制(MySQL 8.x 官方文档逐渐使用 source/replica,传统资料中的 master/slave 对应 source/replica)是把源服务器上的数据库变更传送并重放到一个或多个副本服务器。

它通常用于:

  • 读扩展;
  • 备份和报表读取;
  • 故障切换;
  • 跨机房数据复制;
  • 降低单台服务器故障对业务的影响。

但复制本身不是共识协议,也不是自动高可用系统。异步复制下,源库已经提交的事务可能尚未到达副本;半同步复制可以缩小这个窗口,但不能保证副本已经执行事务,也不能自动完成选主、流量切换和旧源隔离。

因此需要区分四个问题:

  1. 变更如何记录:Binlog。
  2. 副本如何定位和去重:GTID。
  3. 源库何时认为提交可以返回:异步或半同步复制。
  4. 故障后谁成为新源,以及客户端读到什么:切换流程和一致性控制。

二、Binlog:复制的数据来源

2.1 Binlog 与 InnoDB Redo Log 不是一回事

MySQL 中常见的两类日志职责不同:

日志 主要用途 内容形态
InnoDB Redo Log 崩溃恢复,保证已提交修改能够恢复 更接近物理页或存储引擎内部变更
Binary Log(Binlog) 复制、时间点恢复、审计式变更追踪 面向逻辑操作和事务事件

例如:

CREATE TABLE account (
    id BIGINT PRIMARY KEY,
    balance DECIMAL(12, 2) NOT NULL
) ENGINE = InnoDB;

START TRANSACTION;

UPDATE account
SET balance = balance - 100
WHERE id = 1;

UPDATE account
SET balance = balance + 100
WHERE id = 2;

COMMIT;

InnoDB 可能会修改多个数据页和索引页,并把这些修改写入 Redo Log。Binlog 则记录这次事务对应的逻辑事件:

  • 事务开始或事务边界;
  • 两条 UPDATE 的语句事件,或者对应的行变更事件;
  • 事务提交事件;
  • GTID 等复制元数据。

副本并不接收源库的 InnoDB 数据页,而是读取 Binlog,再由副本自己的 InnoDB 执行或应用这些变更。

因此:

  • Redo Log 不能直接替代 Binlog 做复制;
  • Binlog 也不能替代 Redo Log 做本机崩溃恢复;
  • 备份文件加上 Binlog,才能构造从某个时间点继续恢复的能力。

2.2 Binlog 的基本数据流

一个事务从源库到副本,通常经过以下路径:

客户端
  │
  │ 事务提交
  ▼
源库 InnoDB ──写入 Binlog──> 源库 Binlog 文件
  │
  │ 复制连接
  ▼
副本 I/O 接收线程 ──写入 Relay Log──> 副本 SQL/applier 线程
                                      │
                                      ▼
                                  副本 InnoDB

副本至少包含两个逻辑阶段:

  1. 接收阶段:副本连接源库,读取 Binlog,写入本地 Relay Log。
  2. 应用阶段:副本从 Relay Log 读取事件,在本地执行事务。

所以“副本已经收到”不等于“副本已经执行”。

可以用 SHOW REPLICA STATUS\G 检查状态。不同版本和配置下字段很多,核心字段包括:

  • Replica_IO_Running:接收线程是否运行;
  • Replica_SQL_Running:应用线程是否运行;
  • Source_Log_FileRead_Source_Log_Pos:已经从源库读取到哪里;
  • Relay_Source_Log_FileExec_Source_Log_Pos:已经应用到源库 Binlog 的哪里;
  • Seconds_Behind_Source:一个近似延迟指标;
  • Last_IO_ErrorLast_SQL_Error:接收或应用失败原因;
  • Retrieved_Gtid_Set:已经接收到的 GTID 集合;
  • Executed_Gtid_Set:已经在副本执行的 GTID 集合。

Seconds_Behind_Source 不能作为唯一的一致性判断依据。它可能因为源库无新事务、事务长时间执行、时钟差异或并行应用而失真。GTID 集合和具体事务的等待结果通常更可靠。

2.3 Binlog 格式:STATEMENT、ROW 和 MIXED

Binlog 的主要格式有:

  • STATEMENT:记录执行过的 SQL 语句;
  • ROW:记录行变化前后的必要信息;
  • MIXED:根据语句类型在两种格式之间选择。

生产环境通常优先使用 ROW,因为它较少依赖副本上的环境和执行结果。例如:

UPDATE orders
SET status = 'expired'
WHERE created_at < NOW() - INTERVAL 30 DAY;

如果使用语句格式,NOW()、随机数、非确定性函数、不同 SQL_MODE、不同字符集设置等都可能使副本得到不同结果。行格式直接描述被修改的行,复制确定性更好。

但行格式也有边界:

  • 大批量更新会产生大量行事件;
  • binlog_row_image=FULL 会记录完整行,日志量更大;
  • MINIMAL 可以减少部分日志量,但要求表结构、主键和副本状态满足复制需要;
  • 没有合适主键的表可能导致副本定位和应用效率变差;
  • DDL 仍然需要在副本上执行相应的元数据变更。

因此,“使用 ROW 就绝对不会不一致”是错误的。副本上的表结构、过滤规则、手工写入、存储引擎和版本差异仍可能造成问题。

2.4 Binlog 保留与清理

复制依赖源库上的 Binlog。若副本停机太久,源库清理了副本尚未读取的日志,副本就无法从原位置继续复制。

常见恢复路径有两种:

  1. 从仍然保留的 Binlog 继续;
  2. 使用新的备份重新初始化副本,再开启 GTID 自动定位。

不要仅凭磁盘空间不足就直接删除 Binlog。应先确认:

  • 所有副本已经读取并应用到需要的位置;
  • 备份和时间点恢复策略仍然成立;
  • 没有延迟副本、审计订阅者或其他复制通道依赖这些日志。

Binlog 的删除只影响源库上的历史日志文件,不会自动修复已经落后的副本。


三、事务提交与复制:异步为什么会丢数据

3.1 异步复制的提交路径

异步复制下,源库提交事务时不等待副本确认。简化后的过程是:

  1. 客户端向源库执行事务;
  2. InnoDB 写入事务修改;
  3. 源库写入并提交 Binlog;
  4. 源库向客户端返回成功;
  5. 副本稍后读取 Binlog;
  6. 副本再应用事务。

因此,在第 4 步之后、第 5 步之前,源库可能已经故障。

设:

  • CsC_s:源库已经向客户端确认成功的事务集合;
  • RrR_r:副本已经收到并写入 Relay Log 的事务集合;
  • ErE_r:副本已经执行完成的事务集合。

通常有:

ErRrCsE_r \subseteq R_r \subseteq C_s

异步复制允许这三个集合之间存在差距。发生源库永久损坏时,至少可能丢失:

CsErC_s - E_r

这就是复制层面的潜在 RPO。RPO(Recovery Point Objective)表示故障后允许丢失的数据范围。

3.2 持久化设置仍然重要

即使使用复制,源库本地事务的持久性仍受到存储配置影响。常见的强持久性组合是:

innodb_flush_log_at_trx_commit = 1
sync_binlog = 1

其直觉是:

  • innodb_flush_log_at_trx_commit=1:每次事务提交时将 InnoDB Log Buffer 中的日志写入并刷新到持久存储;
  • sync_binlog=1:每次事务提交相关的 Binlog 写入都进行同步。

这不是绝对保证。硬件缓存、磁盘控制器、电源保护、虚拟化存储和文件系统行为仍会影响真正的持久性。降低这些参数可以改善吞吐,但扩大崩溃时可能丢失最近事务的窗口。

此外,Redo Log 和 Binlog 之间需要满足事务提交的一致性。MySQL 使用两阶段提交协调 InnoDB 与 Binlog:

  1. InnoDB 进入 prepare 状态;
  2. Binlog 写入并按配置同步;
  3. InnoDB 完成 commit;
  4. 崩溃恢复时依据两者状态决定提交或回滚。

这解决的是源库自身的崩溃一致性,并不意味着副本已经应用事务。


四、GTID:用事务身份代替文件位置

4.1 GTID 的结构

GTID(Global Transaction Identifier)是事务的全局标识,常见形式为:

source_server_uuid:transaction_number

例如:

3e11fa47-71ca-11e1-9e33-c80aa9429562:42

其中:

  • source_server_uuid 标识产生该事务的服务器;
  • transaction_number 是该服务器分配的递增编号。

多个 GTID 可以表示为 GTID 集合,例如:

3e11fa47-71ca-11e1-9e33-c80aa9429562:1-42

表示同一 UUID 下编号 1 到 42 的事务。

GTID 的核心价值不是“编号更好看”,而是让副本能够回答:

这个事务我是否已经执行过?

在传统文件位置复制中,副本需要知道:

mysql-bin.000123:456789

但主库切换后,文件名和位置可能完全不同。GTID 则允许副本根据已经执行的事务集合自动寻找缺失事务。

4.2 执行集合、清理集合与自动跳过

副本维护的 Executed_Gtid_Set 可以抽象为集合 EE。源库当前可提供的 Binlog 事务集合记为 BB

副本需要的事务大致是:

BEB - E

如果某个事务的 GTID 已经在副本的执行集合中,副本在复制过程中可以跳过该事务,而不是再次执行。

这对故障切换非常重要。例如:

源库已产生:
A:1, A:2, A:3, A:4

副本已执行:
A:1, A:2, A:3

新源只需要继续提供 A:4。如果副本已经执行 A:4,即使复制连接重新从包含 A:4 的日志范围开始,也不会把同一事务再次执行成两次。

但 GTID 不会自动解决所有冲突:

  • 如果两个服务器各自产生了不同事务,可能形成分叉;
  • 如果有人绕过复制直接在副本写入,可能产生不兼容事务;
  • 如果 server_uuid 重复,GTID 身份会被破坏;
  • 如果使用复制过滤器,GTID 集合存在不代表所有数据库对象都一致。

4.3 GTID 自动定位示例

源库和副本都应先正确配置 GTID 相关参数,并保证 server_uuid 唯一。一个概念性的配置示例:

# 源库和副本都需要
gtid_mode = ON
enforce_gtid_consistency = ON
log_bin = ON
log_replica_updates = ON

log_replica_updates=ON 的作用是让副本把自己从上游接收到并执行的事务继续写入自己的 Binlog。这对级联复制和故障切换后的继续复制很重要。

在副本上配置:

CHANGE REPLICATION SOURCE TO
    SOURCE_HOST = 'source.example.com',
    SOURCE_PORT = 3306,
    SOURCE_USER = 'repl',
    SOURCE_PASSWORD = 'change_me',
    SOURCE_AUTO_POSITION = 1;

START REPLICA;

实际生产中不应把明文密码直接留在命令历史中;应使用权限受控的凭据管理方式,并限制复制账号权限。

验证:

SHOW REPLICA STATUS\G

重点观察:

Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Auto_Position: 1
Last_IO_Error:
Last_SQL_Error:
Executed_Gtid_Set: ...

SOURCE_AUTO_POSITION=1 表示使用 GTID 自动定位,而不是指定 SOURCE_LOG_FILESOURCE_LOG_POS

4.4 GTID 模式的迁移边界

不能简单地把在线生产环境的 gtid_mode 直接从 OFF 改成 ON,然后期待现有复制自动安全转换。GTID 模式迁移需要遵循版本支持的分阶段流程,逐步处理:

  • enforce_gtid_consistency
  • gtid_mode 的过渡状态;
  • 现有事务和复制通道;
  • 所有副本的兼容配置。

迁移前应验证是否存在不能生成 GTID 的语句或事务模式。具体过渡步骤应以目标 MySQL 8.4 版本文档为准,而不是套用旧版本博客中的参数变更顺序。


五、半同步复制:缩小确认成功后的丢失窗口

5.1 半同步的定义

半同步复制介于异步和同步之间:

  • 异步:源库不等待副本确认;
  • 半同步:源库至少等待规定数量的副本确认;
  • 强同步或共识式复制:通常需要更严格的多数派和成员状态协议。

MySQL 半同步复制由源库端和副本端插件共同参与。MySQL 8.4 中相关系统变量使用 rpl_semi_sync_source_*rpl_semi_sync_replica_* 命名。

一个典型配置方向是:

# 源库
rpl_semi_sync_source_enabled = ON
rpl_semi_sync_source_timeout = 1000
rpl_semi_sync_source_wait_for_replica_count = 1

# 副本
rpl_semi_sync_replica_enabled = ON

实际启用前需要确认对应插件已安装并加载,变量名称和动态配置能力以当前 MySQL 8.4 安装包及文档为准。

5.2 一次半同步提交发生了什么

假设源库配置要求至少一个副本确认:

  1. 客户端向源库提交事务;
  2. 源库把事务写入 Binlog;
  3. 源库等待副本确认;
  4. 副本接收事务并写入 Relay Log;
  5. 副本发送确认;
  6. 源库完成等待点之后的提交流程;
  7. 源库向客户端返回成功。

关键在第 4 步:半同步确认通常表示副本已经收到事务并按半同步协议写入 Relay Log,而不是已经在副本 InnoDB 中执行完成。

因此:

客户端确认成功至少一个副本已确认接收\text{客户端确认成功} \Rightarrow \text{至少一个副本已确认接收}

但通常不能推出:

客户端确认成功至少一个副本已执行完成\text{客户端确认成功} \Rightarrow \text{至少一个副本已执行完成}

5.3 AFTER_SYNC 与 AFTER_COMMIT

半同步等待点会影响故障语义。常见等待点包括:

  • AFTER_SYNC:源库将 Binlog 同步后,在完成最终提交前等待副本确认;
  • AFTER_COMMIT:源库完成本地提交后,再等待副本确认。

AFTER_SYNC 通常具有更好的提交顺序和故障语义,但具体表现仍取决于版本、插件和存储持久化配置。不要仅凭“启用了半同步”就推断所有故障场景都不会丢数据。

5.4 超时后通常会退化为异步

半同步不是无限等待协议。超过:

rpl_semi_sync_source_timeout = 1000

指定的等待时间后,源库通常会退化为异步方式继续处理,避免一个失联副本阻塞整个写入系统。

这意味着:

半同步已启用⇏每个事务都得到副本确认\text{半同步已启用} \not\Rightarrow \text{每个事务都得到副本确认}

运维上必须监控:

  • 当前半同步是否实际生效;
  • 等待次数和超时次数;
  • 已连接且可确认的副本数量;
  • 复制延迟;
  • 源库是否长期处于异步退化状态。

半同步解决的是“源库确认成功后,数据至少到达一个副本”的部分问题,不解决:

  • 自动选主;
  • 旧源隔离;
  • 客户端连接切换;
  • 副本应用延迟;
  • 双主写入冲突;
  • 备份损坏或误删数据。

六、复制一致性:收到、执行、可读是三个状态

6.1 三种常被混淆的一致性

对一个事务来说,至少需要区分:

  1. 源库提交成功:源库向客户端返回成功;
  2. 副本接收成功:事务进入副本 Relay Log;
  3. 副本执行成功:事务已经应用到副本 InnoDB,查询可以看到结果。

半同步主要覆盖第 2 种;读副本要求的是第 3 种。

例如客户端执行:

INSERT INTO orders(id, status)
VALUES (1001, 'paid');

随后立刻把请求路由到副本:

SELECT status
FROM orders
WHERE id = 1001;

异步复制下可能返回空结果;半同步复制下也仍可能返回空结果,因为半同步确认不必等待副本 SQL/applier 线程执行完成。

6.2 用 GTID 实现读后写一致性

如果客户端在源库执行事务后能获得该事务所属的 GTID,就可以让后续读请求等待副本执行到这个 GTID。

示意:

-- 在写入连接或应用侧获得本次事务对应的 GTID 集合
-- 例如:'3e11fa47-71ca-11e1-9e33-c80aa9429562:42'

-- 在读副本上执行
SELECT WAIT_FOR_EXECUTED_GTID_SET(
    '3e11fa47-71ca-11e1-9e33-c80aa9429562:42',
    5
);

返回值通常可按以下方式理解:

  • 0:副本已经执行到目标 GTID;
  • 1:等待超时;
  • NULL:参数或执行出现错误。

应用应在返回 0 后再读取业务数据;超时则应:

  • 转回源库读取;
  • 返回明确的重试或降级结果;
  • 或根据业务接受短暂旧读。

这个机制只解决因复制延迟造成的读后写问题,不解决事务被回滚、业务读写路由错误或数据已经分叉的问题。

6.3 事务隔离级别与复制可见性

副本执行完成后,查询是否立即看到结果,还受到事务隔离级别影响。

例如,客户端在副本上开启一个长事务:

START TRANSACTION;

SELECT * FROM orders WHERE id = 1001;

即使复制线程随后执行了插入,当前事务的 InnoDB 一致性读仍可能根据 Read View 看不到这个新版本。再次执行普通 SELECT 也不一定看到新数据,直到事务结束并重新建立新的读视图。

所以:

复制已应用

不等于:

当前已有事务一定可见

这与 MySQL 事务和锁中的 Read View、当前读、间隙锁语义是两个层次:

  • 复制线程决定副本物理数据何时被应用;
  • InnoDB 隔离级别决定某个事务何时能观察到这些版本。

SELECT ... FOR UPDATE 等当前读还会涉及锁竞争,可能让复制应用变慢,进而扩大复制延迟。

6.4 并行复制会改变完成顺序

副本可以使用多个应用线程并行执行不冲突的事务。于是:

  • 接收顺序通常仍按源库 Binlog 顺序;
  • 应用过程可能并行;
  • 对无依赖事务,完成时间可能不同;
  • 对存在依赖的事务,副本需要保持必要的提交顺序或依赖关系。

因此,不能把“某个 SQL 线程当前没有报错”理解成“所有已接收事务都已经完成”。判断具体事务是否可读,应使用 GTID 等待或等价的事务位点检查。


七、切换与故障转移:先保护一致性,再改变角色

7.1 计划内切换(switchover)

计划内切换的目标是尽量做到零或接近零数据丢失。一个安全流程需要包含以下逻辑步骤。

第一步:停止或冻结新写入

先通过应用层、代理层或数据库权限控制阻止新的写事务进入旧源库。不能只执行:

SET GLOBAL read_only = ON;

就认为已经完全阻止写入,因为具有足够权限的账号、复制线程或特定管理操作可能不受普通 read_only 限制。生产环境通常还会配合:

SET GLOBAL super_read_only = ON;

并在连接入口实施流量隔离。

第二步:等待副本追平

在副本上检查:

SHOW REPLICA STATUS\G

确认:

Replica_IO_Running: Yes
Replica_SQL_Running: Yes
Last_IO_Error:
Last_SQL_Error:

更严格的方式是获得旧源库最后一批事务的 GTID 集合,然后在候选副本上等待:

SELECT WAIT_FOR_EXECUTED_GTID_SET(
    'source_uuid:1-10500',
    30
);

返回 0 才表示该副本已经执行到目标集合。

第三步:隔离旧源库

停止旧源库继续接受写入,必要时停止数据库服务或切断其业务网络。这个动作叫 fencing,即隔离旧角色,防止它在切换后仍然被客户端写入。

只改变 DNS、VIP 或代理后端,不一定足够,因为:

  • DNS 有缓存;
  • 连接池可能保留旧连接;
  • 长连接可能继续使用旧源;
  • 旧源可能仍被某个应用实例直接访问。

第四步:提升候选副本

在已追平的副本上停止复制,并解除只读限制:

STOP REPLICA;

SET GLOBAL read_only = OFF;
SET GLOBAL super_read_only = OFF;

是否执行 RESET REPLICARESET REPLICA ALL,要根据后续拓扑决定。它们可能清理复制元数据或连接配置,不应作为“提升副本”的无条件步骤。

第五步:切换客户端入口

更新代理、VIP、服务发现或应用配置,使新连接进入新源。切换后验证:

  • 新源可写;
  • 旧源不可写;
  • 新源包含切换前最后事务;
  • 应用连接池已刷新;
  • 监控看到新的复制拓扑。

计划内切换的核心条件可以形式化为:

候选副本执行集合旧源停止写入时的提交集合\text{候选副本执行集合} \supseteq \text{旧源停止写入时的提交集合}

如果这个条件不成立,切换即使看起来成功,也可能已经丢失最近事务。

7.2 非计划故障转移(failover)

旧源已经不可用时,通常不能等待它提供完整的最后 GTID 集合。此时要在多个副本中选择执行进度最靠前的候选者。

假设:

副本 R1: A:1-100
副本 R2: A:1-103
副本 R3: A:1-101

在确认没有其他来源的事务集合后,R2 的数据进度最靠前。它仍可能丢失源库已经提交但尚未传播的 A:104 及后续事务,但相比其他候选者更接近旧源状态。

故障转移至少有三个风险:

风险一:已确认事务丢失

如果事务已经返回给客户端,但没有到达任何可用副本,该事务可能无法恢复。半同步只能降低这种概率;如果发生超时退化或所有确认副本同时故障,仍可能丢失。

风险二:旧源“复活”造成双写

如果网络分区后旧源重新恢复,但它不知道自己已被提升的副本替代,应用仍可能向旧源写入。这会形成两个写入分支,也称 split-brain。

必须先隔离旧源,或者使用外部仲裁、STONITH(通过电源或管理接口强制关闭旧节点)等机制确保旧源不能继续提供写服务。

风险三:副本之间存在分叉

如果某个副本被人工写入,或者旧源曾经作为独立源产生了新 GTID,新源和旧源可能各自包含对方没有的事务。此时不能靠:

RESET MASTER;

或删除日志“抹平”问题。应该:

  1. 保存故障现场和日志;
  2. 比较各节点 GTID 集合;
  3. 识别哪些事务只存在于某一分支;
  4. 判断业务上如何合并或舍弃;
  5. 必要时从可信节点重新构建副本。

数据分叉是数据恢复问题,不是复制线程重启问题。


八、复制失败的诊断路径

8.1 接收线程停止

表现:

Replica_IO_Running: No
Last_IO_Error: ...

常见原因:

  • 网络不可达;
  • 复制账号认证失败;
  • TLS 配置不匹配;
  • 源库 Binlog 已被清理;
  • 源库未开启 Binlog;
  • server_uuid 或复制元数据配置错误。

处理顺序应是:

  1. 读取完整 Last_IO_Error
  2. 验证 DNS、端口和网络;
  3. 验证账号、权限和证书;
  4. 检查源库 Binlog 是否仍包含需要的位置;
  5. 不要直接执行 RESET REPLICA ALL 破坏现场。

8.2 应用线程停止

表现:

Replica_IO_Running: Yes
Replica_SQL_Running: No
Last_SQL_Error: ...

这意味着副本可能继续接收日志,但无法应用。常见原因:

  • 主键冲突;
  • 表不存在或结构不一致;
  • 复制过滤器造成依赖缺失;
  • DDL 与 DML 并发导致元数据锁等待或失败;
  • 副本被人工写入;
  • 存储引擎、字符集或 SQL_MODE 差异;
  • 大事务或锁冲突导致长时间停顿。

不要默认使用:

SET GLOBAL sql_replica_skip_counter = 1;
START REPLICA;

跳过事务可能让复制状态恢复为“运行”,但数据状态已经缺失。只有在明确知道该事务影响范围、能够校验数据并记录修复方案时,才可以采用跳过或注入补偿事务。GTID 模式下也不能通过随意修改 gtid_next 来掩盖数据问题。

8.3 延迟高但线程都在运行

“线程为 Yes”只表示没有立即报错,不表示复制没有延迟。应继续观察:

  • Retrieved_Gtid_SetExecuted_Gtid_Set 的差距;
  • 单个事务是否很大;
  • 副本是否存在长事务和锁等待;
  • 磁盘 I/O、CPU、Redo Log、Relay Log 是否饱和;
  • 并行应用线程是否不足;
  • 源库是否发生大批量 DDL 或更新。

源库上的长事务会持有锁、产生大量 Binlog,并可能让副本在应用时遇到同样的锁竞争。复制延迟有时不是网络问题,而是副本执行路径上的 InnoDB 锁和 I/O 问题。


九、备份、复制和高可用的边界

9.1 复制不是备份

复制会复制很多逻辑错误:

DROP TABLE important_data;

如果该语句进入 Binlog,副本也可能执行删除。误删、错误更新、应用 Bug 会被快速传播到所有副本。

复制也不能替代:

  • 全量备份;
  • 增量或 Binlog 归档;
  • 时间点恢复;
  • 备份恢复演练;
  • 跨故障域保存的副本。

一个副本可以用于备份,但备份过程必须考虑:

  • 备份时的一致性;
  • 长事务和锁影响;
  • Binlog 位点或 GTID 记录;
  • 备份文件是否可恢复;
  • 恢复后是否能继续应用对应 Binlog。

9.2 只读副本也不是绝对只读

副本通常配置:

read_only = ON
super_read_only = ON

但仍需限制管理账号和应用账号权限,避免人为写入。副本上的直接写入可能导致:

  • 主键冲突;
  • GTID 分叉;
  • 后续复制中断;
  • 切换时无法判断哪个事务可信。

9.3 MySQL 复制与更强的高可用协议

传统异步或半同步复制主要是 source-to-replica 日志传输。它缺少完整的成员管理和自动仲裁。

如果需要自动成员检测、故障判定和组内一致的主节点管理,可以研究 MySQL Group Replication 及基于它的 InnoDB Cluster 等方案。但这并不意味着所有问题消失:

  • 网络分区仍需处理;
  • 事务冲突仍有代价;
  • 节点数量和多数派影响可用性;
  • 客户端路由仍需正确配置;
  • 备份仍然必需。

高可用系统的最小闭环不是“部署两个 MySQL”,而是:

数据复制
+ 故障检测
+ 选主或人工决策
+ 旧主隔离
+ 客户端路由
+ 一致性验证
+ 可恢复备份

十、一个可验证的切换判断模型

可以把一次切换前的状态表示为:

  • GsG_s:旧源库停止写入时已经提交的 GTID 集合;
  • GrG_r:候选副本已经执行的 GTID 集合;
  • GbG_b:候选副本已经接收到但尚未执行的 GTID 集合;
  • GlG_l:候选副本缺失、但旧源可能已经拥有的 GTID 集合。

则:

Gs=GrGbGlG_s = G_r \cup G_b \cup G_l

其中三者不一定严格互斥,实际判断应以 MySQL 的 GTID 集合运算和复制状态为准。计划内切换要求:

Gl=G_l = \varnothing

也就是候选副本没有缺失旧源已提交事务。若:

GlG_l \neq \varnothing

就必须在“继续等待”“接受数据丢失”“换另一个候选副本”之间做出明确决策,而不是仅看 Seconds_Behind_Source = 0

故障转移时,旧源不可访问,通常只能在候选副本之间比较:

Gr1,Gr2,,GrnG_{r1}, G_{r2}, \ldots, G_{rn}

选择集合最完整且数据可信的副本,同时记录无法恢复的 GTID 范围。这些 GTID 范围代表故障时可能丢失的事务边界,应当进入故障报告和业务核对流程。


结语

Binlog 是复制和时间点恢复的变更载体;GTID 为事务提供可追踪、可去重、可自动定位的身份;半同步复制把源库确认成功与副本接收之间的距离缩小,但通常不等于副本已经执行;切换则必须同时处理事务追平、旧源隔离、客户端路由和新源验证。

最容易出错的判断有三个:

  1. “半同步开启,所以副本一定已经可读。”
    不成立,半同步确认通常早于副本应用完成。

  2. “两个复制线程都是 Yes,所以数据一致。”
    不成立,可能存在延迟、过滤、结构差异或尚未执行的事务。

  3. “把备用库提升为主库就完成高可用。”
    不成立,没有 fencing、流量切换、GTID 验证和恢复方案,切换可能造成双写或数据丢失。

真正可靠的 MySQL 高可用设计,必须把日志持久化、事务身份、复制确认、InnoDB 可见性、故障路径和恢复验证放在同一个因果链中考察。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。