MySQL GTID 详解

MySQL 自 5.6 版本引入了 GTID(Global Transaction Identifier,全局事务标识)复制。相比传统的基于 binlog 文件名和 position 点的复制方式,GTID 复制对运维更加友好。它能够直接标识出事务是由哪个实例产生的,以及产生了多少个事务,极大地简化了主从切换和故障恢复的流程。

本文将深入讨论从库 SHOW SLAVE STATUS 输出中的 Retrieved_Gtid_SetExecuted_Gtid_Set 字段,并结合实际场景分析 GTID 的工作原理及异常处理。

核心状态变量解析

在 GTID 复制模式下,以下两个字段至关重要:

  • Retrieved_Gtid_Set:从库 IO 线程已经接收到主库的事务编号集合。
  • Executed_Gtid_Set:从库 SQL 线程自身已经执行完成的事务编号集合。

场景演示

1. 查看 Server UUID

首先,确认主库(Master)和从库(Slave)的 server_uuid。GTID 由 server_uuid 和事务序列号组成。

Master 节点:

[root@localhost][db1]> SHOW VARIABLES LIKE '%uuid%';       
+---------------+--------------------------------------+
| Variable_name | Value                                |
+---------------+--------------------------------------+
| server_uuid   | 2a09ee6e-645d-11e7-a96c-000c2953a1cb |
+---------------+--------------------------------------+
1 row in set (0.00 sec)

Slave 节点:

[root@localhost][(none)]> SHOW VARIABLES LIKE '%uuid%';
+---------------+--------------------------------------+
| Variable_name | Value                                |
+---------------+--------------------------------------+
| server_uuid   | 8ce853fc-6f8a-11e7-8940-000c29e3f5ab |
+---------------+--------------------------------------+
1 row in set (0.01 sec)

假设主库 server-id 为 10,从库 server-id 为 20。

2. 初始状态

搭建好主从复制后,如果没有数据写入,SHOW SLAVE STATUS 显示如下(关键字段):

Master_Server_Id: 10
Master_UUID: 2a09ee6e-645d-11e7-a96c-000c2953a1cb
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
Retrieved_Gtid_Set: 
Executed_Gtid_Set: 
Auto_Position: 1

此时 GTID 集合为空,表示尚未发生事务同步。

3. 主库写入事务

在主库创建表并插入 2 条数据(autocommit=1,共产生 3 个事务):

[root@localhost][db1]> CREATE TABLE t2 ( id int);
Query OK, 0 rows affected (0.07 sec)

[root@localhost][db1]> INSERT INTO t2 SELECT 1;
Query OK, 1 row affected (0.07 sec)

[root@localhost][db1]> INSERT INTO t2 SELECT 2;
Query OK, 1 row affected (0.02 sec)

从库状态检查:

Retrieved_Gtid_Set: 2a09ee6e-645d-11e7-a96c-000c2953a1cb:1-3
Executed_Gtid_Set:  2a09ee6e-645d-11e7-a96c-000c2953a1cb:1-3

主库状态检查:

+------------------+----------+--------------+------------------+------------------------------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                        |
+------------------+----------+--------------+------------------+------------------------------------------+
| mysql-bin.000001 |      912 |              |                  | 2a09ee6e-645d-11e7-a96c-000c2953a1cb:1-3 |
+------------------+----------+--------------+------------------+------------------------------------------+

分析:

  • 主库产生了 3 个事务,Executed_Gtid_SetUUID:1-3
  • 从库接收到了这 3 个事务(Retrieved_Gtid_Set),且已全部执行(Executed_Gtid_Set)。
  • 前缀 2a09ee6e-645d-11e7-a96c-000c2953a1cb 是主库的 server_uuid

通过解析主库 binlog 可以验证事务来源:

# at 154
#170823  0:38:38 server id 10  end_log_pos 219 CRC32 0x6268641f         GTID    last_committed=0        sequence_number=1
SET @@SESSION.GTID_NEXT= '2a09ee6e-645d-11e7-a96c-000c2953a1cb:1'/*!*/;
# at 219
#170823  0:38:38 server id 10  end_log_pos 316 CRC32 0x6c837618         Query   thread_id=103   exec_time=0     error_code=0
...
create table t2 ( id int)
/*!*/;

可以看到 server id 为 10,GTID_NEXT 明确标识了事务归属。这体现了 GTID 的核心优势:事务由谁产生、产生多少事务,被直接标识了出来。

4. 从库本地写入(Executed_Gtid_Set 变化)

有时我们会看到 Executed_Gtid_Set 中包含多个 UUID 段,例如:

Executed_Gtid_Set: 2a09ee6e-645d-11e7-a96c-000c2953a1cb:1-33, 8ce853fc-6f8a-11e7-8940-000c29e3f5ab:1
  • 2a09ee6e...:1-33:表示已执行主库的 1-33 号事务。
  • 8ce853fc...:1:这是从库自身的 UUID,表示从库本地执行过事务。

场景 NO.1:从库有数据写入

如果在从库直接插入数据:

[root@localhost][db1]> INSERT INTO t2 SELECT 1;
Query OK, 1 row affected (0.03 sec)

查看状态:

Retrieved_Gtid_Set: 2a09ee6e-645d-11e7-a96c-000c2953a1cb:1-3
Executed_Gtid_Set:  2a09ee6e-645d-11e7-a96c-000c2953a1cb:1-3, 8ce853fc-6f8a-11e7-8940-000c29e3f5ab:1

解析从库 binlog 可证实该事务由从库产生(server id 20):

# at 896
#170823  0:59:19 server id 20  end_log_pos 961 CRC32 0x0492528a         GTID    last_committed=3        sequence_number=4
SET @@SESSION.GTID_NEXT= '8ce853fc-6f8a-11e7-8940-000c29e3f5ab:1'/*!*/;
...
### INSERT INTO `db1`.`t2`
### SET
###  @1=1 /* INT meta=0 nullable=1 is_null=0 */

场景 NO.2:主从切换

如果使用 MHA 等工具进行主从切换,原从库变为主库。此时新主库的 server_uuid 会出现在复制链中。

Master_Server_Id: 20
Master_UUID: 8ce853fc-6f8a-11e7-8940-000c29e3f5ab
Retrieved_Gtid_Set: 8ce853fc-6f8a-11e7-8940-000c29e3f5ab:1
Executed_Gtid_Set:  2a09ee6e-645d-11e7-a96c-000c2953a1cb:1-3, 8ce853fc-6f8a-11e7-8940-000c29e3f5ab:1

这意味着新主库(原从库)接收并执行了自身之前产生的事务。若此时再插入数据,GTID 集合会继续累加:

Retrieved_Gtid_Set: 8ce853fc-6f8a-11e7-8940-000c29e3f5ab:1-2
Executed_Gtid_Set:  2a09ee6e-645d-11e7-a96c-000c2953a1cb:1-3, 8ce853fc-6f8a-11e7-8940-000c29e3f5ab:1-2

GTID 不连续与 Binlog 清理

有时会发现 GTID 集合不连续,例如 UUID:37-45,这通常是由于 binlog 被清理导致的。

Binlog 不可能永久保留,需定期清理(通过 expire_logs_days 控制)。gtid_purged 变量用于记录已经被清除的 binlog 事务集合,它是 gtid_executed 的子集。

  • gtid_executed:本机被执行并写入日志的 GTID 集合。
  • gtid_purged:本机已执行过,但对应的 binlog 已被 PURGE BINARY LOGS 清理的 GTID 集合。

只有当 gtid_executed 为空时(例如刚执行过 RESET MASTER),才能手动设置 gtid_purged,此时 gtid_executed 会同步更新为相同的值。

测试清理过程:

[root@localhost][db1]> SHOW MASTER LOGS;
+------------------+-----------+
| Log_name         | File_size |
+------------------+-----------+
| mysql-bin.000001 |      3530 |
+------------------+-----------+

[root@localhost][db1]> FLUSH LOGS;
Query OK, 0 rows affected (0.05 sec)

[root@localhost][db1]> PURGE BINARY LOGS TO 'mysql-bin.000002';
Query OK, 0 rows affected (0.01 sec)

MySQL 服务器启动时,会读取 binlog 文件初始化 gtid_executedgtid_purged,确保值与上次运行时一致。如果某些 GTID 对应的 binlog 已被清理,它们将出现在 gtid_purged 中。

异常处理:主库 Binlog 被清理

以下两个实验展示了当主库日志被清除,而从库尚未获取这些日志时的处理方法。

实验一:Slave 所需 GTID 在 Master 上已被 Purge

在 Master 上查看 GTID 相关变量:

master [localhost] {msandbox} (test) > SHOW GLOBAL VARIABLES LIKE '%gtid%';
+---------------------------------+----------------------------------------+
| Variable_name                   | Value                                  |
+---------------------------------+----------------------------------------+
| gtid_executed                   | 24024e52-bd95-11e4-9c6d-926853670d0b:1 |
| gtid_mode                       | ON                                     |
| gtid_purged                     |                                        |
+---------------------------------+----------------------------------------+

模拟产生事务并清理 binlog:

master [localhost] {msandbox} (test) > FLUSH LOGS; CREATE TABLE gtid_test2 (ID int) ENGINE=InnoDB;
master [localhost] {msandbox} (test) > FLUSH LOGS; CREATE TABLE gtid_test3 (ID int) ENGINE=InnoDB;
master [localhost] {msandbox} (test) > SHOW MASTER STATUS;
+------------------+----------+--------------+------------------+------------------------------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                        |
+------------------+----------+--------------+------------------+------------------------------------------+
| mysql-bin.000005 |      359 |              |                  | 24024e52-bd95-11e4-9c6d-926853670d0b:1-3 |
+------------------+----------+--------------+------------------+------------------------------------------+

master [localhost] {msandbox} (test) > PURGE BINARY LOGS TO 'mysql-bin.000004';

此时 gtid_purged 变为 24024e52-bd95-11e4-9c6d-926853670d0b:1

在 Slave2 上尝试建立复制:

slave2 [localhost] {msandbox} ((none)) > CHANGE MASTER TO MASTER_HOST='127.0.0.1', MASTER_PORT=21288, MASTER_USER='rsandbox', MASTER_PASSWORD='rsandbox', MASTER_AUTO_POSITION=1;
slave2 [localhost] {msandbox} ((none)) > START SLAVE;
slave2 [localhost] {msandbox} ((none)) > SHOW SLAVE STATUS\G
...
Last_IO_Errno: 1236
Last_IO_Error: Got fatal error 1236 from master when reading data from binary log: 'The slave is connecting using CHANGE MASTER TO MASTER_AUTO_POSITION = 1, but the master has purged binary logs containing GTIDs that the slave requires.'
...

错误原因:Slave 请求同步它缺失的事务,但 Master 告知这些事务对应的 binlog 已被清理。

实验二:忽略 Purged 部分,强行同步

在生产环境中,如果 DBA 确认 Slave 数据与 Master 一致(例如通过备份恢复),或者差异不影响后续业务(如仅涉及 INSERT 操作),可以跳过 Master 已 purge 的部分。

操作步骤:

  1. 确认 Master 上缺失的事务 GTID(本例为 24024e52-bd95-11e4-9c6d-926853670d0b:1)。
  2. 在 Slave 上停止复制,手动设置 gtid_purged
slave2 [localhost] {msandbox} ((none)) > STOP SLAVE;
Query OK, 0 rows affected (0.04 sec)

slave2 [localhost] {msandbox} ((none)) > SET GLOBAL gtid_purged = '24024e52-bd95-11e4-9c6d-926853670d0b:1';
Query OK, 0 rows affected (0.05 sec)

slave2 [localhost] {msandbox} ((none)) > START SLAVE;
Query OK, 0 rows affected (0.01 sec)

验证结果:

slave2 [localhost] {msandbox} ((none)) > SHOW SLAVE STATUS\G
...
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Retrieved_Gtid_Set: 24024e52-bd95-11e4-9c6d-926853670d0b:2-3
Executed_Gtid_Set:  24024e52-bd95-11e4-9c6d-926853670d0b:1-3
...

此时 Slave 已正常同步,并自动补齐了 2-3 范围的 binlog 日志,跳过了已 purge 的 1 号事务。

说明

  • 本文基于 MySQL 5.6+ 版本特性编写,GTID 功能在 5.7 及 8.0 版本中有所增强(如免从库只读限制、并行复制优化等),具体行为请以官方文档为准。
  • 手动设置 gtid_purged 需谨慎,务必确保数据一致性,否则可能导致主从数据差异。
  • 参考文档:MySQL Replication Options and Variables