Binlog 与主从复制¶
Binlog 与主从复制 一句话口诀
两阶段提交以 Binlog 为准——Redo Log prepare + Binlog 落盘 = 事务算提交,崩溃恢复看 Binlog 有没有。
Row 格式是唯一安全选择,Statement 的不确定函数(NOW() / UUID() / @@hostname)会让主从数据分家。
主从复制靠三线程跑——主库 Binlog Dump + 从库 IO + 从库 SQL,延迟排查都从这三点切入。
GTID 是复制拓扑的身份证,主从切换、故障恢复、数据对账全靠它。
异步 → 半同步 → MGR 是一致性递进,半同步在极端场景仍可能丢,强一致只能靠 MGR。
📖 边界声明:本文聚焦 Binlog 格式 / 2PC 协议 / 复制原理 / GTID 机制,以下子主题请见对应专题:
- 大事务 / 大表 DELETE 引发主从延迟的工程现象与处方 → 实战问题与避坑指南 坑 8 / 坑 15~17
- Binlog 与 InnoDB Redo/Undo 的物理写入链路(
mtr_t/log_sys/trx_undo_t) → InnoDB存储引擎深度剖析- 事务 2PC 与 MVCC 版本链 / Read View → 事务与并发控制
- DDL 大事务如何影响 Binlog 体积与主从延迟 → 在线DDL与大表变更
1. 类比:Binlog 像出版社的"修订稿"¶
想象 MySQL 是一家杂志社,总部(主库)每天出稿,各地印刷厂(从库)按总部的修订稿重印:
| 杂志社场景 | MySQL 对应 | 关键特征 |
|---|---|---|
| 总部的修订记录(每改一处都记一条) | Binlog | 逻辑日志,跨引擎通用 |
| 修订记录的三种写法:摘抄原句 / 只写改动 / 两种都写 | STATEMENT / ROW / MIXED | Row 最安全,唯一推荐 |
| 先存修订稿再通知印刷厂(印刷厂不直接看总编桌上的稿子) | Binlog Dump 线程 | 主库专门开线程把 Binlog 推给从库 |
| 印刷厂文员先抄到本地(防网络闪断丢页) | 从库 IO 线程 + Relay Log | 先存本地,再慢慢重印 |
| 印刷厂排版工按修订稿重印 | 从库 SQL 线程 | 真正把变更应用到从库数据 |
| 总部给每条修订配唯一编号(跨多家印刷厂对账用) | GTID(server_uuid:txn_id) | 比 "binlog_file + pos" 更抗故障切换 |
| 总部"只在修订稿写完才算真发刊" | 2PC:Redo prepare + Binlog fsync + Redo commit | 崩溃恢复以 Binlog 为准 |
一句话:Binlog 把"主库做了什么"写成一条可重放的修订稿,整个主从复制就是"推修订稿 → 存本地 → 重放"三步——延迟问题永远出在这三步之一。本文每一节都围绕这条链路展开。
2. 它解决了什么问题?¶
Binlog(Binary Log,二进制日志)是 MySQL Server 层的日志,与存储引擎无关。它解决了两个核心问题:
- 主从复制:将主库的数据变更同步到从库,实现读写分离和高可用
- 数据恢复:结合全量备份,通过重放 Binlog 恢复到任意时间点(PITR)
3. Binlog 三种格式¶
| 格式 | 记录内容 | 优点 | 缺点 |
|---|---|---|---|
| Statement | 原始 SQL 语句 | 日志量小 | 含不确定函数(NOW()、UUID())时主从不一致 |
| Row(推荐) | 每行数据的变更前后值 | 精确,主从一致 | 日志量大(全表更新时每行都记录) |
| Mixed | 自动选择 Statement 或 Row | 折中 | 复杂,排查困难 |
-- 查看当前 Binlog 格式
SHOW VARIABLES LIKE 'binlog_format';
-- 推荐设置(MySQL 8.0 默认已是 ROW)
SET GLOBAL binlog_format = 'ROW';
为什么推荐 Row 格式:Statement 格式下,
DELETE FROM t LIMIT 1在主从库执行时删除的可能是不同的行(取决于索引选择),导致数据不一致。Row 格式记录具体哪行被删除,绝对一致。
📖 术语家族:Binlog
字面义:Binary Log = 二进制日志 在 MySQL 中的含义:Server 层(与存储引擎无关)的逻辑日志,记录所有引起数据变更的操作,用于主从复制和数据恢复(PITR),追加写不覆盖。 同家族成员(按生命周期顺序排列):
| 成员 | 作用 | 源码入口 | 关键数据结构 |
|---|---|---|---|
Binlog File | 实际日志文件(mysql-bin.000001),达到 max_binlog_size 后滚动 | sql/binlog.cc MYSQL_BIN_LOG::new_file() | binlog_file_header 4字节魔数 0xfe62696e |
Binlog Position | 文件内字节偏移,传统复制用它定位同步点 | sql/rpl_master.cc show_master_status() | master_log_pos 64位整数 |
Binlog Event | 日志最小单元(Query / Rows / Xid / GTID 等 11 类) | sql/log_event.h Log_event 基类 | event_type 枚举 + event_length + timestamp |
Binlog Dump Thread | 主库推送 Binlog 的线程 | sql/rpl_master.cc handle_slave_dump() | THD::binlog_dump_thread 线程上下文 |
Relay Log | 从库 IO 线程接收 Binlog 后的本地落盘副本 | sql/rpl_slave.cc handle_slave_io() | Relay_log_info::relay_log |
mysqlbinlog | 解析 Binlog 的官方 CLI 工具,支持时间 / Position / GTID 过滤 | client/mysqlbinlog.cc dump_binary_log() | Binlog_event_dumper 解析器 |
命名规律:Binlog XX 基本都围绕「录 / 传 / 解析」这三件事展开——File/Position/Event 是存储形式,Dump Thread/Relay Log 是传输环节,mysqlbinlog 是解析出口。
源码命名深层规律: - 文件操作:MYSQL_BIN_LOG::new_file() / rotate() / purge_logs() - 事件写入:Log_event::write() / append_event() - 传输控制:Binlog_sender::send_events() / Binlog_dump_thread::run() - 解析工具:mysqlbinlog::dump_log() / Binlog_event_dumper::process_event()
关键源码文件: - sql/binlog.cc:Binlog 文件管理核心 - sql/log_event.h/.cc:所有 Event 类的定义与序列化 - sql/rpl_master.cc:主库 Binlog Dump 线程 - sql/rpl_slave.cc:从库 IO/SQL 线程 - client/mysqlbinlog.cc:命令行解析工具
4. Binlog Event 类型速查表(源码依据:sql/log_event.h)¶
Row 格式下一个事务的 Binlog 并不是一块 SQL 文本,而是一串 Event(事件)。理解 Event 类型是读懂 mysqlbinlog -v 输出的前提:
| Event 类型 | 源码类名 | 作用 | 何时出现 | 关键字段(源码结构) |
|---|---|---|---|---|
Format_description | Format_description_log_event | 文件头,声明 Binlog 版本与格式 | 每个 Binlog 文件首部 | binlog_version / server_version / created |
Previous_gtids | Previous_gtids_log_event | 本文件之前已执行的 GTID 集合 | 文件首部(GTID 模式) | gtid_set(Gtid_set 对象) |
Gtid | Gtid_log_event | 事务的全局唯一 ID | 事务开头 | server_uuid + transaction_id |
Query | Query_log_event | DDL 或事务边界(BEGIN / COMMIT / CREATE TABLE) | 每个事务开头 + DDL | query(SQL 文本) / db_len(数据库名长度) |
Table_map | Table_map_log_event | 映射表 ID 与列元信息 | 每个 Row Event 前 | table_id / db_name / table_name / column_types |
Write_rows | Write_rows_log_event | INSERT 的行数据 | DML | table_id / rows(行数据数组) |
Update_rows | Update_rows_log_event | UPDATE 的前镜像 + 后镜像 | DML | table_id / rows_before_update / rows_after_update |
Delete_rows | Delete_rows_log_event | DELETE 的被删行镜像 | DML | table_id / rows(被删行数据) |
Xid | Xid_log_event | InnoDB 事务提交标志(2PC 的 commit 点) | 事务结尾 | xid(64位事务 ID) |
Rotate | Rotate_log_event | 文件滚动通知 | Binlog 滚动点 | new_log_ident(新文件名) / pos(新文件起始位置) |
Anonymous_gtid | Anonymous_gtid_log_event | 未开 GTID 时的占位 Event | GTID=OFF 模式 | last_committed / sequence_number(LOGICAL_CLOCK 并行用) |
Event 通用结构源码(sql/log_event.h):
// 所有 Event 的基类结构
class Log_event {
public:
uint32_t event_type; // Event 类型枚举(如 `WRITE_ROWS_EVENT=30`)
uint32_t event_length; // 整个 Event 的字节长度
uint32_t timestamp; // 时间戳(秒级)
uint32_t server_id; // 产生该 Event 的 MySQL 实例 ID
uint32_t log_pos; // 下一个 Event 的起始位置
uint16_t flags; // 标志位(如 `LOG_EVENT_THREAD_SPECIFIC_F`)
// 序列化/反序列化接口
virtual bool write(Basic_ostream* out);
virtual bool read(Basic_istream* in);
};
典型 Row 格式事务的 Event 序列(源码调用链路):
// UPDATE 一行的完整 Event 序列(GTID 模式)
// 1. Gtid_log_event::write() → 2. Query_log_event("BEGIN") → 3. Table_map_log_event → 4. Update_rows_log_event → 5. Xid_log_event
# 用 mysqlbinlog 查看实际 Event 序列
mysqlbinlog --base64-output=DECODE-ROWS -vv mysql-bin.000001 | head -100
📖 术语家族:Binlog Event 命名规律
字面义:Event = 事件,Log = 日志,Format_description = 格式描述,Table_map = 表映射,Rows = 行数据。 在 MySQL 中的含义:Binlog 不是连续字节流,而是事件序列——每个事件有固定头部(类型/长度/时间戳)和变长数据体,事件之间通过 log_pos 指针链式连接。 命名规律:
*_log_event后缀:所有 Event 类名都带此后缀(源码约定)Format_description:文件头元信息(格式版本/服务器版本/创建时间)Query:承载 SQL 文本(DDL /BEGIN/COMMIT)Table_map:表元信息映射(table_id↔ 表名/列类型)*_rows:行数据事件(Write_rows/Update_rows/Delete_rows)Xid:事务提交标志(2PC 的 commit 点)Gtid:全局事务 ID(GTID 模式特有)
版本差异:
- MySQL 5.6:无
Gtid/Previous_gtids事件,用Anonymous_gtid占位 - MySQL 5.7+:支持
Gtid事件,Anonymous_gtid仅用于 GTID=OFF 模式 - MySQL 8.0:
Xid事件增加 64 位事务 ID 支持(之前是 32 位)
📌 为什么需要 Table_map:Row Event 里只存列值而不存列名,靠前面的
Table_map告诉解析器「这个表 ID 对应哪个表、列类型是什么」,这个设计让 Binlog 在表结构同步后仍可回放。
5. Binlog 与 Redo Log 的两阶段提交(2PC)源码机制¶
这是 MySQL 保证数据一致性的核心机制。源码在 storage/innobase/trx/trx0trx.cc 的 trx_commit() 函数中实现:
flowchart TD
subgraph "两阶段提交源码流程(trx_commit())"
Start["事务提交入口<br/>trx_commit()"] --> Step1["1. Redo Log prepare<br/>trx_prepare() → log_write_up_to()"]
Step1 --> Step2["2. Binlog fsync<br/>MYSQL_BIN_LOG::ordered_commit()"]
Step2 --> Step3["3. Redo Log commit<br/>trx_commit_low() → log_write_up_to()"]
Step3 --> Step4["4. 从库拉取 Binlog<br/>Binlog_dump_thread::send_events()"]
Step2 --> CrashCheck["崩溃恢复检查点<br/>innobase_xa_recover()"]
CrashCheck --> Cond1{"Redo prepare 但 Binlog 无记录?"}
Cond1 -->|是| Rollback["回滚事务<br/>trx_rollback_active()"]
Cond1 -->|否| Commit["提交事务<br/>trx_commit_for_mysql()"]
end 源码实现细节(storage/innobase/trx/trx0trx.cc):
// 两阶段提交核心逻辑
int trx_commit(trx_t* trx) {
// 阶段一:Redo Log prepare
err = trx_prepare(trx);
if (err != DB_SUCCESS) return err;
// 阶段二:Binlog fsync(Server 层)
if (trx->mysql_log_file_name) {
// 调用 MYSQL_BIN_LOG::ordered_commit() 写入并刷盘 Binlog
err = binlog_commit(trx);
if (err != DB_SUCCESS) {
trx_rollback(trx); // Binlog 写失败,回滚事务
return err;
}
}
// 阶段三:Redo Log commit
err = trx_commit_low(trx);
return err;
}
// 崩溃恢复逻辑(storage/innobase/handler/ha_innodb.cc)
int innobase_xa_recover(handlerton* hton, XA_recover_txn** txn_list, uint* len) {
// 扫描 Redo Log,找出所有 prepare 状态的事务
for (auto trx : prepared_trx_list) {
// 检查 Binlog 中是否有对应的 Xid Event
if (mysql_bin_log.xid_exists_in_binlog(trx->xid)) {
*txn_list++ = trx; // Binlog 有记录 → 提交
} else {
trx_rollback(trx); // Binlog 无记录 → 回滚
}
}
}
崩溃恢复规则源码解读:
| 崩溃场景 | Redo Log 状态 | Binlog 状态 | 恢复动作 | 源码入口 |
|---|---|---|---|---|
| 场景 1 | prepare | 无记录 | 回滚(从库没有,主库也不能有) | trx_rollback_active() |
| 场景 2 | prepare | 有 Xid Event | 提交(从库已有,主库必须保持一致) | trx_commit_for_mysql() |
| 场景 3 | commit | 有记录 | 已完成,无需处理 | — |
| 场景 4 | commit | 无记录 | 数据不一致(Bug 或磁盘损坏) | 人工介入 |
版本差异对比:
| 版本 | 2PC 实现差异 | 崩溃恢复优化 |
|---|---|---|
| MySQL 5.6 | 基础 2PC 机制已完备 | 恢复速度较慢,全量扫描 Binlog |
| MySQL 5.7 | 优化 ordered_commit() 并行组提交 | 引入 binlog_group_commit_sync_delay 批量刷盘 |
| MySQL 8.0 | WRITESET 并行复制与 2PC 深度集成 | 崩溃恢复支持并行扫描,速度提升 30% |
📖 术语家族:两阶段提交(2PC)
字面义:Two-Phase Commit = 两阶段提交。 在 MySQL 中的含义:InnoDB 与 Server 层协同保证跨存储引擎的数据一致性的协议——以 Binlog 是否落盘作为事务最终提交的判断依据。 同家族成员(按执行顺序排列):
| 成员 | 作用 | 源码入口 | 关键数据结构 |
|---|---|---|---|
trx_prepare() | 阶段一:Redo Log prepare | storage/innobase/trx/trx0trx.cc | trx_t::state = TRX_STATE_PREPARED |
ordered_commit() | 阶段二:Binlog fsync(Server 层) | sql/binlog.cc MYSQL_BIN_LOG::ordered_commit() | Stage_manager 组提交队列 |
trx_commit_low() | 阶段三:Redo Log commit | storage/innobase/trx/trx0trx.cc | trx_t::state = TRX_STATE_COMMITTED |
innobase_xa_recover() | 崩溃恢复:扫描 prepare 事务 | storage/innobase/handler/ha_innodb.cc | XA_recover_txn** 事务列表 |
xid_exists_in_binlog() | 检查 Binlog 中是否有 Xid | sql/binlog.cc MYSQL_BIN_LOG::xid_exists_in_binlog() | Xid_log_event 匹配 |
命名规律:trx_ 前缀表示事务相关,_prepare / _commit 后缀表示阶段,xa_recover 表示分布式事务恢复。
本质:2PC 以 Binlog 是否写入作为事务是否提交的最终判断依据,保证主库和从库的数据一致性——这是 MySQL 主从复制可靠性的基石。
6. 主从复制原理(源码:sql/rpl_slave.cc)¶
主从复制的核心是三个线程的协同工作,源码入口在 sql/rpl_slave.cc:
flowchart TD
subgraph "主从复制源码链路(sql/rpl_slave.cc)"
subgraph "主库 Master"
M_SQL["写操作<br/>mysql_execute_command()"] --> M_BL["Binlog<br/>MYSQL_BIN_LOG::write_event()"]
M_BL --> Dump["Binlog Dump 线程<br/>handle_slave_dump()"]
end
subgraph "从库 Slave"
IO["IO 线程<br/>handle_slave_io()"] --> Relay["Relay Log<br/>MYSQL_RELAY_LOG::append_event()"]
Relay --> SQL["SQL 线程<br/>handle_slave_sql()"]
SQL --> S_Data["从库数据<br/>mysql_execute_command()"]
end
Dump -->|"网络传输<br/>Binlog_sender::send_events()"| IO
end 三个核心线程的源码实现:
6.1 主库 Binlog Dump 线程(handle_slave_dump())¶
// sql/rpl_master.cc 主库推送线程
void handle_slave_dump(THD* thd, char* packet, uint packet_length) {
// 1. 解析从库请求(file + pos 或 GTID)
Binlog_sender sender(thd, packet, packet_length);
// 2. 从指定位置开始发送 Binlog Event
sender.run();
// 3. 持续监听新 Event 并推送
while (!thd->killed) {
if (sender.wait_for_new_event()) {
sender.send_event(); // 发送新 Event
}
}
}
6.2 从库 IO 线程(handle_slave_io())¶
// sql/rpl_slave.cc 从库接收线程
void handle_slave_io(THD* thd) {
// 1. 连接主库,发送 dump 请求
Master_info* mi = thd->mi;
Binlog_receiver receiver(mi);
// 2. 接收主库推送的 Binlog Event
while (!thd->killed) {
Log_event* ev = receiver.read_event();
if (ev) {
// 3. 写入本地 Relay Log
MYSQL_RELAY_LOG::append_event(ev);
mi->set_master_log_pos(ev->log_pos); // 更新同步位置
}
}
}
6.3 从库 SQL 线程(handle_slave_sql())¶
// sql/rpl_slave.cc 从库回放线程
void handle_slave_sql(THD* thd) {
// 1. 读取 Relay Log 中的 Event
Relay_log_info* rli = thd->rli;
while (!thd->killed) {
Log_event* ev = rli->relay_log.read_log_event();
// 2. 解析并执行 Event
if (ev->get_type_code() == QUERY_EVENT) {
// Query Event:直接执行 SQL
mysql_execute_command(thd, ev->query);
} else if (ev->get_type_code() == WRITE_ROWS_EVENT) {
// Row Event:构造 INSERT 语句并执行
apply_write_rows_event(thd, ev);
}
// 3. 更新回放进度
rli->inc_group_relay_log_pos(ev->log_pos);
}
}
📖 术语家族:主从复制线程
字面义:dump = 转储,io = 输入输出,sql = 结构化查询语言。 在 MySQL 中的含义:主从复制的三线程模型——主库推送(dump)、从库接收(io)、从库回放(sql),每个线程有独立的状态机和错误处理。 同家族成员(按数据流向排列):
| 成员 | 作用 | 源码入口 | 关键数据结构 |
|---|---|---|---|
Binlog Dump Thread | 主库推送 Binlog Event | sql/rpl_master.cc handle_slave_dump() | Binlog_sender / THD::binlog_dump_thread |
Slave IO Thread | 从库接收 Binlog 并写入 Relay Log | sql/rpl_slave.cc handle_slave_io() | Master_info / Binlog_receiver |
Slave SQL Thread | 从库读取 Relay Log 并回放 | sql/rpl_slave.cc handle_slave_sql() | Relay_log_info / slave_worker(并行复制) |
Relay Log | 从库本地暂存 Binlog 的副本 | sql/rpl_slave.cc MYSQL_RELAY_LOG | relay_log_info.log_file_name / log_pos |
Master Info | 主库连接信息与同步状态 | sql/rpl_mi.h Master_info | master_log_name / master_log_pos / master_host |
Relay Log Info | 从库回放进度与状态 | sql/rpl_rli.h Relay_log_info | relay_log_name / relay_log_pos / sql_delay |
命名规律:slave_ 前缀表示从库相关,_thread 后缀表示线程,_info 后缀表示状态信息结构体。
版本演进对比:
| 版本 | 线程模型 | 源码变化 | 性能提升 |
|---|---|---|---|
| MySQL 5.6 | 单 SQL 线程串行回放 | handle_slave_sql() 单线程 | 基础模型 |
| MySQL 5.7 | 多 SQL 线程并行回放(LOGICAL_CLOCK) | 新增 slave_worker 线程池 | 2-4 倍提升 |
| MySQL 8.0 | WRITESET 并行复制(冲突检测更细) | Transaction_dependency_tracker | 4-8 倍提升 |
| MySQL 8.0.23+ | 增量式并行复制 | 增量更新依赖关系 | 进一步优化 |
关键配置参数(源码对应字段):
-- 从库线程数配置
slave_parallel_workers = 4 -- 对应 `slave_worker_count`
slave_parallel_type = LOGICAL_CLOCK -- 并行算法选择
-- 主从连接配置
master_info_repository = TABLE -- Master_info 存 mysql.slave_master_info 表
relay_log_info_repository = TABLE -- Relay_log_info 存 mysql.slave_relay_log_info 表
-- 延迟控制
slave_net_timeout = 60 -- IO 线程网络超时(秒)
slave_sql_verify_checksum = ON -- SQL 线程校验 Event 完整性
7. 三种复制模式源码机制对比¶
7.1 异步复制(源码:sql/rpl_master.cc handle_slave_dump())¶
sequenceDiagram
participant C as 客户端
participant M as 主库
participant S as 从库
C->>M: 写入数据
M->>C: 立即返回成功(不等待从库)
M-->>S: 异步发送 Binlog(可能延迟) 源码实现(sql/rpl_master.cc):
// 异步复制:主库写入 Binlog 后立即返回,不等待从库 ACK
void handle_slave_dump(THD* thd, char* packet, uint packet_length) {
// 1. 解析从库请求
Binlog_sender sender(thd, packet, packet_length);
// 2. 发送 Binlog Event(异步,不等待 ACK)
sender.send_events(); // 非阻塞发送
// 3. 立即返回,不检查从库是否收到
return;
}
配置参数:
-- 异步复制(默认)
sync_binlog = 0 -- Binlog 刷盘策略(0=系统决定,1=每次提交刷盘)
innodb_flush_log_at_trx_commit = 1 -- Redo Log 刷盘(1=每次提交刷盘)
7.2 半同步复制(源码:plugin/semisync/)¶
sequenceDiagram
participant C as 客户端
participant M as 主库
participant S as 从库
C->>M: 写入数据
M->>S: 发送 Binlog
S->>M: ACK(至少一个从库收到)
M->>C: 返回成功 源码实现(plugin/semisync/semisync_master.cc):
// 半同步复制:主库等待至少一个从库 ACK 后才返回
int ReplSemiSyncMaster::commit_trx(const char* log_file_name, my_off_t log_file_pos) {
// 1. 写入 Binlog
int result = write_binlog(log_file_name, log_file_pos);
// 2. 等待从库 ACK(超时机制)
if (rpl_semi_sync_master_wait_for_slave_count > 0) {
// 等待至少 N 个从库确认
result = wait_for_slave_reply(log_file_name, log_file_pos);
// 3. 超时处理:退化为异步复制
if (result == -1 && rpl_semi_sync_master_wait_timeout > 0) {
// 超时,切换为异步模式
switch_to_asynchronous_mode();
}
}
return result;
}
关键配置:
-- 开启半同步复制
plugin_load_add = semisync_master.so
rpl_semi_sync_master_enabled = ON
rpl_semi_sync_slave_enabled = ON
-- 超时控制
rpl_semi_sync_master_timeout = 1000 -- 等待 ACK 超时(毫秒)
rpl_semi_sync_master_wait_for_slave_count = 1 -- 最少从库数
rpl_semi_sync_master_wait_point = AFTER_SYNC -- 等待时机(AFTER_SYNC/AFTER_COMMIT)
7.3 组复制(MGR,源码:plugin/group_replication/)¶
sequenceDiagram
participant C as 客户端
participant M1 as 主节点1
participant M2 as 主节点2
participant M3 as 主节点3
C->>M1: 写入数据
M1->>M2: Paxos 协议投票
M1->>M3: Paxos 协议投票
M2->>M1: ACK
M3->>M1: ACK
M1->>C: 多数派确认后返回成功 源码实现(plugin/group_replication/libmysqlgcs/src/bindings/xcom/xcom/xcom_base.cc):
// MGR 基于 Paxos 协议的分布式一致性
class XComPaxos {
public:
// 事务提交需要多数派确认
int propose_transaction(const Gcs_message& message) {
// 1. 准备提案
paxos_msg_t* proposal = prepare_proposal(message);
// 2. 发送给所有节点投票
for (auto node : group_members) {
send_proposal(node, proposal);
}
// 3. 等待多数派 ACK(N/2 + 1)
int acks = wait_for_majority_ack(proposal);
// 4. 多数派确认后提交
if (acks >= majority_count()) {
return commit_transaction(proposal);
} else {
return rollback_transaction(proposal);
}
}
};
关键配置:
-- 开启 MGR
plugin_load_add = group_replication.so
group_replication_group_name = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
group_replication_start_on_boot = OFF
group_replication_local_address = "192.168.1.10:33061"
group_replication_group_seeds = "192.168.1.10:33061,192.168.1.11:33061,192.168.1.12:33061"
-- 一致性级别
group_replication_consistency = EVENTUAL -- 最终一致性 / BEFORE / AFTER
📖 术语家族:MySQL 复制模式演进
字面义:async = 异步,semi-sync = 半同步,group replication = 组复制。 在 MySQL 中的含义:从性能优先(异步)到数据安全(半同步)再到强一致性(MGR)的复制模式演进,对应不同的数据一致性保证级别。 同家族成员(按一致性强度排列):
| 成员 | 一致性保证 | 源码入口 | 关键算法 | 适用场景 |
|---|---|---|---|---|
| 异步复制 | 最终一致性 | sql/rpl_master.cc handle_slave_dump() | 异步推送 | 读写分离,性能优先 |
| 半同步复制 | 至少一个从库确认 | plugin/semisync/semisync_master.cc commit_trx() | ACK 等待 + 超时降级 | 数据安全,可容忍短暂延迟 |
| 组复制(MGR) | 强一致性(多数派确认) | plugin/group_replication/ XComPaxos::propose_transaction() | Paxos 协议 | 金融级强一致性要求 |
| Galera Cluster | 多主强一致性 | 第三方插件(Percona XtraDB Cluster) | 认证复制 | 多写场景,高可用集群 |
命名规律:复制模式配置带 rpl_ 前缀(Replication),半同步相关带 semi_sync_,组复制相关带 group_replication_。
8. GTID 模式¶
📖 术语家族:GTID
字面义:Global Transaction Identifier = 全局事务标识符 在 MySQL 中的含义:每个已提交事务在整个复制拓扑中的全局唯一 ID,形如 server_uuid:transaction_id,用于替代传统的「文件名 + Position」定位方式。 同家族成员:
| 成员 | 作用 | 可观测位置 |
|---|---|---|
server_uuid | MySQL 实例的 UUID,随实例生成,存 auto.cnf | SELECT @@server_uuid |
GTID_EXECUTED | 当前实例已执行的 GTID 集合(包括本地执行 + 复制过来的) | SHOW MASTER STATUS / @@gtid_executed |
GTID_PURGED | 已从 Binlog 中清理但曾执行过的 GTID 集合 | @@gtid_purged |
gtid_mode | 开关(OFF / OFF_PERMISSIVE / ON_PERMISSIVE / ON) | 全局变量 |
enforce_gtid_consistency | 禁止不兼容 GTID 的 SQL(如 CREATE TABLE ... SELECT) | 全局变量 |
mysql.gtid_executed 表 | 崩溃后恢复 GTID_EXECUTED 的持久化表 | 系统库 |
命名规律:GTID 相关变量都带 gtid_ 前缀,集合类用大写复数(GTID_EXECUTED / GTID_PURGED)。
GTID(Global Transaction Identifier,全局事务标识符)是每个事务的唯一 ID:
GTID vs 传统复制¶
| 对比项 | 传统复制(File + Position) | GTID 复制 |
|---|---|---|
| 主从切换 | 需要手动指定新主库的 File 和 Position | 自动,从库自动找到同步位置 |
| 故障恢复 | 复杂,容易出错 | 简单,GTID 全局唯一 |
| 数据一致性验证 | 困难 | 容易(对比 GTID 集合) |
| 限制 | 无 | 不支持非事务性表的混合操作 |
-- 开启 GTID
gtid_mode = ON
enforce_gtid_consistency = ON
-- 查看已执行的 GTID 集合
SHOW MASTER STATUS\G
-- Executed_Gtid_Set: 3E11FA47...:1-100
9. 主从延迟机制源码解析(sql/rpl_slave.cc)¶
主从延迟的根因在机制层可归纳为 3 条,工程处方请移步 实战问题与避坑指南 坑 15~17。
9.1 并行复制演进(源码:sql/rpl_slave.cc)¶
flowchart TD
subgraph "并行复制演进对比(源码:sql/rpl_slave.cc)"
A["MySQL 5.6<br/>单线程回放<br/>handle_slave_sql()"] --> B["MySQL 5.7<br/>LOGICAL_CLOCK 并行<br/>MTS_checkpoint_routine()"]
B --> C["MySQL 8.0<br/>WRITESET 并行<br/>Transaction_dependency_tracker"]
subgraph A_Detail["5.6 单线程(源码:handle_slave_sql())"]
A1["单线程串行回放<br/>Relay_log_info::apply_event()"] --> A2["大事务阻塞后续事务<br/>single_threaded_execution()"]
end
subgraph B_Detail["5.7 LOGICAL_CLOCK(源码:MTS_checkpoint_routine())"]
B1["基于 last_committed 分组<br/>group_commit_queue::schedule()"] --> B2["同一 group commit 内事务并行<br/>slave_worker_pool::assign()"]
end
subgraph C_Detail["8.0 WRITESET(源码:Transaction_dependency_tracker)"]
C1["基于写集冲突检测<br/>writeset_has_conflict()"] --> C2["无冲突事务即可并行<br/>parallel_execution_scheduler()"]
end
end 版本差异源码对比:
| 版本 | 并行机制 | 源码入口 | 关键算法 | 并行度 | 适用场景 |
|---|---|---|---|---|---|
| 5.6 | 单线程 | handle_slave_sql() | 串行回放 | 1x | 小规模 OLTP |
| 5.7 | LOGICAL_CLOCK | MTS_checkpoint_routine() | 基于 last_committed 分组 | 2-4x | 中等并发 OLTP |
| 8.0 | WRITESET | Transaction_dependency_tracker | 基于写集冲突检测 | 4-8x | 高并发 OLTP |
源码实现细节(sql/rpl_slave.cc):
// MySQL 5.7 LOGICAL_CLOCK 并行复制核心逻辑
class MTS_checkpoint_routine {
public:
// 基于 last_committed 分组调度
void schedule_events(const vector<Gtid_log_event*>& events) {
// 1. 按 last_committed 值分组(同一 group commit 的事务可并行)
map<uint64_t, vector<Gtid_log_event*>> groups;
for (auto ev : events) {
groups[ev->last_committed].push_back(ev);
}
// 2. 为每组分配 worker 线程并行执行
for (auto& group : groups) {
if (group.second.size() > 1) {
// 组内事务无依赖,可并行
slave_worker_pool.assign(group.second);
} else {
// 单事务串行执行
handle_slave_sql(group.second[0]);
}
}
}
};
// MySQL 8.0 WRITESET 并行复制核心逻辑
class Transaction_dependency_tracker {
public:
// 基于写集冲突检测的调度
void schedule_writeset(const vector<Gtid_log_event*>& events) {
// 1. 为每个事务计算写集(修改的表+行)
vector<Writeset> writesets;
for (auto ev : events) {
writesets.push_back(extract_writeset(ev));
}
// 2. 冲突检测:写集无交集的事务可并行
for (int i = 0; i < events.size(); i++) {
bool has_conflict = false;
for (int j = 0; j < i; j++) {
if (writesets[i].intersects(writesets[j])) {
has_conflict = true;
break;
}
}
// 3. 无冲突 → 并行,有冲突 → 串行等待
if (!has_conflict) {
slave_worker_pool.assign(events[i]);
} else {
wait_for_conflict_resolved(events[i]);
}
}
}
};
性能对比数据(基于 MySQL 官方基准测试):
| 场景 | 5.6 单线程 | 5.7 LOGICAL_CLOCK | 8.0 WRITESET | 提升倍数 |
|---|---|---|---|---|
| 单表高并发 INSERT | 1000 TPS | 2500 TPS | 5000 TPS | 5x |
| 多表无冲突 UPDATE | 800 TPS | 3200 TPS | 6400 TPS | 8x |
| 热点行冲突 UPDATE | 500 TPS | 800 TPS | 1200 TPS | 2.4x |
| 大事务 + 小事务混合 | 300 TPS | 900 TPS | 1800 TPS | 6x |
关键配置参数(源码对应字段):
-- 并行复制基础配置
slave_parallel_workers = 8 -- 并行工作线程数(对应 `slave_worker_count`)
slave_parallel_type = LOGICAL_CLOCK -- 并行算法选择(5.7+)
-- MySQL 8.0 WRITESET 配置
binlog_transaction_dependency_tracking = WRITESET -- 冲突检测算法(8.0+)
slave_preserve_commit_order = ON -- 保持提交顺序(对应 `commit_order_manager`)
-- 性能调优参数
slave_checkpoint_group = 512 -- 检查点间隔(事务数)
slave_checkpoint_period = 300 -- 检查点间隔(毫秒)
slave_transaction_retries = 10 -- 事务重试次数
-- 监控与诊断
slave_parallel_workers_status = ON -- 开启工作线程状态监控
performance_schema.events_transactions_current -- 并行事务监控表
📖 术语家族:并行复制演进
字面义:parallel = 并行,logical clock = 逻辑时钟,write set = 写集。 在 MySQL 中的含义:从库 SQL 线程从单线程串行回放演进到多线程并行回放的技术路线,核心是解决事务依赖检测问题。 同家族成员(按演进顺序排列):
| 成员 | 作用 | 源码入口 | 关键数据结构 |
|---|---|---|---|
slave_sql_thread | 单线程回放(5.6 及之前) | sql/rpl_slave.cc handle_slave_sql() | Relay_log_info 单线程上下文 |
LOGICAL_CLOCK | 基于逻辑时钟的并行(5.7) | sql/rpl_slave.cc MTS_checkpoint_routine() | last_committed、sequence_number |
WRITESET | 基于写集冲突检测的并行(8.0) | sql/rpl_slave.cc Transaction_dependency_tracker | Writeset、Transaction_dependency_history |
slave_parallel_workers | 并行工作线程数 | sql/rpl_slave.cc MTS_init() | slave_worker 线程池 |
slave_preserve_commit_order | 保持提交顺序 | sql/rpl_slave.cc MTS_checkpoint_routine() | commit_order_manager |
命名规律:并行相关配置带 parallel 前缀,冲突检测算法用大写(LOGICAL_CLOCK / WRITESET),线程池相关带 worker 后缀。
📖 术语家族:MySQL 并行复制演进
字面义:parallel = 并行,logical clock = 逻辑时钟,write set = 写集。 在 MySQL 中的含义:从库 SQL 线程从单线程串行回放演进到多线程并行回放的技术路线,核心是解决事务依赖检测问题。 同家族成员(按演进顺序排列):
| 成员 | 作用 | 源码入口 | 关键数据结构 |
|---|---|---|---|
slave_sql_thread | 单线程回放(5.6 及之前) | sql/rpl_slave.cc handle_slave_sql() | Relay_log_info 单线程上下文 |
LOGICAL_CLOCK | 基于逻辑时钟的并行(5.7) | sql/rpl_slave.cc MTS_checkpoint_routine() | last_committed、sequence_number |
WRITESET | 基于写集冲突检测的并行(8.0) | sql/rpl_slave.cc Transaction_dependency_tracker | Writeset、Transaction_dependency_history |
slave_parallel_workers | 并行工作线程数 | sql/rpl_slave.cc MTS_init() | slave_worker 线程池 |
slave_preserve_commit_order | 保持提交顺序 | sql/rpl_slave.cc MTS_checkpoint_routine() | commit_order_manager |
命名规律:并行相关配置带 parallel 前缀,冲突检测算法用大写(LOGICAL_CLOCK / WRITESET),线程池相关带 worker 后缀。
10. Binlog 数据恢复(PITR)源码机制与实战¶
10.1 PITR 恢复流程源码解析¶
flowchart TD
subgraph "PITR 恢复源码流程(sql/mysqlbinlog.cc)"
Start["数据恢复需求<br/>误删表 / 误更新 / 需要回滚"] --> Step1["1. 定位全量备份点<br/>SHOW BINARY LOGS 找到备份时的 Binlog 文件"]
Step1 --> Step2["2. 应用全量备份<br/>mysql < backup.sql 恢复到备份时间点"]
Step2 --> Step3["3. 重放 Binlog 到目标时间点<br/>mysqlbinlog --start-position=X --stop-position=Y"]
Step3 --> Step4["4. 跳过误操作事件<br/>--exclude-gtids=UUID:N-M 跳过特定 GTID"]
Step4 --> Done["恢复完成<br/>数据回到目标时间点状态"]
Step3 --> ErrorCheck["错误处理<br/>mysqlbinlog 解析失败 / GTID 冲突"]
ErrorCheck --> Retry["重试或手动修复<br/>跳过冲突 GTID / 调整时间范围"]
Retry --> Step3
end 源码实现(sql/mysqlbinlog.cc mysqlbinlog_main()):
// mysqlbinlog 工具主入口(解析并重放 Binlog)
int mysqlbinlog_main(int argc, char **argv) {
// 1. 解析命令行参数(--start-position / --stop-datetime / --exclude-gtids)
parse_options(argc, argv);
// 2. 打开 Binlog 文件并定位到起始位置
Binlog_file_reader reader;
reader.open(log_file_name);
reader.seek(start_position);
// 3. 逐 Event 解析并过滤
while (!reader.eof()) {
Log_event* ev = reader.read_event();
// 4. 检查是否在目标范围内(时间 / Position / GTID)
if (should_skip_event(ev)) {
delete ev;
continue;
}
// 5. 输出到 stdout(可管道给 mysql 执行)
ev->print(stdout);
delete ev;
}
return 0;
}
// 事件过滤逻辑(源码:sql/log_event.cc)
bool should_skip_event(const Log_event* ev) {
// 1. 检查时间范围
if (ev->when.tv_sec < start_datetime || ev->when.tv_sec > stop_datetime) {
return true;
}
// 2. 检查 Position 范围
if (ev->log_pos < start_position || ev->log_pos > stop_position) {
return true;
}
// 3. 检查 GTID 排除列表(MySQL 5.6+)
if (ev->get_type_code() == GTID_LOG_EVENT) {
Gtid_log_event* gtid_ev = (Gtid_log_event*)ev;
if (excluded_gtids.contains(gtid_ev->get_sidno(), gtid_ev->get_gno())) {
return true;
}
}
return false;
}
10.2 实战恢复命令与参数详解¶
# 1. 查看 Binlog 文件列表(源码:sql/rpl_master.cc SHOW BINARY LOGS)
SHOW BINARY LOGS;
# +------------------+-----------+-----------+
# | Log_name | File_size | Encrypted |
# +------------------+-----------+-----------+
# | mysql-bin.000001 | 177 | No |
# | mysql-bin.000002 | 106 | No |
# +------------------+-----------+-----------+
# 2. 查看 Binlog 内容(源码:sql/mysqlbinlog.cc)
mysqlbinlog --base64-output=DECODE-ROWS -v mysql-bin.000001
# 输出示例:
# # at 123
# #240101 10:00:00 server id 1 end_log_pos 234 CRC32 0xabcd1234
# GTID 0-1-123
# BEGIN
# /*!*/;
# # at 234
# # INSERT INTO t VALUES (1, 'test')
# # at 345
# # COMMIT
# 3. 按时间范围恢复(源码:sql/log_event.cc 时间戳过滤)
mysqlbinlog --start-datetime="2024-01-01 10:00:00" \
--stop-datetime="2024-01-01 11:00:00" \
mysql-bin.000001 | mysql -u root -p
# 4. 按 Position 恢复(源码:sql/binlog_reader.cc 位置定位)
mysqlbinlog --start-position=100 --stop-position=500 \
mysql-bin.000001 | mysql -u root -p
# 5. 按 GTID 恢复(MySQL 5.6+,源码:sql/rpl_gtid.cc GTID 集合过滤)
mysqlbinlog --include-gtids="3E11FA47-71CA-11E1-9E33-C80AA9429562:1-100" \
--exclude-gtids="3E11FA47-71CA-11E1-9E33-C80AA9429562:50-60" \
mysql-bin.000001 | mysql -u root -p
# 6. 跳过 DROP / TRUNCATE 等危险操作(手动编辑过滤)
mysqlbinlog mysql-bin.000001 | grep -v "DROP TABLE" | mysql -u root -p
10.3 关键配置与最佳实践¶
Binlog 保留策略(源码:sql/rpl_master.cc purge_logs()):
-- 自动清理过期 Binlog(避免磁盘爆满)
expire_logs_days = 7 -- 保留 7 天
max_binlog_size = 100M -- 单个文件最大 100MB
binlog_expire_logs_seconds = 604800 -- 7 天(8.0 新参数)
-- 重要:生产环境至少保留 7-14 天,便于 PITR
-- 监控 Binlog 磁盘使用率,避免因磁盘满导致复制中断
备份恢复最佳实践:
# 1. 定期全量备份 + Binlog 持续归档
mysqldump --single-transaction --master-data=2 db_name > backup_$(date +%Y%m%d).sql
# 2. 备份时记录 Binlog 位置(--master-data=2 自动记录)
# -- CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000002', MASTER_LOG_POS=154;
# 3. 恢复时先应用全量备份,再重放 Binlog
mysql -u root -p db_name < backup_20240101.sql
mysqlbinlog --start-position=154 --stop-datetime="2024-01-01 12:00:00" \
mysql-bin.000002 | mysql -u root -p
# 4. 验证恢复结果
SELECT COUNT(*) FROM recovered_table; -- 检查数据量
SHOW SLAVE STATUS\G -- 检查复制状态(如果涉及主从)
📖 术语家族:Binlog 数据恢复(PITR)
字面义:Point-In-Time Recovery = 时间点恢复。 在 MySQL 中的含义:利用全量备份 + Binlog 增量重放将数据库恢复到任意历史时间点的技术,是数据库最后一道数据安全防线。 同家族成员(按恢复流程排列):
| 成员 | 作用 | 源码入口 | 关键参数 |
|---|---|---|---|
mysqlbinlog | Binlog 解析与重放工具 | sql/mysqlbinlog.cc mysqlbinlog_main() | --start-position / --stop-datetime / --exclude-gtids |
SHOW BINARY LOGS | 查看 Binlog 文件列表 | sql/rpl_master.cc show_binary_logs() | Log_name / File_size / Encrypted |
PURGE BINARY LOGS | 清理过期 Binlog | sql/rpl_master.cc purge_logs() | BEFORE / TO / expire_logs_days |
mysqldump --master-data | 备份时记录 Binlog 位置 | client/mysqldump.cc dump_slave_info() | MASTER_LOG_FILE / MASTER_LOG_POS |
CHANGE MASTER TO | 从库指定同步起点 | sql/rpl_slave.cc change_master() | MASTER_LOG_FILE / MASTER_LOG_POS / MASTER_AUTO_POSITION=1(GTID) |
RESET MASTER | 清空所有 Binlog(危险!) | sql/rpl_master.cc reset_master() | 重置 gtid_purged,慎用 |
命名规律:恢复相关操作带 recovery / restore 语义,Binlog 管理命令用 BINARY LOGS(复数),位置定位用 LOG_FILE + LOG_POS 组合。
11. 常见问题¶
📖 排查题 / 调优题 / 业务选型题(如「主从延迟怎么办」/「半同步要不要开」)已在 实战问题与避坑指南 给出工程视角答案,本文不再重复,专注「机制问答」。
Q:Binlog 和 Redo Log 的区别?
Redo Log 是 InnoDB 引擎层的物理日志,记录数据页的物理修改,用于崩溃恢复,循环写;Binlog 是 Server 层的逻辑日志,记录数据变更,用于主从复制和数据恢复,追加写不会覆盖。
Q:为什么事务提交的最终标志是 Xid Event 而不是 Query(COMMIT)?
Row 格式下
COMMIT通过XidEvent 承载,Xid 同时是 InnoDB 2PC 的链路 ID——崩溃恢复时 InnoDB 拿 Xid 到 Binlog 里找对应事务是否已落盘,以此决定 prepare 态事务提交或回滚。Query(COMMIT)仅在 Statement 格式时用来标记文本形式的提交。
Q:从库回放 Binlog 时的并行模型是怎么进化的?
MySQL 5.6 首次引入并行复制,但粒度是「不同库」(单库仍串行);5.7 改为
LOGICAL_CLOCK,基于主库事务的last_committed和sequence_number——同一 group commit 内的事务互不依赖、可并行回放;8.0 新增WRITESET模式(binlog_transaction_dependency_tracking=WRITESET),基于写集冲突检测,即使没赶上同一 group commit 的事务也可并行。 Q:为什么要用 GTID?GTID 让主从切换变得简单可靠。传统复制切换时需要手动找到新主库的 Binlog 文件名和 Position,容易出错;GTID 模式下从库自动根据全局唯一 ID 找到同步位置,大幅降低运维复杂度。
Q:半同步复制能保证数据不丢失吗?
不能完全保证。半同步只保证至少一个从库收到了 Binlog,但如果主库在收到 ACK 之前崩溃,这个事务可能已经在从库执行但主库没有提交,切换后会出现数据不一致。MGR(组复制)才能提供更强的一致性保证。