MySQL 8 复制(四)——GTID与复制

目录

一、GTID简介

1. 什么是GTID

2. GTID的格式与存储

二、GTID生命周期

1. 典型事务的GTID生命周期

2. GTID分配

3. gtid_next系统变量

4. gtid_purged系统变量

三、GTID自动定位


        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_id,并保存到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的生命周期包括以下步骤:

  1. 客户端事务在主库上执行并提交,此事务被分配一个GTID,该GTID由主服务器的UUID和此服务器上尚未使用的最小非零事务序列号组成。GTID作为Gtid_log_event紧接在事务本身之前,与事务本身一起被写入主库的二进制日志,这是一个原子操作。如果未将客户端事务写入二进制日志(例如,因为事务已被过滤掉,或者事务是只读的),则不会为其分配GTID。轮转二进制日志或关闭MySQL实例时,都会将写入之前二进制日志文件的所有事务的GTID写入mysql.gtid_executed表。
  2. 如果为事务分配了GTID,则将GTID添加到主库gtid_executed系统变量(@@global.gtid_executed)的GTID集合中,这步将在事务提交后进行,并且与事务处理本身不是一个原子操作。gtid_executed系统变量包含所有已提交事务的GTID集,是应用事务的完整记录,并在复制中用作表示服务器状态的标记。mysql.gtid_executed表不包含当前二进制日志文件中的最新GTID记录。
  3. 在将二进制日志数据传输到从库并存储在从库的中继日志中之后,从库读取GTID并将其设置为gtid_next系统变量的值。这告诉从库必须使用此GTID记录下一个事务。
  4. 在处理事务本身之前,从库首先读取和检查复制事务的GTID,不仅保证没有先前事务具有此GTID,而且还保证没有其它会话已经读取此GTID但尚未提交相关事务。因此,如果多个客户端同时提交同一GTID事务,则服务器只允许其中一个执行。从库的gtid_owned系统变量(@@global.gtid_owned)显示当前正在使用的GTID以及拥有它的线程ID。如果已经使用了该GTID,通过自动跳过功能忽略具该事务,并且不会引发错误。
  5. 如果GTID尚未使用,则从库应用复制的事务。gtid_next设置为主库已分配的GTID,从库不会为此事务生成新的GTID,而是使用存储在gtid_next中的GTID。
  6. 如果在从库上启用了二进制日志记录,则与主库操作类似。GTID会在提交时作为Gtid_log_event原子写入其二进制日志。当轮转二进制日志或关闭MySQL实例时,都会将写入之前二进制日志文件的所有事务的GTID写入mysql.gtid_executed表。
  7. 如果从库禁用二进制日志记录,则通过将GTID直接写入mysql.gtid_executed表保留GTID。MySQL会在事务中附加一条语句,将GTID插入该表中。从MySQL 8.0开始,此操作对于DDL语句和DML语句都是原子操作。在这种情况下,mysql.gtid_executed表是从库上应用事务的完整记录。
  8. 从库提交复制事务后,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

 

你可能感兴趣的:(MySQL,MySQL高可用方案)