3.1 GTID简介
3.1.1 什么是GTID
全局事务标识符GTID的英文全称为Global Transaction Identifier,是在整个复制环境中对一个事务的唯一标识。它是MySQL 5.6加入的一个强大特性,目的在于能够实现主从自动定位和切换,而不像以前需要指定文件和位置。使用GTID复制时,在主库上提交事务时创建事务对应的GTID,从库在应用中继日志时用GTID识别和跟踪每个事务。在启动新从库或因故障转移到新主库时,可以使用GTID来标识复制的位置,极大地简化了这些任务。由于GTID的复制完全基于事务,因此只要在主库上提交的所有事务也在从库上提交,两者之间的一致性就能得到保证。GTID支持基于语句或基于行的复制格式,但为了获得最佳效果,MySQL建议使用基于行的格式。GTID始终保留在主库和从库上,这意味着可以通过检查它的二进制日志来确定应用源于哪一个从库的何种事务。而且,一旦在指定库上提交了具有给定GTID的事务,则该库将忽略具有相同GTID的任何后续事务。因此,在主库上提交的事务只会在从库上应用一次,这也有助于保证一致性。
3.1.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以避免冲突。数据目录下的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:
(3)将GDIT设置为已经执行过的值,再执行事务:
可以看到,服务器已经执行了GTID为356的事务,后续相同GTID的事务都被自动跳过,虽然truncate语句没有报错,但并未执行,数据无变化。
实验2:验证两个相同GTID事务,事务1提交,事务2被跳过
(1)准备两个SQL脚本s1.sql和s2.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)查询数据:
可以看到,事务1提交前,事务2被阻塞。事务1提交后,具有相同GTID的事务2被跳过。
实验3:验证两个相同GTID事务,事务1回滚,事务2提交
(1)准备两个SQL脚本s1.sql和s2.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)查询数据:
可以看到,事务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.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,同时在服务器上记录警告信息:
前面已经提到,mysql.gtid_executed表的记录可能并不是完整的已执行GTID,而且有不可访问的可能性,例如误删除此表,因此建议始终通过查询@@global.gtid_executed来确认MySQL服务器的GTID状态,而不是查询mysql.gtid_executed表。mysql.gtid_executed表可能随着事务量的增多而快速膨胀,存储了源自同一个服务器的大量不同的单个GTID,这些GTID构成一个范围,例如:
为了节省空间,MySQL服务器定期压缩mysql.gtid_executed表,方法是将每个这样的行集替换为跨越整个事务标识符间隔的单行,如下所示:
通过设置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中查询到:
通常该线程都处于暂停状态,只有当满足条件时被唤醒,如达到gtid_executed_compression_period或发生了二进制日志轮转(如flush logs等)时。
下面做个简单实验展示一下reset master的作用和影响。
(1)查看从库当前已经执行的GTID和二进制日志:
show master status\G show variables like 'gtid%'; select * from mysql.gtid_executed; show slave status\G
(2)查询结果如下:
所有查询显示已经执行的GTID均为8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-6。
查看当前的binlog结果如下:
当前从库有4个binlog文件。
(1)在从库执行reset master。
(2)再次执行(1)的查询。可以看到所有查询的gtid_executed都置空,binlog文件只有binlog.000001一个。说明reset master命令会清空gtid_executed变量和mysql.gtid_executed表,并会只保留一个初始的binlog文件。
(3)在主库上执行一些更新。
use test; create table t1(a int); insert into t1 select 1;
(4)再次执行(1)的查询。可以看到mysql.gtid_executed表中没有记录,其他查询都已显示出新执行GTID的值,复制正常。说明mysql.gtid_executed不记录当前binlog中的GTID。
(5)从库执行flush logs。在从库上执行flush logs后,mysql.gtid_executed表中存储了从reset master到flush logs之间binlog中的GTID。
从以上步骤看到,从库上执行reset master只是清空从库的gtid_executed,随着复制的继续,其gtid_executed的值也将随之变化,对复制和主从数据一致性没有影响。下面继续实验,看一下在主库上执行reset master会产生哪些影响。
(6)在主库上执行以下语句:
(7)在上一步执行期间,开启一个新会话在主库上执行reset master。
(8)查看从库的复制状态。从show slave status的输出中可以看到复制的I/O线程已停止,并报以下错误:
由于主库正在执行事务中间进行了reset master,从库无法读取主库的二进制日志而报错。更有甚之,这些二进制日志的丢失是永久性的,结果很可能需要从头重建复制。由此实验得出的结论是,作为一条基本原则,不要随意在主库上执行reset master,这样做极有可能导致复制停止或造成主从数据不一致等严重后果,而且不易恢复。