16.1.3.2 GTID 生命周期

GTID 的生命周期包括以下步骤:

  • 在复制源服务器上执行并提交事务。为该 Client 事务分配了一个 GTID,该 GTID 由源的 UUID 和此服务器上尚未使用的最小非零事务序列号组成。 GTID 将被写入源的二进制日志中(紧接在日志中事务本身之前)。如果没有将 Client 事务写入二进制日志中(例如,由于该事务已被滤除或该事务为只读),则不会为其分配 GTID。

  • 如果为事务分配了 GTID,则在提交时通过在事务开始时将其写入二进制日志(作为Gtid_log_event)将 GTID 自动保留。每当旋转二进制日志或关闭服务器时,服务器都会将写入先前的二进制日志文件的所有事务的 GTID 写入mysql.gtid_executedtable。

  • 如果为事务分配了 GTID,则将 GTID 添加到gtid_executed系统变量(@@GLOBAL.gtid_executed)的 GTID 集中,从而以非原子方式(在提交事务之后不久)将 GTID 外部化。该 GTID 集包含所有已提交 GTID 事务集的 table 示,并且在复制中用作 table 示服务器状态的令牌。启用二进制日志记录后(根据源的要求),gtid_executed系统变量中的 GTID 集是已应用事务的完整记录,但mysql.gtid_executedtable 不是完整记录,因为最新历史记录仍在当前二进制日志文件中。

  • 在将二进制日志数据传输到副本并存储在副本的中继日志中之后(使用此过程的已构建机制,有关详细信息,请参见第 16.2 节“复制实现”),副本将读取 GTID 并将其gtid_next系统变量的值设置为此 GTID。这告诉副本必须使用此 GTID 记录下一个事务。重要的是要注意,副本在会话上下文中设置gtid_next

  • 复制副本验证gtid_next中尚未有任何线程拥有 GTID 的所有权以处理事务。通过先读取并检查复制的事务的 GTID,在处理事务本身之前,副本不仅保证没有以前具有该 GTID 的事务已应用到副本,而且还确保没有其他会话已经读取此 GTID,但尚未会话提交了关联的 Transaction。因此,如果多个 Client 端尝试同时应用同一事务,则服务器将通过仅执行其中一个来解决此问题。副本的gtid_owned系统变量(@@GLOBAL.gtid_owned)显示当前正在使用的每个 GTID 以及拥有它的线程的 ID。如果已经使用了 GTID,则不会引发任何错误,并且使用自动跳过功能来忽略事务。

  • 如果尚未使用 GTID,则副本将应用复制的事务。因为gtid_next设置为源已经分配的 GTID,所以副本不会尝试为此事务生成新的 GTID,而是使用gtid_next中存储的 GTID。

  • 如果在副本服务器上启用了二进制日志记录,则 GTID 将在提交时自动保留,方法是在事务开始时将其写入二进制日志(作为Gtid_log_event)。每当旋转二进制日志或关闭服务器时,服务器都会将写入先前的二进制日志文件的所有事务的 GTID 写入mysql.gtid_executedtable。

  • 如果在副本数据库上禁用了二进制日志记录,则将 GTID 直接写入mysql.gtid_executedtable,从而自动保留 GTID。 MySQL 将一个语句添加到事务中,以将 GTID 插入 table 中。在这种情况下,mysql.gtid_executedtable 是复制副本上应用的事务的完整记录。请注意,在 MySQL 5.7 中,对于 DML 语句,将 GTID 插入 table 的操作是原子的,但对于 DDL 语句则不是原子的,因此,如果服务器在涉及 DDL 语句的事务处理后意外退出,则 GTID 状态可能会变得不一致。从 MySQL 8.0 开始,该操作对于 DDL 语句和 DML 语句都是原子的。

  • 将复制事务提交到副本后不久,通过将 GTID 添加到副本的gtid_executed系统变量(@@GLOBAL.gtid_executed)中的 GTID 集中,以非原子方式将 GTID 外部化。至于源,此 GTID 集包含所有已提交 GTID 事务集的 table 示。如果在副本服务器上禁用了二进制日志记录,则mysql.gtid_executedtable 也是该副本服务器上应用的事务的完整记录。如果在副本服务器上启用了二进制日志记录,这意味着某些 GTID 仅记录在二进制日志中,则gtid_executed系统变量中的 GTID 集是唯一完整的记录。

未在源上完全过滤掉的 Client 端事务未分配 GTID,因此不会将它们添加到gtid_executed系统变量中的事务集中,也不会添加到mysql.gtid_executedtable 中。但是,在副本上完全过滤掉的复制事务的 GTID 会保留。如果在副本服务器上启用了二进制日志记录,则过滤出的事务将以Gtid_log_event的形式写入二进制日志,然后是仅包含BEGINCOMMIT语句的空事务。如果禁用了二进制日志记录,则将过滤出的事务的 GTID 写入mysql.gtid_executedtable。为过滤出的事务保留 GTID,可确保可以压缩mysql.gtid_executedtable 和gtid_executed系统变量中的 GTID 集。如第 16.1.3.3 节“ GTID 自动定位”所述,它还确保如果副本重新连接到源,则不会再次检索过滤出的事务。

在多线程副本(带有slave_parallel_workers> 0)上,可以并行应用事务,因此复制的事务可以无序提交(除非设置了slave_preserve_commit_order=1)。发生这种情况时,gtid_executed系统变量中的 GTID 集将包含多个 GTID 范围,它们之间存在间隙。 (在源或单线程副本上,GTID 将单调增加,而数字之间没有间隔.)多线程副本上的间隙仅出现在最近应用的事务中,并且随着复制的进行而被填充。当使用STOP SLAVE语句完全停止复制线程时,将应用进行中的事务以填补空白。在诸如服务器故障之类的关闭事件或使用KILL语句停止复制线程的情况下,间隙可能仍然存在。

为 GTID 分配了哪些更改?

典型的情况是服务器为已提交的事务生成新的 GTID。但是,GTID 也可以分配给除事务之外的其他更改,在某些情况下,可以为单个事务分配多个 GTID。

写入二进制日志的每个数据库更改(DDL 或 DML)都分配有一个 GTID。这包括自动提交的更改以及使用BEGINCOMMITSTART TRANSACTION语句提交的更改。 GTID 还分配给数据库以及非 table 数据库对象(例如过程,函数,触发器,事件,视图,用户,角色或授权)的创建,更改或删除。

非事务更新以及事务更新都分配了 GTID。另外,对于非事务性更新,如果在尝试写入二进制日志缓存时发生磁盘写故障,因此在二进制日志中创建了一个间隙,则将所得的事件日志事件分配给 GTID。

当二进制日志中生成的语句自动删除 table 时,会将 GTID 分配给该语句。当副本开始从刚刚启动的源中应用事件时,以及在使用基于语句的复制(binlog_format=STATEMENT)并且具有打开的临时 table 的用户会话断开连接时,临时 table 将自动删除。在启动服务器后,首次访问使用MEMORY存储引擎的 table 会被自动删除,因为在关闭过程中可能会丢失行。

如果未将事务写入原始服务器上的二进制日志,则服务器不会为其分配 GTID。这包括回退的事务和在原始服务器上禁用二进制日志记录时执行的事务,这些服务器可以是全局(在服务器配置中指定--skip-log-bin)或会话(SET @@SESSION.sql_log_bin = 0)。当使用基于行的复制(binlog_format=ROW)时,这还包括无操作事务。

在 Transaction 的XA PREPARE阶段和 Transaction 的XA COMMITXA ROLLBACK阶段,为 XATransaction 分配了单独的 GTID。 XA 事务是持久准备的,以便用户可以在发生故障(在复制拓扑中可能包括故障转移到另一台服务器)的情况下提交或回滚它们。因此,事务的两个部分是分别复制的,因此,即使回滚的非 XA 事务不会具有 GTID,它们也必须具有自己的 GTID。

在以下特殊情况下,单个语句可以生成多个事务,因此可以分配多个 GTID:

  • 调用存储过程以提交多个事务。该过程提交的每个事务都会生成一个 GTID。

  • 多 tableDROP TABLE语句删除不同类型的 table。

  • 当使用基于行的复制(binlog_format=ROW)时,发出创建 table...选择语句。为CREATE TABLE动作生成一个 GTID,为行插入动作生成一个 GTID。

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,则可以使用它将 GTID 值设置回AUTOMATIC

当复制应用程序线程应用复制的事务时,它们使用此技术,将@@SESSION.gtid_next显式设置为原始服务器上分配的复制事务的 GTID。这意味着将保留源服务器的 GTID,而不是副本生成并分配新的 GTID。这也意味着即使在副本上禁用了二进制日志记录或副本更新日志,或者在无操作或已在副本上过滤掉事务的情况下,GTID 也会添加到副本上的gtid_executed

Client 端可以通过在执行事务之前将@@SESSION.gtid_next设置为特定的 GTID 来模拟复制的事务。 mysqlbinlog使用此技术来生成二进制日志的转储,Client 端可以重播该日志以保留 GTID。通过 Client 端提交的模拟复制事务与通过复制应用程序线程提交的复制事务完全等效,因此事后无法对其进行区分。

gtid_purged 系统变量

gtid_purged系统变量(@@GLOBAL.gtid_purged)中的 GTID 集合包含服务器上已提交但服务器上任何二进制日志文件中不存在的所有事务的 GTID。 gtid_purgedgtid_executed的子集。以下类别的 GTID 位于gtid_purged中:

  • 在副本数据库上禁用了二进制日志记录的情况下提交的复制事务的 GTID。

  • 写入到现已清除的二进制日志文件中的事务的 GTID。

  • 由语句SET @@GLOBAL.gtid_purged显式添加到集合中的 GTID。

您可以更改gtid_purged的值,以在服务器上记录已应用某些 GTID 集中的事务,尽管服务器上任何二进制日志中都不存在这些事务。将 GTID 添加到gtid_purged时,它们也将添加到gtid_executed。此操作的一个示例用例是,正在还原服务器上一个或多个数据库的备份,但是没有包含服务器上事务的相关二进制日志。在 MySQL 5.7 中,只能在gtid_executed(因此gtid_purged)为空时更改gtid_purged的值。有关如何执行此操作的详细信息,请参见gtid_purged的描述。

服务器启动时,将初始化gtid_executedgtid_purged系统变量中的 GTID 集。每个二进制日志文件均以事件Previous_gtids_log_event开头,该事件包含所有先前二进制日志文件中的 GTID 集(由先前文件Previous_gtids_log_event中的 GTID 和先前文件本身中每个Gtid_log_event的 GTID 组成)。最早和最新的二进制日志文件中Previous_gtids_log_event的内容用于在服务器启动时计算gtid_executedgtid_purged集:

  • gtid_executed计算为最新二进制日志文件中Previous_gtids_log_event中的 GTID,该二进制日志文件中事务的 GTID 和存储在mysql.gtid_executedtable 中的 GTID 的并集。该 GTID 集包含服务器上已使用(或显式添加到gtid_purged)的所有 GTID,无论它们当前是否在服务器上的二进制日志文件中。它不包含服务器(@@GLOBAL.gtid_owned)上当前正在处理的事务的 GTID。

  • gtid_purged的计算方法是,首先将最新二进制日志文件中Previous_gtids_log_event中的 GTID 与该二进制日志文件中的事务 GTID 相加。此步骤给出了当前或曾经在服务器上的二进制日志(gtids_in_binlog)中记录的一组 GTID。接下来,从gtids_in_binlog中减去最旧的二进制日志文件中Previous_gtids_log_event中的 GTID。此步骤提供了一组当前记录在服务器上的二进制日志(gtids_in_binlog_not_purged)中的 GTID。最后,从gtid_executed中减去gtids_in_binlog_not_purged。结果是服务器上已使用但当前未记录在服务器上的二进制日志文件中的一组 GTID,并且此结果用于初始化gtid_purged

如果这些计算涉及 MySQL 5.7.7 或更早版本的二进制日志,则可能会为gtid_executedgtid_purged计算不正确的 GTID 集,即使稍后重新启动服务器,它们也仍然不正确。有关详细信息,请参见binlog_gtid_simple_recovery系统变量的描述,该变量控制如何迭代二进制日志以计算 GTID 集。如果此处描述的情况之一适用于服务器,请在启动服务器之前在服务器的配置文件中设置binlog_gtid_simple_recovery=FALSE。该设置使服务器可以迭代所有二进制日志文件(而不仅仅是最新的和最旧的),以查找 GTID 事件开始出现的位置。如果服务器具有大量没有 GTID 事件的二进制日志文件,则此过程可能需要很长时间。

重置 GTID 执行历史

如果需要在服务器上重置 GTID 执行历史记录,请使用RESET MASTER语句。例如,在执行测试查询以验证新启用了 GTID 的服务器上的复制设置之后,或者当您要将新服务器加入复制组但其中包含一些不被接受的本地事务时,您可能需要执行此操作通过组复制。

Warning

请谨慎使用RESET MASTER,以避免丢失任何所需的 GTID 执行历史记录和二进制日志文件。

在发出RESET MASTER之前,请确保已备份服务器的二进制日志文件和二进制日志索引文件(如果有),并获取并保存保存在gtid_executed系统变量的全局值中的 GTID 集(例如,通过发出SELECT @@GLOBAL.gtid_executed语句)并保存结果)。如果要从该 GTID 集中删除不需要的事务,请使用mysqlbinlog检查事务的内容,以确保它们没有价值,不包含任何必须保存或复制的数据,并且不会导致服务器上的数据更改。

发出RESET MASTER时,将执行以下重置操作:

  • gtid_purged系统变量的值设置为空字符串('')。

  • gtid_executed系统变量的全局值(而不是会话值)设置为空字符串。

  • mysql.gtid_executedtable 已清除(请参阅mysql.gtid_executed Table)。

  • 如果服务器启用了二进制日志记录,则将删除现有的二进制日志文件,并清除二进制日志索引文件。

请注意,即使服务器是禁用了二进制日志记录的副本,RESET MASTER也是重置 GTID 执行历史记录的方法。 RESET SLAVE对 GTID 执行历史没有影响。