目录
MySQL 复制中使用的事务类型有以下两种:
- GTID 事务:在二进制日志中每个 GTID 事务始终都以 Gtid_log_event 开头。可以使用 GTID 或使用文件名和位置来定位 GTID 事务。
- 匿名事务:MySQL 8 的二进制日志中的每个匿名事务都以 Anonymous_gtid_log_event 开头,不分配 GTID。匿名事务只能使用文件名和位置来定位。
GTID 出现之前,在一主多从的复制拓扑中,如果主库宕机,需要从多个从库选择之一作为新主库,这个过程比较复杂。没有一种直接了当的方法找到其它从库对应的新主库二进制日志坐标。通常的做法是先要寻找每个从库复制原主库的最后语句,然后找到新主库中包含该语句的二进制日志文件,其中该语句后的第一个事件位置即为连接新主库的二进制坐标。主要难点在于不存在一个唯一标识指出“复制原主库的最后语句”,于是后来的 MySQL 中就出现了 GTID 的概念。
一、GTID 简介
1. 什么是 GTID
全局事务标识符 GTID 的全称为 Global Transaction Identifier,是在整个复制环境中对一个事务的唯一标识。它是 MySQL 5.6 加入的一个强大特性,目的在于能够实现主从自动定位和切换,而不像以前需要指定文件和位置。使用 GTID 复制时,主库上提交事务时创建事务对应的 GTID,从库在应用中继日志时用 GTID 识别和跟踪每个事务。在启动新从库或因故障转移到新主库时可以使用 GTID 来标识复制的位置,极大地简化了这些任务。由于 GTID 的复制完全基于事务,因此只要在主库上提交的所有事务也在从库上提交,两者之间的一致性就得到保证。GTID 支持基于语句或基于行的复制格式,但为了获得最佳效果,MySQL 建议使用基于行的格式。GTID 始终保留在主库和从库上,这意味着可以通过检查其二进制日志来确定应用于任何从库的任何事务的来源。而且,一旦在给定库上提交了具有给定 GTID 的事务,则该库将忽略具有相同 GTID 的任何后续事务。因此,在主库上提交的事务只会在从库上应用一次,这也有助于保证一致性。
2. GTID 的格式与存储
(1)单个 GTID
GTID 与主库上提交的每个事务相关联。此标识符不仅对发起事务的库是唯一的,而且在给定复制拓扑中的所有库中都是唯一的。GTID 是用冒号分隔的一对坐标表示,例如:
8eed0f5b-6f9b-11e9-94a9-005056a57a4e:23
前一部分是主库的 server_uuid,后面一部分是主库上按提交事务的顺序确定的序列号,提交的事务序号从 1 开始。上面显式的 GTID 表示:具有 8eed0f5b-6f9b-11e9-94a9-005056a57a4e 的服务器上提交的第 23 个事务具有此 GTID。MySQL 5.6 以后用 128 位的 server_uuid 代替了原本的 32 位 server_id 的大部分功能。原因很简单,server_id 依赖于 my.cnf 的手工配置,很可能产生冲突。而自动产生 128 位 UUID 的算法可以保证所有的 MySQL UUID 都不会冲突。数据目录下的 auto.cnf 文件用来保存 server_uuid。MySQL 启动的时候会读取 auto.cnf 文件,如果没有读取到则会生成一个 server_uuid,并保存到 auto.cnf 文件中。
在主库上提交客户端事务时,如果事务已写入二进制日志,则会为其分配新的 GTID,保证为客户事务生成单调递增且没有间隙的 GTID。如果未将客户端事务写入二进制日志(例如,因为事务已被过滤掉,或者事务是只读的),则不会在源服务器上为其分配 GTID。从库上复制的事务保留与主库上事务相同的 GTID,即使从库上未开启二进制日志,GTID 也会被保存。MySQL 系统表 mysql.gtid_executed 用于保存 MySQL 服务器上应用的所有事务的 GTID,但存储在当前活动二进制日志文件中的事务除外。
GTID 的自动跳过功能意味着一旦在给定服务器上提交了具有给定 GTID 的事务,则该服务器将忽略使用相同 GTID 执行的任何后续事务(这种情况是可能发生的,如手工设置了 gtid_next 时)。这有助于保证主从一致性,因为在主库上提交的事务在从库上应用不超过一次。如果具有给定 GTID 的事务已开始在服务器上执行但尚未提交或回滚,则任何在该服务器上启动具有相同 GTID 的并发事务都将被阻止。服务器既不执行并发事务也不将控制权返回给客户端。一旦先前的事务提交或回滚,就可以继续执行同一 GTID 上被阻塞的并发会话。如果是回滚,则一个并发会话继续执行事务,并且在同一 GTID 上阻塞的任何其它并发会话仍然被阻止。如果是提交,则所有并发会话都将被阻止,并自动跳过事务的所有语句。mysqlbinlog 的输出中的 GTID_NEXT 包含事务的 GTID,用于标识复制中的单个事务。
下面做三个简单实验验证 GTID 的自动跳过功能。
- 实验1:验证自动跳过。
(1)准备初始数据
use test;
create table t1(a int);
create table t2(a int);
insert into t1 values(1),(2);
insert into t2 values(1),(2);
commit;
(2)查看当前 GTID
mysql> show master status;
+---------------+----------+--------------+------------------+--------------------------------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+---------------+----------+--------------+------------------+--------------------------------------------+
| binlog.000027 | 34614 | | | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-356 |
+---------------+----------+--------------+------------------+--------------------------------------------+
1 row in set (0.00 sec)
(3)将 GDIT 设置为已经执行过的值,再执行事务。
mysql> set gtid_next = '8eed0f5b-6f9b-11e9-94a9-005056a57a4e:356';
Query OK, 0 rows affected (0.00 sec)
mysql> truncate table test.t1;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test.t1;
+------+
| a |
+------+
| 1 |
| 2 |
+------+
2 rows in set (0.00 sec)
mysql> set gtid_next = automatic;
Query OK, 0 rows affected (0.00 sec)
mysql>
可以看到,服务器已经执行了 GTID 为 356 的事务,后续相同 GTID 的事务都被自动跳过,虽然 truncate 语句没有报错,但并未执行,数据无变化。
- 实验2:验证两个相同 GTID 事务,事务 1 提交,事务 2 被跳过。
(1)准备两个 SQL 脚本 s1.sql、s1.sql,gtid_next 是一个没用过的新值
s1.sql 内容如下:
set gtid_next='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:357';
begin;
delete from test.t1 where a=1;
select sleep(10);
commit;
set gtid_next=automatic;
s2.sql 内容如下:
set gtid_next='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:357';
begin;
delete from test.t2 where a=1;
commit;
set gtid_next=automatic;
(2)在会话 1 执行 s1.sql,并且在其 sleep 期间,在会话 2 执行 s2.sql
-- 会话1
mysql -uroot -p123456 test < s1.sql
-- 会话2
mysql -uroot -p123456 test < s2.sql
(3)查询数据
mysql> select * from t1;
+------+
| a |
+------+
| 2 |
+------+
1 row in set (0.00 sec)
mysql> select * from t2;
+------+
| a |
+------+
| 1 |
| 2 |
+------+
2 rows in set (0.00 sec)
mysql>
可以看到,事务 1 提交前,事务 2 被阻塞。事务 1 提交后,具有相同 GTID 的事务 2 被跳过。
- 实验3:验证两个相同 GTID 事务,事务 1 回滚,事务 2 提交。
(1)准备两个 SQL 脚本 s1.sql、s1.sql,gtid_next 是一个没用过的新值
s1.sql 内容如下:
set gtid_next='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:360';
begin;
delete from test.t1 where a=2;
select sleep(10);
rollback;
set gtid_next=automatic;
s2.sql 内容如下:
set gtid_next='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:360';
begin;
delete from test.t2 where a=1;
commit;
set gtid_next=automatic;
(2)在会话 1 执行 s1.sql,并且在其 sleep 期间,在会话 2 执行 s2.sql
-- 会话1
mysql -uroot -p123456 test < s1.sql
-- 会话2
mysql -uroot -p123456 test < s2.sql
(3)查询数据
mysql> select * from t1;
+------+
| a |
+------+
| 2 |
+------+
1 row in set (0.00 sec)
mysql> select * from t2;
+------+
| a |
+------+
| 2 |
+------+
1 row in set (0.00 sec)
mysql>
可以看到,事务 1 回滚前,事务 2 被阻塞。事务 1 回滚后,具有相同 GTID 的事务 2 被提交。
(2)GTID 集
GTID 集是包括一个或多个单个 GTID 或 GTID 范围的集合。源自同一服务器的一系列 GTID 可以折叠为单个表达式,例如:
8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-321
上面的示例表示源自 server_uuid 为 8eed0f5b-6f9b-11e9-94a9-005056a57a4e 服务器的第 1 到第 321 个事务。源自同一服务器的多个单 GTID 或 GTID 范围可以同时包含在由冒号分隔的单个表达式中,例如:
8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-3:11:47-49
GTID 集可以包括单个 GTID 和 GTID 范围的任意组合,甚至它可以包括源自不同服务器的 GTID。例如一个存储在从库 gtid_executed 系统变量中的 GTID 集可能如下:
565a6b0a-6f05-11e9-b95c-005056a5497f:1-20, 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-321
表示该从库已从两个主库应用了事务(也有可能是在从库执行的写操作)。当从库变量返回 GTID 集时,UUID 按字母顺序排列,并且数值间隔按升序合并。
MySQL 服务器中很多地方都用到 GTID 集,例如:gtid_executed 和 gtid_purged 系统变量存储的值是 GTID 集;START SLAVE 的 UNTIL SQL_BEFORE_GTIDS 和 UNTIL SQL_AFTER_GTIDS 子句的值是 GTID 集;内置函数 GTID_SUBSET() 和 GTID_SUBTRACT() 需要 GTID 集作为输入等。
(3)mysql.gtid_executed 表
mysql.gtid_executed 表结构如下:
mysql> desc mysql.gtid_executed;
+----------------+------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------------+------------+------+-----+---------+-------+
| source_uuid | char(36) | NO | PRI | NULL | |
| interval_start | bigint(20) | NO | PRI | NULL | |
| interval_end | bigint(20) | NO | | NULL | |
+----------------+------------+------+-----+---------+-------+
3 rows in set (0.00 sec)
mysql.gtid_executed 表记录的是服务器上已经执行事务的 GTID。三个字段分别表示发起事务的服务器 UUID、UUID 集的起始和结束事务 ID。对于单个 GTID,后两个字段的值相同。
mysql.gtid_executed 表供 MySQL 服务器内部使用。当从库禁用二进制日志时用该表记录 GTID,或者当二进制日志丢失时,可从该表查询 GTID 状态。RESET MASTER 命令将重置 mysql.gtid_executed 表(清空表数据)。和所有系统表一样,用户不要修改该表。
仅当 gtid_mode 设置为 ON 或 ON_PERMISSIVE 时,GTID 才存储在 mysql.gtid_executed 表中。存储的 GTID 值取决于是否开启二进制日志:
- 对于从库,如果禁用了二进制日志记录(skip-log-bin)或 log_slave_updates,则服务器将在该表中存储每个事务的 GTID。
- 如果启用了二进制日志记录,当刷新二进制日志或重启服务器时,服务器都会将当前二进制日志中所有事务的 GTID 写入 mysql.gtid_executed 表。这种情况适用于主库或启用了二进制日志记录的从库。
启用二进制日志记录时,mysql.gtid_executed 表并不保存所有已执行事务的 GTID 的完整记录,该信息由 gtid_executed 全局系统变量的值提供。如果服务器意外停止,则当前二进制日志文件中的 GTID 集不会保存在 mysql.gtid_executed 表中。在 MySQL 实例恢复期间,这些 GTID 将从二进制日志文件添加到表中。
即使服务器处于只读模式,MySQL 服务器也可以写入 mysql.gtid_executed 表,这样二进制日志文件仍然可以在只读模式下轮转。如果无法访问 mysql.gtid_executed 表时进行二进制日志文件轮转,则继续使用二进制日志文件存储 GTID,同时在服务器上记录警告信息:
2019-06-03T09:37:07.777423Z 287633 [Warning] [MY-010015] [Repl] Gtid table is not ready to be used. Table 'mysql.gtid_executed' cannot be opened.
前面已经提到,mysql.gtid_executed 表的记录可能并不是完整的已执行 GTID,而且有不可访问的可能性(例如误删除此表),因此建议始终通过查询 @@global.gtid_executed(每次提交后更新)来确认 MySQL 服务器的 GTID 状态,而不是查询 mysql.gtid_executed 表。
mysql.gtid_executed 表可能随着事务量的增多而快速膨胀,存储了源自同一服务器的大量不同的单个 GTID,这些 GTID 构成一个范围,例如:
+--------------------------------------+----------------+--------------+
| source_uuid | interval_start | interval_end |
+--------------------------------------+----------------+--------------+
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e | 1 | 329 |
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e | 330 | 330 |
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e | 331 | 331 |
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e | 332 | 332 |
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e | 333 | 333 |
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e | 334 | 334 |
+--------------------------------------+----------------+--------------+
为了节省空间,MySQL 服务器定期压缩 mysql.gtid_executed 表,方法是将每个这样的行集替换为跨越整个事务标识符间隔的单行,如下所示:
+--------------------------------------+----------------+--------------+
| source_uuid | interval_start | interval_end |
|--------------------------------------+----------------+--------------|
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e | 1 | 334 |
...
通过设置 gtid_executed_compression_period 系统变量,可以控制压缩表之前允许的事务数,从而控制压缩率。此变量的默认值为 1000,指的是在每 1000 次事务之后执行表的压缩。将 gtid_executed_compression_period 设置为 0 将不执行压缩。注意,启用二进制日志时不使用 gtid_executed_compression_period 的值,并在每个二进制日志轮转时压缩 mysql.gtid_executed 表。mysql.gtid_executed 表的压缩由名为 thread/sql/compress_gtid_table 的专用前台线程执行。此线程未在 SHOW PROCESSLIST 的输出中列出,但可以从 performance_schema.threads 中查询到:
mysql> select * from performance_schema.threads where name like '%gtid%'\G
*************************** 1. row ***************************
THREAD_ID: 44
NAME: thread/sql/compress_gtid_table
TYPE: FOREGROUND
PROCESSLIST_ID: 6
PROCESSLIST_USER: NULL
PROCESSLIST_HOST: NULL
PROCESSLIST_DB: NULL
PROCESSLIST_COMMAND: Daemon
PROCESSLIST_TIME: 438302
PROCESSLIST_STATE: Suspending
PROCESSLIST_INFO: NULL
PARENT_THREAD_ID: 1
ROLE: NULL
INSTRUMENTED: YES
HISTORY: YES
CONNECTION_TYPE: NULL
THREAD_OS_ID: 73199
RESOURCE_GROUP: SYS_default
1 row in set (0.00 sec)
mysql>
通常该线程都处于暂停状态,只有当满足条件时被唤醒,如达到 gtid_executed_compression_period 或发生了二进制日志轮转(如 flush logs 等)时。
下面做个简单实验展示一下 reset master 的作用和影响。
(1)查看从库当前已经执行的 GTID 和二进制日志
show master status;
show variables like 'gtid%';
select * from mysql.gtid_executed;
show slave status\G
查询结果如下:
mysql> show master status;
+---------------+----------+--------------+------------------+------------------------------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+---------------+----------+--------------+------------------+------------------------------------------+
| binlog.000004 | 195 | | | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-6 |
+---------------+----------+--------------+------------------+------------------------------------------+
1 row in set (0.00 sec)
mysql> show variables like 'gtid%';
+----------------------------------+------------------------------------------+
| Variable_name | Value |
+----------------------------------+------------------------------------------+
| gtid_executed | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-6 |
| gtid_executed_compression_period | 1000 |
| gtid_mode | ON |
| gtid_next | AUTOMATIC |
| gtid_owned | |
| gtid_purged | |
+----------------------------------+------------------------------------------+
6 rows in set (0.01 sec)
mysql> select * from mysql.gtid_executed;
+--------------------------------------+----------------+--------------+
| source_uuid | interval_start | interval_end |
+--------------------------------------+----------------+--------------+
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e | 1 | 6 |
+--------------------------------------+----------------+--------------+
1 row in set (0.00 sec)
mysql> show slave status\G
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 172.16.1.125
Master_User: repl
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: binlog.000001
Read_Master_Log_Pos: 1493
Relay_Log_File: hdp4-relay-bin.000005
Relay_Log_Pos: 315
Relay_Master_Log_File: binlog.000001
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 0
Last_Error:
Skip_Counter: 0
Exec_Master_Log_Pos: 1493
Relay_Log_Space: 682
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 1125
Master_UUID: 8eed0f5b-6f9b-11e9-94a9-005056a57a4e
Master_Info_File: mysql.slave_master_info
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
Master_Retry_Count: 86400
Master_Bind:
Last_IO_Error_Timestamp:
Last_SQL_Error_Timestamp:
Master_SSL_Crl:
Master_SSL_Crlpath:
Retrieved_Gtid_Set: 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-6
Executed_Gtid_Set: 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-6
Auto_Position: 1
Replicate_Rewrite_DB:
Channel_Name:
Master_TLS_Version:
Master_public_key_path:
Get_master_public_key: 0
Network_Namespace:
1 row in set (0.00 sec)
mysql>
所有查询显示的已经执行的 GTID 均为 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-6。
查看当前的 binlog 结果如下:
[mysql@hdp4/usr/local/mysql/data]$more binlog.index
./binlog.000001
./binlog.000002
./binlog.000003
./binlog.000004
[mysql@hdp4/usr/local/mysql/data]$ls -lt binlog.*
-rw-r----- 1 mysql mysql 64 Jun 5 14:43 binlog.index
-rw-r----- 1 mysql mysql 195 Jun 5 14:43 binlog.000004
-rw-r----- 1 mysql mysql 239 Jun 5 14:43 binlog.000003
-rw-r----- 1 mysql mysql 239 Jun 5 14:43 binlog.000002
-rw-r----- 1 mysql mysql 1569 Jun 5 14:43 binlog.000001
[mysql@hdp4/usr/local/mysql/data]$
当前从库有 4 个 binlog 文件。
(2)在从库执行 reset master
(3)再次执行(1)的查询
可以看到所有查询的 gtid_executed 都置空,binlog 文件只有 binlog.000001 一个。说明 reset master 命令会清空 gtid_executed 变量和 mysql.gtid_executed 表,并会只保留一个初始的 binlog 文件。
(4)在主库上执行一些更新
use test;
create table t1(a int);
insert into t1 select 1;
(5)再次执行(1)的查询
可以看到 mysql.gtid_executed 表中没有记录,其它查询都已显示出新执行 GTID 的值,复制正常。说明 mysql.gtid_executed 不记录当前 binlog 中的 GTID。
(6)在从库上执行 flush logs
现在 mysql.gtid_executed 表中存储了从 reset master 到 flush logs 之间 binlog 中的 GTID。
从以上步骤看到,从库上执行 reset master 只是清空从库的 gtid_executed,随着复制的继续,其 gtid_executed 的值也将随之变化,对复制和主从数据一致性没有影响。下面继续实验,看一下在主库上执行 reset master 会产生哪些影响。
(7)在主库上执行以下语句
use test;
delimiter //
create procedure p1(a int)
begin
declare i int default 1;
while i<=a do
insert into t1 values (i);
set i=i+1;
end while;
end;
//
delimiter ;
call p1(10000);
(8)在上一步执行期间,开一个新会话在主库上执行 reset master
(9)查看从库的复制状态
从 show slave status 的输出中可以看到复制的 IO 线程已停止,并报以下错误:
Last_IO_Errno: 13114
Last_IO_Error: Got fatal error 1236 from master when reading data from binary log: 'I/O error reading log event; the first event '' at 4, the last event read from './binlog.000001' at 201303, the last byte read from './binlog.000001' at 201303.'
由于主库正在执行事务中间进行了 reset master,从库无法读取主库的二进制日志而报错。更甚之,这些二进制日志的丢失是永久性的,结果很可能需要从头重建复制。由此实验得出的结论是,作为一条基本原则,不要随意在主库上执行 reset master,这样做极有可能导致复制停止或造成主从数据不一致等严重后果,而且不易恢复。
二、GTID 生命周期
1. 典型事务的 GTID 生命周期
典型事务的 GTID 的生命周期中包括以下步骤:
- 客户端事务在主库上执行并提交,此事务被分配一个 GTID,该 GTID 由主服务器的 UUID 和此服务器上尚未使用的最小非零事务序列号组成。GTID 作为 Gtid_log_event 紧接在事务本身之前,与事务本身一起被写入主库的二进制日志,这是一个原子操作。如果未将客户端事务写入二进制日志(例如,因为事务已被过滤掉,或者事务是只读的),则不会为其分配 GTID。轮转二进制日志或关闭 MySQL 实例时,都会将写入之前二进制日志文件的所有事务的 GTID 写入 mysql.gtid_executed 表。
- 如果为事务分配了 GTID,则将 GTID 添加到主库 gtid_executed 系统变量(@@global.gtid_executed)的 GTID 集合中,这步将在事务提交后进行,并且与事务处理本身不是一个原子操作。gtid_executed 系统变量包含所有已提交事务的 GTID 集,是应用事务的完整记录,并在复制中用作表示服务器状态的标记。mysql.gtid_executed 表中不包含当前二进制日志文件中的最新 GTID 记录。
- 在将二进制日志数据传输到从库并存储在从库的中继日志中之后,从库读取 GTID 并将其设置为 gtid_next 系统变量的值。这告诉从库必须使用此 GTID 记录下一个事务。
- 在处理事务本身之前,从库首先读取和检查复制事务的 GTID,不仅保证没有先前事务具有此 GTID,而且还保证没有其它会话已经读取此 GTID 但尚未提交相关事务。因此,如果多个客户端同时提交同一 GTID 事务,则服务器只允许其中一个执行。从库的 gtid_owned 系统变量(@@global.gtid_owned)显示当前正在使用的 GTID 以及拥有它的线程 ID。如果已经使用了该 GTID,通过自动跳过功能忽略具该事务,并且不会引发错误。
- 如果 GTID 尚未使用,则从库应用复制的事务。gtid_next 已经被设置为主库已分配的 GTID,从库不会为此事务生成新的 GTID,而是使用存储在 gtid_next 中的 GTID。
- 如果在从库上启用了二进制日志记录,则与主库操作类似。GTID 会在提交时作为 Gtid_log_event 原子写入其二进制日志。当轮转二进制日志或关闭 MySQL 实例时,都会将写入之前二进制日志文件的所有事务的 GTID 写入 mysql.gtid_executed 表。
- 如果从库禁用二进制日志记录,则通过将 GTID 直接写入 mysql.gtid_executed 表保留 GTID。MySQL 会在事务中附加一条语句,将 GTID 插入该表中。从 MySQL 8.0 开始,此操作对于 DDL 语句和 DML 语句都是原子操作。在这种情况下,mysql.gtid_executed 表是从库上应用事务的完整记录。
- 从库提交复制事务后,GTID 将被添加到从库 gtid_executed 系统变量(@@global.gtid_executed)的 GTID 集合中,这步将在事务应用后进行,并且与事务处理本身不是一个原子操作。
主库上过滤掉的客户端事务未分配 GTID,因此它们不会添加到 gtid_executed 系统变量中的事务集中,也不会添加到 mysql.gtid_executed 表中。但是,在从库上过滤掉的复制事务的 GTID 是持久化的。如果在从库上启用了二进制日志,则过滤掉的事务将作为 Gtid_log_event 写入其二进制日志,后跟仅包含 BEGIN 和 COMMIT 语句的空事务。如果禁用二进制日志,则已过滤掉的事务的 GTID 将写入 mysql.gtid_executed 表。为过滤掉的事务保留 GTID 确保可以将 mysti.gtid_executed 表和 gtid_executed 系统变量中的 GTID 用 GTID 集表示。它还确保如果从库重新连接到主库,不会再次检索过滤掉的事务。
在主库或单线程复制的从库上,GTID 从 1 开始单向递增且没有间隙。但在多线程复制的从库(slave_parallel_workers > 0)上,可以并行应用事务,因此复制的事务可能无序提交(除非设置了 slave_preserve_commit_order = 1)。发生这种情况时,gtid_executed 系统变量中的 GTID 集合将包含多个 GTID 范围,它们之间存在间隙。多线程复制从库上的间隙仅发生在最近应用的事务中,并在复制过程中填充。当使用 STOP SLAVE 语句干净地停止复制线程时,将应用正在进行的事务以填补空白。如果发生异常关闭,例如服务器故障或使用 KILL 语句停止复制线程,则可能依然存在间隙。
下面实验中将演示 GTID 存在间隙的情况。
(1)在从库开启多线程复制。
set global slave_parallel_workers=8;
stop slave;
start slave;
show processlist;
在最后的输出中可以看到 8 个复制线程:
+------+-----------------+-----------+------+---------+------+--------------------------------------------------------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+------+-----------------+-----------+------+---------+------+--------------------------------------------------------+------------------+
| 4 | event_scheduler | localhost | NULL | Daemon | 1657 | Waiting on empty queue | NULL |
| 11 | wxy | localhost | NULL | Query | 0 | starting | show processlist |
| 1122 | system user | | NULL | Connect | 2 | Waiting for master to send event | NULL |
| 1123 | system user | | NULL | Query | 2 | Slave has read all relay log; waiting for more updates | NULL |
| 1124 | system user | | NULL | Connect | 2 | Waiting for an event from Coordinator | NULL |
| 1125 | system user | | NULL | Connect | 2 | Waiting for an event from Coordinator | NULL |
| 1126 | system user | | NULL | Connect | 2 | Waiting for an event from Coordinator | NULL |
| 1127 | system user | | NULL | Connect | 2 | Waiting for an event from Coordinator | NULL |
| 1128 | system user | | NULL | Connect | 2 | Waiting for an event from Coordinator | NULL |
| 1129 | system user | | NULL | Connect | 2 | Waiting for an event from Coordinator | NULL |
| 1130 | system user | | NULL | Connect | 2 | Waiting for an event from Coordinator | NULL |
| 1131 | system user | | NULL | Connect | 2 | Waiting for an event from Coordinator | NULL |
+------+-----------------+-----------+------+---------+------+--------------------------------------------------------+------------------+
12 rows in set (0.00 sec)
(2)在主库上执行一个可以并行复制的长操作。
因为并行复制缺省是按数据库分配线程的,所以建立多个库表:
create database db1;
create database db2;
create database db3;
create database db4;
create database db5;
create database db6;
create database db7;
create database db8;
create table db1.t1(a int);
create table db2.t1(a int);
create table db3.t1(a int);
create table db4.t1(a int);
create table db5.t1(a int);
create table db6.t1(a int);
create table db7.t1(a int);
create table db8.t1(a int);
use test;
delimiter //
create procedure p1(a int)
begin
declare i int default 1;
while i<=a do
insert into db1.t1 values (i);
insert into db2.t1 values (i);
insert into db3.t1 values (i);
insert into db4.t1 values (i);
insert into db5.t1 values (i);
insert into db6.t1 values (i);
insert into db7.t1 values (i);
insert into db8.t1 values (i);
set i=i+1;
end while;
end;
//
delimiter ;
call p1(5000);
(3)在上一步正在执行过程当中杀掉从库的 mysqld 进程,模拟异常宕机。
ps -ef | grep mysqld | grep -v grep | awk {'print $2'} | xargs kill -9
(4)启动从库,不自动启动复制。
mysqld_safe --defaults-file=/etc/my.cnf --skip-slave-start --slave_parallel_workers=8 &
(5)查看从库的 GTID 间隙。
mysql> show variables like 'gtid_executed'\G
*************************** 1. row ***************************
Variable_name: gtid_executed
Value: 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-42171:42173-42179:42181-42187:42189-42195:42197-42203:42205-42211:42213-42219:42221-42227:42229-42235:42237-42243:42245-42251:42253-42259:42261-42267:42269-42275:42277-42283:42285-42291:42293-42299:42301-42307:42309-42315:42317-42323:42325-42331:42333-42339:42341-42347:42349-42355:42357-42363:42365-42371:42373-42379:42381-42387:42389-42395:42397-42403:42405-42411:42413-42419:42421-42427:42429-42435:42437-42443:42445-42451:42453-42459:42461-42467:42469-42475:42477-42483:42485-42491:42493-42499:42501-42507:42509-42515:42517-42523:42525-42531:42533-42539:42541-42547:42549-42555:42557-42563:42565-42571:42573-42579:42581-42587:42589-42595:42597-42603:42605-42611:42613:42615-42619:42621:42623-42627:42629:42631-42635:42637:42639-42643
1 row in set (0.01 sec)
mysql>
GTID 范围的输出是排序的,可以看到 42172、42180、42188、42196 ... 这些 GTID 没有出现在 gtid_executed 变量中,这些就是 GTID 间隙。查询各个库的记录数(已经执行的事务)也是各不相同。
(6)启动从库的复制,检查复制情况。
start slave;
当所有事务都执行完后,再次查看 gtid_executed 系统变量,已经合并为一个 GTID 范围,所有间隙都已经被填充:
mysql> show variables like 'gtid_executed'\G
*************************** 1. row ***************************
Variable_name: gtid_executed
Value: 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-50021
1 row in set (0.00 sec)
mysql>
从 show slave status 的输出和各个库表的记录数,也能确认复制正常。从这个简单的实验可以看到,启用并行复制的从库,在复制期间从库实例异常终止会产生 GTID 间隙,但在实例重启后复制会自动填充 GTID 间隙,最终达到主从数据一致。
2. GTID 分配
典型情况是服务器为已提交的事务生成新的 GTID。写入二进制日志的每个数据库更改(DDL 或 DML)都会分配一个 GTID。这包括自动提交的更改以及使用 BEGIN 和 COMMIT 或 START TRANSACTION 语句提交的更改。当数据库,以及非表数据库对象,例如过程、函数、触发器、事件、视图、用户、角色在创建、更改或删除时会分配 GTID。授权语句和非事务表的更新也会分配 GTID。
当二进制日志中的生成语句自动删除表时,会为该语句分配 GTID。例如,当具有打开临时表的用户会话断开连接时,将自动删除临时表,或者使用 MEMORY 存储引擎的表在服务器启动后第一次访问时会自动删除。
未写入二进制日志事务不会分配 GTID。这包括回滚的事务,或在禁用二进制日志时执行的事务,或指定 sql_log_bin=0 时执行的事务,或空事务(begin;commit;)等。
XA 事务为事务的 XA PREPARE 阶段和事务的 XA COMMIT 或 XA ROLLBACK 阶段分配了单独的 GTID。XA 事务的准备阶段是持久化的,以便用户可以在发生故障时将其提交或回滚。因此,事务的两个部分是分开复制的,因此两个阶段必须有自己单独的 GTID。
在以下特殊情况下,单个语句可以生成多个事务,因此会分配多个 GTID:
- 调用存储过程时,为过程提交的每个更新事务生成一个 GTID。
- 多表 DROP TABLE 语句中包含任何不支持原子 DDL 存储引擎的表(如myisam)或临时表,会生成多个 GTID。
注意,触发器内的语句和触发它的语句是在一个事务中,因此不会单独分配 GTID。MySQL 不支持类似 Oracle 自治事务的功能。
3. gtid_next 系统变量
gtid_next 是会话系统变量。默认情况下,对于在用户会话中提交的新事务,服务器会自动生成并分配新的 GTID。在从库上应用事务时,将保留来自原始服务器的 GTID。可以通过设置 gtid_next 系统变量的会话值来更改此行为:
- 当 gtid_next 设置为 AUTOMATIC(默认值),并且事务已提交并写入二进制日志时,服务器会自动生成并分配新的 GTID。如果由于其它原因而回滚事务或未将事务写入二进制日志,则服务器不会生成和分配 GTID。
- 如果将 gtid_next 设置为有效的单个 GTID(由 UUID 和事务序列号组成,用冒号分隔),服务器会将该 GTID 分配给下一个事务。只要事务提交,就会将此 GTID 分配并添加到 gtid_executed。
在将 gtid_next 设置为已提交或回滚事务之后的特定 GTID 时,必须在任何其它语句之前发出显式 SET @@SESSION.gtid_next 语句。如果不想分配更多 GTID,可以将此选项值的值设置回 AUTOMATIC。
mysql> show variables like 'gtid%';
+----------------------------------+----------------------------------------------+
| Variable_name | Value |
+----------------------------------+----------------------------------------------+
| gtid_executed | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-50057 |
| gtid_executed_compression_period | 1000 |
| gtid_mode | ON |
| gtid_next | AUTOMATIC |
| gtid_owned | |
| gtid_purged | |
+----------------------------------+----------------------------------------------+
6 rows in set (0.00 sec)
mysql> set gtid_next='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50058';
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'gtid%';
+----------------------------------+----------------------------------------------+
| Variable_name | Value |
+----------------------------------+----------------------------------------------+
| gtid_executed | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-50057 |
| gtid_executed_compression_period | 1000 |
| gtid_mode | ON |
| gtid_next | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50058 |
| gtid_owned | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50058 |
| gtid_purged | |
+----------------------------------+----------------------------------------------+
6 rows in set (0.00 sec)
mysql> begin;commit;
Query OK, 0 rows affected (0.00 sec)
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'gtid%';
+----------------------------------+----------------------------------------------+
| Variable_name | Value |
+----------------------------------+----------------------------------------------+
| gtid_executed | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-50058 |
| gtid_executed_compression_period | 1000 |
| gtid_mode | ON |
| gtid_next | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50058 |
| gtid_owned | |
| gtid_purged | |
+----------------------------------+----------------------------------------------+
6 rows in set (0.01 sec)
mysql> create table t1(a int);
ERROR 1837 (HY000): When @@SESSION.GTID_NEXT is set to a GTID, you must explicitly set it to a different value after a COMMIT or ROLLBACK. Please check GTID_NEXT variable manual page for detailed explanation. Current @@SESSION.GTID_NEXT is '8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50058'.
mysql> set gtid_next=automatic;
Query OK, 0 rows affected (0.00 sec)
mysql> create table t1(a int);
Query OK, 0 rows affected (0.01 sec)
mysql>
前面已经提到,从库的 SQL 线程应用复制事务时使用此技术,将 @@SESSION.gtid_next 显式设置为在源服务器上分配给事务的 GTID。这意味着保留来自原始服务器的 GTID,而不是由从库生成和分配的新 GTID。即使从库禁用 log_bin 或 log_slave_updates,或者事务是空操作或在从库上过滤掉时,GTID 也会添加到从库上的 gtid_executed。
客户端可以通过在执行事务之前将 @@SESSION.gtid_next 设置为特定 GTID 来模拟复制的事务。mysqlbinlog 使用此技术生成二进制日志的转储,客户端可以重放该转储以保留 GTID。通过客户端提交的模拟复制事务完全等同于通过复制应用程序线程提交的复制事务,并且事后无法区分它们。
[mysql@hdp2/usr/local/mysql/data]$mysqlbinlog --base64-output=decode-rows binlog.000001 | tail -15
/*!80001 SET @@session.original_commit_timestamp=1559800983100268*//*!*/;
/*!80014 SET @@session.original_server_version=80016*//*!*/;
/*!80014 SET @@session.immediate_server_version=80016*//*!*/;
SET @@SESSION.GTID_NEXT= '8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50059'/*!*/;
# at 13622355
#190606 14:03:03 server id 1125 end_log_pos 13622465 CRC32 0xbf6bf581 Query thread_id=184 exec_time=0 error_code=0 Xid = 601312
SET TIMESTAMP=1559800983/*!*/;
/*!80013 SET @@session.sql_require_primary_key=0*//*!*/;
create table t1(a int)
/*!*/;
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;
[mysql@hdp2/usr/local/mysql/data]$
4. gtid_purged 系统变量
gtid_purged 是全局系统变量。@@GLOBAL.gtid_purged 中的 GTID 集包含已在服务器上提交但在服务器上的任何二进制日志文件中不存在的所有事务的 GTID。gtid_purged 是gtid_executed 的子集。以下类别的 GTID 位于 gtid_purged 中:
- 在从库上禁用二进制日志记录时提交的复制事务的 GTID。
- 已清除的二进制日志文件中事务的 GTID。
- 通过语句 SET @@GLOBAL.gtid_purged 明确添加到集合中的 GTID。
第一种情况:
[mysql@hdp4~]$mysqladmin -uroot -p123456 shutdown
mysqladmin: [Warning] Using a password on the command line interface can be insecure.
[mysql@hdp4~]$mysqld_safe --defaults-file=/etc/my.cnf --skip-log-bin &
[1] 97160
[mysql@hdp4~]$2019-06-06T06:25:56.483366Z mysqld_safe Logging to '/usr/local/mysql/data/hdp4.err'.
2019-06-06T06:25:56.544557Z mysqld_safe Starting mysqld daemon with databases from /usr/local/mysql/data
[mysql@hdp4~]$mysql -uroot -p123456 -e "show variables like 'gtid_purged'"
mysql: [Warning] Using a password on the command line interface can be insecure.
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| gtid_purged | |
+---------------+-------+
... 主库执行更新 ...
[mysql@hdp4~]$mysql -uroot -p123456 -e "show variables like 'gtid_purged'"
mysql: [Warning] Using a password on the command line interface can be insecure.
+---------------+--------------------------------------------+
| Variable_name | Value |
+---------------+--------------------------------------------+
| gtid_purged | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50060 |
+---------------+--------------------------------------------+
[mysql@hdp4~]$
第二种情况:
mysql> show binary logs;
+---------------+-----------+-----------+
| Log_name | File_size | Encrypted |
+---------------+-----------+-----------+
| binlog.000001 | 13683049 | No |
+---------------+-----------+-----------+
1 row in set (0.00 sec)
mysql> show variables like 'gtid_purged';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| gtid_purged | |
+---------------+-------+
1 row in set (0.00 sec)
mysql> flush logs;
Query OK, 0 rows affected (0.01 sec)
mysql> purge master logs to 'binlog.000002';
Query OK, 0 rows affected (0.01 sec)
mysql> show variables like 'gtid_purged';
+---------------+----------------------------------------------+
| Variable_name | Value |
+---------------+----------------------------------------------+
| gtid_purged | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-50060 |
+---------------+----------------------------------------------+
1 row in set (0.01 sec)
mysql>
第三种情况:
mysql> show variables like 'gtid%';
+----------------------------------+--------------------------------------------------+
| Variable_name | Value |
+----------------------------------+--------------------------------------------------+
| gtid_executed | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50060-50061 |
| gtid_executed_compression_period | 1000 |
| gtid_mode | ON |
| gtid_next | AUTOMATIC |
| gtid_owned | |
| gtid_purged | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50060 |
+----------------------------------+--------------------------------------------------+
6 rows in set (0.01 sec)
mysql> set gtid_purged='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50060-50061';
ERROR 1229 (HY000): Variable 'gtid_purged' is a GLOBAL variable and should be set with SET GLOBAL
mysql> set global gtid_purged='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50060-50061';
ERROR 3546 (HY000): @@GLOBAL.GTID_PURGED cannot be changed: the added gtid set must not overlap with @@GLOBAL.GTID_EXECUTED
mysql> set global gtid_purged='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:50061';
ERROR 3546 (HY000): @@GLOBAL.GTID_PURGED cannot be changed: the new value must be a superset of the old value
mysql> set global gtid_purged='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-50059';
ERROR 3546 (HY000): @@GLOBAL.GTID_PURGED cannot be changed: the new value must be a superset of the old value
mysql> set global gtid_purged='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-50059:50060';
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'gtid%';
+----------------------------------+----------------------------------------------+
| Variable_name | Value |
+----------------------------------+----------------------------------------------+
| gtid_executed | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-50061 |
| gtid_executed_compression_period | 1000 |
| gtid_mode | ON |
| gtid_next | AUTOMATIC |
| gtid_owned | |
| gtid_purged | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-50060 |
+----------------------------------+----------------------------------------------+
6 rows in set (0.01 sec)
mysql>
可以更改 gtid_purged 的值,以便在服务器上记录已应用某个 GTID 集中的事务,尽管它们不存在于服务器上的任何二进制日志中。将 GTID 添加到 gtid_purged 时,它们也会添加到 gtid_executed 中。来看一个相对极端的例子。
(1)从库清除二进制日志和 gtid_executed 信息
reset master;
stop slave;
reset slave all;
show variables like 'gtid%';
发出 RESET MASTER 会导致 gtid_purged 和 gtid_executed 的全局值重置为空字符串。最后的输出为:
mysql> show variables like 'gtid%';
+----------------------------------+-----------+
| Variable_name | Value |
+----------------------------------+-----------+
| gtid_executed | |
| gtid_executed_compression_period | 1000 |
| gtid_mode | ON |
| gtid_next | AUTOMATIC |
| gtid_owned | |
| gtid_purged | |
+----------------------------------+-----------+
6 rows in set (0.00 sec)
(2)重置复制
change master to
master_host = '172.16.1.125',
master_port = 3306,
master_user = 'repl',
master_password = '123456',
master_auto_position = 1;
start slave;
show slave status\G
最后的输出为:
mysql> show slave status\G
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 172.16.1.125
Master_User: repl
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: binlog.000001
Read_Master_Log_Pos: 2731306
Relay_Log_File: hdp4-relay-bin.000002
Relay_Log_Pos: 363
Relay_Master_Log_File: binlog.000001
Slave_IO_Running: Yes
Slave_SQL_Running: No
Replicate_Do_DB:
Replicate_Ignore_DB:
Replicate_Do_Table:
Replicate_Ignore_Table:
Replicate_Wild_Do_Table:
Replicate_Wild_Ignore_Table:
Last_Errno: 1007
Last_Error: Error 'Can't create database 'test'; database exists' on query. Default database: 'test'. Query: 'create database test'
Skip_Counter: 0
Exec_Master_Log_Pos: 155
Relay_Log_Space: 2731721
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: NULL
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 1007
Last_SQL_Error: Error 'Can't create database 'test'; database exists' on query. Default database: 'test'. Query: 'create database test'
Replicate_Ignore_Server_Ids:
Master_Server_Id: 1125
Master_UUID: 8eed0f5b-6f9b-11e9-94a9-005056a57a4e
Master_Info_File: mysql.slave_master_info
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State:
Master_Retry_Count: 86400
Master_Bind:
Last_IO_Error_Timestamp:
Last_SQL_Error_Timestamp: 190606 15:52:51
Master_SSL_Crl:
Master_SSL_Crlpath:
Retrieved_Gtid_Set: 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-10005
Executed_Gtid_Set:
Auto_Position: 1
Replicate_Rewrite_DB:
Channel_Name:
Master_TLS_Version:
Master_public_key_path:
Get_master_public_key: 0
Network_Namespace:
1 row in set (0.00 sec)
可以看到,从主库读到的 GTID 已经到了 10005,但没有已经执行的 GTID。实际上这些事务都已经在从库应用了,只是由于 reset master 而没有留下执行的痕迹,所以要从 1 开始执行,而重复执行事务造成了错误。
(3)将所有已重放的 GTID 都标记为已执行,然后重启复制
set global gtid_purged='8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-10005';
stop slave;
start slave;
show slave status\G
show variables like 'gtid%';
从 show slave status 的输出中可以看到复制已恢复正常,最后的输出为:
mysql> show variables like 'gtid%';
+----------------------------------+----------------------------------------------+
| Variable_name | Value |
+----------------------------------+----------------------------------------------+
| gtid_executed | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-10005 |
| gtid_executed_compression_period | 1000 |
| gtid_mode | ON |
| gtid_next | AUTOMATIC |
| gtid_owned | |
| gtid_purged | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-10005 |
+----------------------------------+----------------------------------------------+
6 rows in set (0.00 sec)
服务器启动时,将初始化 gtid_executed 和 gtid_purged 系统变量中的 GTID 集。每个二进制日志文件都以事件 Previous_gtids_log_event 开头,该事件包含所有先前二进制日志文件中的 GTID 集(由前一个文件的 Previous_gtids_log_event 中的 GTID 和前一个文件本身中每个 Gtid_log_event 的 GTID 组成)。最旧和最新的二进制日志文件中的 Previous_gtids_log_event 的内容用于计算服务器启动时的 gtid_executed 和 gtid_purged 的 GTID 集:
- gtid_executed 是最新二进制日志文件中 Previous_gtids_log_event 中的 GTID、该二进制日志文件中的事务的 GTID、存储在 mysql.gtid_executed 表中的GTID,三者的并集。此 GTID 集包含服务器上已使用(或显式添加到 gtid_purged)的所有 GTID,无论它们当前是否位于服务器上的二进制日志文件中。它不包括当前正在服务器上正在处理事务的 GTID(@@GLOBAL.gtid_owned)。
- gtid_purged 的计算方法是首先添加最新二进制日志文件 Previous_gtids_log_event 中的 GTID,再添加该二进制日志文件中事务的 GTID。此步骤提供当前或曾经记录在服务器上的二进制日志中的 GTID 集(gtids_in_binlog)。然后从 gtids_in_binlog 中减去最旧的二进制日志文件中的 Previous_gtids_log_event 中的 GTID。此步骤提供当前记录在服务器上的二进制日志中的 GTID 集(gtids_in_binlog_not_purged)。最后,从 gtid_executed 中减去 gtids_in_binlog_not_purged。结果是服务器上已经执行,但当前未记录在服务器上的二进制日志文件中的 GTID 集,此结果用于初始化 gtid_purged。
下面用一个的例子说明 gtid_executed 和 gtid_purged 的计算过程。
mysql> show variables like 'gtid%';
+----------------------------------+----------------------------------------------+
| Variable_name | Value |
+----------------------------------+----------------------------------------------+
| gtid_executed | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-11006 |
| gtid_executed_compression_period | 1000 |
| gtid_mode | ON |
| gtid_next | AUTOMATIC |
| gtid_owned | |
| gtid_purged | 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-10005 |
+----------------------------------+----------------------------------------------+
6 rows in set (0.01 sec)
服务器重启重启后 gtid_executed 的值为 1-11006,gtid_purged 值为 1-10005,下面倒推这些数是怎么得来的。
当前有三个二进制日志文件,最老的是 binlog.000001,最新的是 binlog.000003:
[mysql@hdp4/usr/local/mysql/data]$ls -lt binlog.*
-rw-r----- 1 mysql mysql 195 Jun 6 16:02 binlog.000003
-rw-r----- 1 mysql mysql 48 Jun 6 16:02 binlog.index
-rw-r----- 1 mysql mysql 275358 Jun 6 16:01 binlog.000002
-rw-r----- 1 mysql mysql 199 Jun 6 16:00 binlog.000001
[mysql@hdp4/usr/local/mysql/data]$
binlog.000001 的 Previous-GTIDs 为空,文件本身也没有 GTID:
[mysql@hdp4/usr/local/mysql/data]$mysqlbinlog --base64-output=decode-rows binlog.000001
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 4
#190606 15:52:18 server id 1127 end_log_pos 124 CRC32 0x57a0d989 Start: binlog v 4, server v 8.0.16 created 190606 15:52:18 at startup
ROLLBACK/*!*/;
# at 124
#190606 15:52:18 server id 1127 end_log_pos 155 CRC32 0x69663b35 Previous-GTIDs
# [empty]
# at 155
#190606 16:00:15 server id 1127 end_log_pos 199 CRC32 0x08490198 Rotate to binlog.000002 pos: 4
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;
[mysql@hdp4/usr/local/mysql/data]$
binlog.000002 的 Previous-GTIDs 由 binlog.000001的 Previous-GTIDs 和 binlog.000001 本身的 GTID 组成,由于两者都为空,所以 binlog.000002 的 Previous-GTIDs 也为空:
[mysql@hdp4/usr/local/mysql/data]$mysqlbinlog --base64-output=decode-rows binlog.000002 | head -10
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 4
#190606 16:00:15 server id 1127 end_log_pos 124 CRC32 0x66692a6f Start: binlog v 4, server v 8.0.16 created 190606 16:00:15
# at 124
#190606 16:00:15 server id 1127 end_log_pos 155 CRC32 0x2c439049 Previous-GTIDs
# [empty]
# at 155
#190606 16:00:35 server id 1125 end_log_pos 239 CRC32 0x4bace6c6 GTID last_committed=0 sequence_number=1 rbr_only=no original_committed_timestamp=1559808035672038 immediate_commit_timestamp=1559808035637464 transaction_length=180
[mysql@hdp4/usr/local/mysql/data]$
binlog.000002 本身的 GTID 为 10006-11006:
[mysql@hdp4/usr/local/mysql/data]$mysqlbinlog --base64-output=decode-rows binlog.000002 > binlog.000002.txt
[mysql@hdp4/usr/local/mysql/data]$grep @@SESSION.GTID_NEXT binlog.000002.txt | head -1
SET @@SESSION.GTID_NEXT= '8eed0f5b-6f9b-11e9-94a9-005056a57a4e:10006'/*!*/;
[mysql@hdp4/usr/local/mysql/data]$grep @@SESSION.GTID_NEXT binlog.000002.txt | tail -2
SET @@SESSION.GTID_NEXT= '8eed0f5b-6f9b-11e9-94a9-005056a57a4e:11006'/*!*/;
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
[mysql@hdp4/usr/local/mysql/data]$
binlog.000003 的 Previous-GTIDs 由 binlog.000002 的 Previous-GTIDs 和 binlog.000002 本身的 GTID 组成,所以 binlog.000003 的 Previous-GTIDs 为 10006-11006:
[mysql@hdp4/usr/local/mysql/data]$mysqlbinlog --base64-output=decode-rows binlog.000003
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 4
#190606 16:02:02 server id 1127 end_log_pos 124 CRC32 0x9443c747 Start: binlog v 4, server v 8.0.16 created 190606 16:02:02 at startup
# Warning: this binlog is either in use or was not closed properly.
ROLLBACK/*!*/;
# at 124
#190606 16:02:02 server id 1127 end_log_pos 195 CRC32 0xbcd9d46f Previous-GTIDs
# 8eed0f5b-6f9b-11e9-94a9-005056a57a4e:10006-11006
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;
[mysql@hdp4/usr/local/mysql/data]$
binlog.000003 本身没有 GTID。
mysql.gtid_executed 的记录为:
mysql> select * from mysql.gtid_executed;
+--------------------------------------+----------------+--------------+
| source_uuid | interval_start | interval_end |
+--------------------------------------+----------------+--------------+
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e | 1 | 10005 |
| 8eed0f5b-6f9b-11e9-94a9-005056a57a4e | 10006 | 11006 |
+--------------------------------------+----------------+--------------+
2 rows in set (0.00 sec)
按照 gtid_executed 的计算方法,gtid_executed 为 10006-11006 和 1-11006 的并集,于是得出 1-11006。
gtid_purged 的计算过程如下:
gtids_in_binlog_not_purged = gtids_in_binlog - binlog.000001 的 Previous-GTIDs = gtids_in_binlog
gtids_in_binlog = binlog.000003 的 Previous-GTIDs + binlog.000003 本身的 GTID = binlog.000003 的 Previous-GTIDs = 10006-11006
gtid_purged = gtid_executed - gtids_in_binlog_not_purged = 1-11006 - 10006-11006 = 1-10005
三、GTID 自动定位
GTID 是用来代替传统复制的方法,GTID 复制与普通复制模式的最大不同在于,启动和恢复复制时能够自动定位,而不需要指定二进制日志文件名和位置。配置非 GTID 复制时,需要在 CHANGE MASTER TO 语句中包含 MASTER_LOG_FILE 或 MASTER_LOG_POS 选项,用于指示从主库复制的开始点。但对于 GTID,从库不需要此非本地数据,其与主库同步的所有信息都直接从复制数据流中获取,因此不需要指定这些选项。要使用基于 GTID 的复制启动从库,推荐启用 MASTER_AUTO_POSITION 选项。
默认情况下禁用 MASTER_AUTO_POSITION 选项。如果在从库上启用了多源复制,则需要为每个适用的复制通道设置该选项。设置 MASTER_AUTO_POSITION = 0 会使从库恢复为基于文件的复制,这时必须指定 MASTER_LOG_FILE 或 MASTER_LOG_POS 选项。当从库启用 GTID(GTID_MODE = ON、ON_PERMISSIVE 或 OFF_PERMISSIVE)并使用 MASTER_AUTO_POSITION 选项时,将激活自动定位以连接到主库。主库必须设置 GTID_MODE = ON 才能使连接成功。
在初始握手中,从库向主库发送一个 GTID 集,其中包含已经收到、已提交或两者都已完成的事务。此 GTID 集等于 @@GLOBAL.gtid_executed 系统变量与 select received_transaction_set from performance_schema.replication_connection_status 查询结果的并集。主库会比较其二进制日志中记录的所有事务和从库发来的 GTID 集合,并将不包括在从库发送的 GTID 集中的事务全部发送给从库。自动跳过功能可确保同一事务不会应用两次。如果从库缺失的 GTID 已经被主库清除(purge),则复制中断,主库将错误 ER_MASTER_HAS_PURGED_REQUIRED_GTIDS 发送给从库。主库错误日志的 ER_FOUND_MISSING_GTIDS 警告消息中将列出丢失事务的 GTID。从库无法自动解决此问题,尝试在不启用 MASTER_AUTO_POSITION 选项的情况下重新连接主库只会导致已清除事务在从库上的丢失。可以考虑修改主库上的 binlog_expire_logs_seconds 系统参数值(缺省为 2592000 秒,即 30 天),以确保不再发生二进制日志还需要时已经被提前清除的情况。下面模拟一下这个场景。
-- 从库停止复制
stop slave;
-- 主库做更新
truncate table t1;
-- 主库修改binlog文件名,模拟事务丢失
mysql -uroot -p123456 -e "show master status;"
mv binlog.000001 binlog.000001.bak
-- 从库启动复制
start slave;
show slave status\G
会看到 1236 错误:
会看到1236错误:
Last_IO_Errno: 13114
Last_IO_Error: Got fatal error 1236 from master when reading data from binary log: 'Cannot replicate because the master purged required binary logs. Replicate the missing transactions from elsewhere, or provision a new slave from backup. Consider increasing the master's binary log expiration period. To find the missing transactions, see the master's error log or the manual for GTID_SUBTRACT.'
主库的错误日志中会显示如下信息:
2019-06-11T00:18:14.500248Z 207 [ERROR] [MY-010958] [Server] Could not open log file.
2019-06-11T00:18:14.500299Z 207 [Warning] [MY-011809] [Server] Cannot replicate to server with server_uuid='565a6b0a-6f05-11e9-b95c-005056a5497f' because the present server has purged required binary logs. The connecting server needs to replicate the missing transactions from elsewhere, or be replaced by a new server created from a more recent backup. To prevent this error in the future, consider increasing the binary log expiration period on the present server. The missing transactions are '8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-11007'.
主库二进制日志修改成正确的文件名,重启从库复制后恢复正常:
-- 主库
mv binlog.000001.bak binlog.000001
-- 从库
stop slave;
start slave;
show slave status\G
如果在事务交换期间发现从库已经在 GTID 中接收或提交了事务,但主库本身没有它们的记录,则复制停止,主库将错误 ER_SLAVE_HAS_MORE_GTIDS_THAN_MASTER 发送给从库。当没有配置 sync_binlog=1 的主库遇到电源故障或操作系统崩溃,导致尚未同步到二进制日志文件的已提交事务已被从库接收,则会发生这种情况。如果主库重新提交事务,可能导致主库和从库对不同的事务使用相同的 GTID,这时只能根据需要对各个事务手动解决冲突(例如手工设置 gtid_next)。如果问题仅在于主库缺少事务,则可以主从切换,允许它跟上复制拓扑中的其它服务器,然后在需要时再次将其设置为主库。可见 sync_binlog=1 对于主从数据一致至关重要,这也是 MySQL 8 的缺省配置值。下面模拟一下这个场景。
-- 主库reset master
reset master;
-- 从库重启复制
stop slave;
start slave;
show slave status\G
会看到以下错误:
Last_IO_Errno: 13114
Last_IO_Error: Got fatal error 1236 from master when reading data from binary log: 'Slave has more GTIDs than the master has, using the master's SERVER_UUID. This may indicate that the end of the binary log was truncated or that the last binary log file was lost, e.g., after a power or disk failure when sync_binlog != 1. The master may or may not have rolled back transactions that were already replica'
重新配置从库以恢复复制:
reset master;
stop slave;
reset slave all;
change master to
master_host = '172.16.1.125',
master_port = 3306,
master_user = 'repl',
master_password = '123456',
master_auto_position = 1;
start slave;
show slave status\G