Skip to main content

3.使用全局事务标识符进行复制(未完成)

本节介绍使用全局事务标识符(GTID) 的基于事务的复制 。使用 GTID 时,每个事务在原始服务器上提交并由任何副本应用时都可以被识别和跟踪;这意味着在启动新副本或故障转移到新源时,无需使用 GTID 来引用日志文件或这些文件中的位置,这极大地简化了这些任务。由于基于GTID的复制完全基于事务,因此很容易判断源和副本是否一致;只要在源上提交的所有事务也在副本上提交,就可以保证两者之间的一致性。可以使用 GTID 的基于语句或基于行的复制;但是,为了获得最佳结果,建议使用基于行的格式。

GTID 始终保留在源和副本之间。这意味着始终可以通过检查二进制日志来确定应用于任何副本的任何事务的来源。此外,一旦在给定服务器上提交了具有给定 GTID 的事务,则该服务器将忽略任何具有相同 GTID 的后续事务。因此,在源上提交的事务只能在副本上应用一次,这有助于保证一致性。

本节讨论以下主题:

  • 如何定义和创建 GTID,以及如何在 MySQL 服务器中表示它们。
  • GTID 的生命周期。
  • 自动定位功能,用于同步使用 GTID 的副本和源。
  • 设置和启动基于 GTID 的复制的一般过程。
  • 使用 GTID 时配置新复制服务器的建议方法。
  • 使用基于 GTID 的复制时应注意的限制和限制。
  • 可用于处理 GTID 的存储函数。

GTID 格式和存储

全局事务标识符 (GTID) 是创建并与原始服务器(源)上提交的每个事务关联的唯一标识符。 该标识符不仅对于其起源的服务器是唯一的,而且对于给定复制拓扑中的所有服务器也是唯一的。

GTID 分配区分客户端事务(在源上提交)和复制事务(在副本上复制)。 当在源上提交客户端事务时,如果该事务已写入二进制日志,则会为其分配一个新的 GTID。 保证客户端交易具有单调递增的 GTID,生成的数字之间没有间隙。 如果客户端事务未写入二进制日志(例如,因为事务被过滤掉,或者事务是只读的),则不会在源服务器上为其分配 GTID。

复制的事务保留与原始服务器上分配给事务的相同 GTID。 GTID 在复制事务开始执行之前就存在,并且即使复制事务未写入副本上的二进制日志或在副本上被过滤掉,GTID 也会保留。 mysql.gtid_execulated 系统表用于保存在 MySQL 服务器上应用的所有事务的分配的 GTID,存储在当前活动二进制日志文件中的事务除外。

GTID 的自动跳过功能意味着在源上提交的事务只能在副本上应用一次,这有助于保证一致性。 一旦在给定服务器上提交了具有给定 GTID 的事务,则该服务器将忽略执行具有相同 GTID 的后续事务的任何尝试。 不会引发错误,也不会执行事务中的任何语句。

如果具有给定 GTID 的事务已开始在服务器上执行,但尚未提交或回滚,则任何在具有相同 GTID 的服务器上启动并发事务的尝试都会被阻止。 服务器既不开始执行并发事务,也不将控制权返回给客户端。 一旦事务的第一次尝试提交或回滚,在同一 GTID 上阻塞的并发会话就可以继续进行。 如果第一次尝试回滚,一个并发会话将继续尝试该事务,并且在同一 GTID 上阻塞的任何其他并发会话仍保持阻塞状态。 如果提交了第一次尝试,则所有并发会话将停止被阻止,并自动跳过该事务的所有语句。

GTID 表示为一对坐标,用冒号字符 (:) 分隔,如下所示:

GTID = source_id:transaction_id

source_id 标识原始服务器。 通常,源的 server_uuid 用于此目的。 transaction_id 是由事务在源上提交的顺序确定的序列号。 例如,要提交的第一个事务的 transaction_id 为 1,而同一源服务器上要提交的第十个事务的 transaction_id 为 10。事务不可能在事务中使用 0 作为GTID序列号 。 例如,最初在 UUID 3E11FA47-71CA-11E1-9E33-C80AA9429562 的服务器上提交的第 23 个事务具有以下 GTID:

3E11FA47-71CA-11E1-9E33-C80AA9429562:23

服务器实例上 GTID 序列号的上限是有符号 64 位整数的非负值的数量(2^63 - 1 或 9223372036854775807)。 如果服务器用完 GTID,它将采取 binlog_error_action 指定的操作。 当服务器实例接近限制时,会发出警告消息。

MySQL 8.3 还支持标记的 GTID。 带标签的 GTID 由三部分组成,用冒号字符分隔,如下所示:

GTID = source_id:tag:transaction_id

在这种情况下,source_id 和 transaction_id 如之前所定义。 标签是用户定义的字符串,用于标识特定的一组交易; 有关允许的语法,请参阅 gtid_next 系统变量的描述。 示例:最初在 UUID 为 ed102faf-eb00-11eb-8f20-0c5415bfaa1d 且标签 Domain_1 的服务器上提交的第 117 个事务具有以下 GTID:

ed102faf-eb00-11eb-8f20-0c5415bfaa1d:Domain_1:117

事务的 GTID 显示在 mysqlbinlog 的输出中,它用于标识性能模式复制状态表中的单个事务,例如,replication_applier_status_by_worker。 gtid_next 系统变量 (@@GLOBAL.gtid_next) 存储的值是单个 GTID。

GTID 集

GTID集合是包括一个或多个单个GTID或GTID范围的集合。 GTID 集在 MySQL 服务器中以多种方式使用。 例如,gtid_execulated 和 gtid_purged 系统变量存储的值是 GTID 集。 START REPLICA 选项 UNTIL SQL_BEFORE_GTIDS 和 UNTIL SQL_AFTER_GTIDS 可用于使副本仅处理 GTID 集中的第一个 GTID 之前的事务,或在 GTID 集中的最后一个 GTID 之后停止。 内置函数 GTID_SUBSET() 和 GTID_SUBTRACT() 需要 GTID 集作为输入。

源自同一服务器的一系列 GTID 可以折叠为单个表达式,如下所示:

3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5

上面的示例表示源自 server_uuid 为 3E11FA47-71CA-11E1-9E33-C80AA9429562 的 MySQL 服务器的第一到第五个事务。 来自同一服务器的多个单个 GTID 或 GTID 范围也可以包含在单个表达式中,GTID 或范围用冒号分隔,如下例所示:

3E11FA47-71CA-11E1-9E33-C80AA9429562:1-3:11:47-49

GTID集可以包括单个GTID和GTID范围的任意组合,并且它可以包括源自不同服务器的GTID。 此示例显示了存储在已应用来自多个源的事务的副本的 gtid_execulated 系统变量 (@@GLOBAL.gtid_execulated) 中的 GTID 集:

2174B383-5441-11E8-B90A-C80AA9429562:1-3, 24DA167-0C0C-11E8-8442-00059A3C7B00:1-19

当 GTID 集从服务器变量返回时,UUID 按字母顺序排列,数字间隔按升序合并。

构建 GTID 集时,用户定义的标签将被视为 UUID 的一部分。 这意味着来自同一服务器并具有相同标签的多个 GTID 可以包含在单个表达式中,如本示例所示:

3E11FA47-71CA-11E1-9E33-C80AA9429562:Domain_1:1-3:11:47-49

源自同一服务器但具有不同标签的 GTID 的处理方式与源自不同服务器的 GTID 类似,如下所示:

3E11FA47-71CA-11E1-9E33-C80AA9429562:Domain_1:1-3:15-21, 3E11FA47-71CA-11E1-9E33-C80AA9429562:Domain_2:8-52

GTID 集的完整语法如下:

gtid_set:
uuid_set [, uuid_set] ...
| ''

uuid_set:
uuid:[tag:]interval[:interval]...

uuid:
hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh

h:
[0-9|A-F]

tag:
[a-z_][a-z0-9_]{0,31}

interval:
m[-n]

(m >= 1; n > m)

mysql.gtid_executed 表

GTID 存储在 mysql 数据库中名为 gtid_executed 的表中。 对于它所代表的每个 GTID 或 GTID 集,该表中的一行包含原始服务器的 UUID、用户定义的标签(如果有)以及该组的开始和结束事务 ID; 对于仅引用单个 GTID 的行,最后两个值是相同的。

当安装或升级 MySQL Server 时,使用类似于此处所示的 CREATE TABLE 语句创建 mysql.gtid_execulated 表(如果它尚不存在):

CREATE TABLE gtid_executed (
source_uuid CHAR(36) NOT NULL,
interval_start BIGINT NOT NULL,
interval_end BIGINT NOT NULL,
gtid_tag CHAR(32) NOT NULL,
PRIMARY KEY (source_uuid, gtid_tag, interval_start)
);

注意:

与其他 MySQL 系统表一样,不要尝试自己创建或修改此表。

mysql.gtid_executed 表供MySQL 服务器内部使用。 当在副本上禁用二进制日志记录时,它允许副本使用 GTID,并且当二进制日志丢失时,它允许保留 GTID 状态。 请注意,如果发出 RESET BINARY LOGS AND GTIDS,则 mysql.gtid_execulated 表将被清除。

仅当 gtid_mode 为 ON 或 ON_PERMISSIVE 时,GTID 才会存储在 mysql.gtid_execulated 表中。 如果禁用了二进制日志记录(log_bin为OFF),或者禁用了log_replica_updates,则服务器在提交事务时将属于每个事务的GTID与该事务一起存储在缓冲区中,并且后台线程定期添加缓冲区的内容 作为 mysql.gtid_executed 表的一个或多个条目。 此外,该表会按照用户可配置的速率定期进行压缩,如 mysql.gtid_execulated 表压缩中所述。

如果启用了二进制日志记录(log_bin 为 ON),则仅对于 InnoDB 存储引擎,服务器会以与禁用二进制日志记录或副本更新日志记录相同的方式更新 mysql.gtid_executed 表,在事务提交时存储每个事务的 GTID 时间。 对于其他存储引擎,服务器仅在二进制日志轮换或服务器关闭时更新 mysql.gtid_execulated 表。 此时,服务器将写入先前二进制日志的所有事务的 GTID 写入 mysql.gtid_executed 表中。

如果无法访问 mysql.gtid_executed 表进行写入,并且由于达到最大文件大小 (max_binlog_size) 之外的任何原因而轮换二进制日志文件,则继续使用当前二进制日志文件。 将向请求轮换的客户端返回一条错误消息,并在服务器上记录一条警告。 如果无法访问 mysql.gtid_executed 表进行写入并且达到 max_binlog_size,服务器将根据其 binlog_error_action 设置进行响应。 如果设置了 IGNORE_ERROR,则会在服务器上记录错误并停止二进制日志记录,或者如果设置了 ABORT_SERVER,则服务器将关闭。

mysql.gtid_execulated 表压缩

随着时间的推移,mysql.gtid_execulated 表可能会充满许多行,这些行引用源自同一服务器的各个 GTID,具有相同的 GTID 标记(如果有),并且其事务 ID 构成一个范围,类似下面内容:

+--------------------------------------+----------------+--------------+----------+
| source_uuid | interval_start | interval_end | gtid_tag |
|--------------------------------------+----------------+--------------|----------+
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 31 | 31 | Domain_1 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 32 | 32 | Domain_1 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 33 | 33 | Domain_1 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 34 | 34 | Domain_1 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 35 | 35 | Domain_1 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 36 | 36 | Domain_2 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 37 | 37 | Domain_2 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 38 | 38 | Domain_2 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 39 | 39 | Domain_2 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 40 | 40 | Domain_1 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 41 | 41 | Domain_1 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 42 | 42 | Domain_1 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 43 | 43 | Domain_1 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 44 | 44 | Domain_2 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 45 | 45 | Domain_2 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 46 | 46 | Domain_2 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 47 | 47 | Domain_1 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 48 | 48 | Domain_1 |
...

为了节省空间,MySQL 服务器可以定期压缩 mysql.gtid_executed 表,方法是将每个此类行集替换为跨越整个事务标识符间隔的单行,如下所示:

+--------------------------------------+----------------+--------------+----------+
| source_uuid | interval_start | interval_end | gtid_tag |
|--------------------------------------+----------------+--------------|----------+
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 31 | 35 | Domain_1 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 36 | 39 | Domain_2 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 40 | 43 | Domain_1 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 44 | 46 | Domain_2 |
| 3E11FA47-71CA-11E1-9E33-C80AA9429562 | 47 | 48 | Domain_1 |
...

服务器可以使用名为 thread/sql/compress_gtid_table 的专用前台线程来执行压缩。 该线程未在 SHOW PROCESSLIST 的输出中列出,但可以将其视为线程表中的一行,如下所示:

mysql> SELECT * FROM performance_schema.threads WHERE NAME LIKE '%gtid%'\G
*************************** 1. row ***************************
THREAD_ID: 26
NAME: thread/sql/compress_gtid_table
TYPE: FOREGROUND
PROCESSLIST_ID: 1
PROCESSLIST_USER: NULL
PROCESSLIST_HOST: NULL
PROCESSLIST_DB: NULL
PROCESSLIST_COMMAND: Daemon
PROCESSLIST_TIME: 1509
PROCESSLIST_STATE: Suspending
PROCESSLIST_INFO: NULL
PARENT_THREAD_ID: 1
ROLE: NULL
INSTRUMENTED: YES
HISTORY: YES
CONNECTION_TYPE: NULL
THREAD_OS_ID: 18677

当服务器上启用二进制日志记录时,不会使用此压缩方法,而是会在每次二进制日志轮转时压缩 mysql.gtid_executed 表。 但是,当服务器上禁用二进制日志记录时,thread/sql/compress_gtid_table 线程会休眠,直到执行了指定数量的事务,然后醒来以执行 mysql.gtid_execulated 表的压缩。 然后它会休眠,直到发生相同数量的事务,然后醒来再次执行压缩,无限期地重复此循环。 压缩表之前经过的事务数以及压缩率由 gtid_execulated_compression_period 系统变量的值控制。 将该值设置为 0 意味着线程永远不会唤醒,这意味着不使用这种显式压缩方法。 相反,压缩会根据需要隐式发生。

InnoDB 事务由与涉及 InnoDB 以外的存储引擎的事务所使用的进程分开的进程写入 mysql.gtid_execulated 表。 这个过程由另一个线程innodb/clone_gtid_thread控制。 该 GTID 持久化线程按组收集 GTID,将它们刷新到 mysql.gtid_executed 表,然后压缩该表。 如果服务器混合了 InnoDB 事务和非 InnoDB 事务(这些事务分别写入 mysql.gtid_execulated 表),则 compress_gtid_table 线程执行的压缩会干扰 GTID 持久化线程的工作,并会显着减慢速度。 因此,建议将 gtid_execulated_compression_period 设置为 0,这样 compress_gtid_table 线程就永远不会被激活。

gtid_execulated_compression_period的默认值为0,无论存储引擎如何,所有事务都由GTID持久化线程写入mysql.gtid_execulated表。

当服务器实例启动时,如果 gtid_execulated_compression_period 设置为非零值并且启动了 thread/sql/compress_gtid_table 线程,则在大多数服务器配置中,将对 mysql.gtid_execulated 表执行显式压缩。 压缩是由线程启动触发的。

GTID 生命周期

GTID的生命周期由以下步骤组成:

  1. 事务在源上执行并提交。 该客户端事务被分配一个由源的 UUID 和该服务器上尚未使用的最小非零事务序列号组成的 GTID。 GTID 被写入源的二进制日志(紧邻日志中事务本身之前)。 如果客户端事务没有写入二进制日志(例如,因为事务被过滤掉,或者事务是只读的),则不会为其分配 GTID。
  2. 如果为事务分配了 GTID,则 GTID 会在提交时通过在事务开始时将其写入二进制日志(作为 Gtid_log_event)以原子方式持久保存。 每当二进制日志轮转或服务器关闭时,服务器都会将写入前一个二进制日志文件的所有事务的 GTID 写入 mysql.gtid_executed 表中。
  3. 如果为事务分配了 GTID,则通过将 GTID 添加到 gtid_execulated 系统变量 (@@GLOBAL.gtid_execulated) 中的 GTID 集合,以非原子方式(事务提交后不久)将 GTID 外部化。 该 GTID 集包含所有已提交 GTID 事务集的表示,并且在复制中用作表示服务器状态的令牌。 启用二进制日志记录(根据源的要求)后,gtid_execulated 系统变量中的 GTID 集是所应用事务的完整记录,但 mysql.gtid_execulated 表不是,因为最新的历史记录仍在当前二进制文件中日志档案。
  4. 在二进制日志数据传输到副本并存储在副本的中继日志中之后,副本读取 GTID 并设置其 gtid_next 的值系统变量为此 GTID。 这告诉副本必须使用此 GTID 记录下一个事务。 值得注意的是,副本在会话上下文中设置 gtid_next。
  5. 副本验证是否还没有线程取得 gtid_next 中 GTID 的所有权,以便处理事务。 通过在处理事务本身之前首先读取并检查复制事务的 GTID,副本不仅可以保证之前没有具有此 GTID 的事务应用于副本,而且还没有其他会话已读取此 GTID 但尚未读取已提交关联交易。 因此,如果多个客户端尝试同时应用同一事务,服务器会通过仅让其中一个客户端执行来解决此问题。 副本的 gtid_owned 系统变量 (@@GLOBAL.gtid_owned) 显示当前正在使用的每个 GTID 以及拥有它的线程的 ID。 如果GTID已经被使用,则不会引发错误,并且使用自动跳过功能来忽略该事务。
  6. 如果尚未使用 GTID,副本将应用复制的事务。 由于 gtid_next 设置为源已分配的 GTID,因此副本不会尝试为此事务生成新的 GTID,而是使用 gtid_next 中存储的 GTID。
  7. 如果在副本上启用了二进制日志记录,则 GTID 会在提交时通过在事务开始时将其写入二进制日志(作为 Gtid_log_event)以原子方式持久保存。 每当二进制日志轮转或服务器关闭时,服务器都会将写入前一个二进制日志文件的所有事务的 GTID 写入 mysql.gtid_executed 表中。
  8. 如果在副本上禁用二进制日志记录,则通过将 GTID 直接写入 mysql.gtid_execulated 表以原子方式持久保存 GTID。 MySQL 在事务中附加一条语句,将 GTID 插入表中。 此操作对于 DDL 语句和 DML 语句都是原子的。 在这种情况下,mysql.gtid_executed 表是应用于副本的事务的完整记录。
  9. 在副本上提交复制的事务后不久,GTID 将通过将其添加到副本的 gtid_execulated 系统变量 (@@GLOBAL.gtid_execulated) 中的 GTID 集来非原子地外部化。 至于源,该 GTID 集包含所有已提交 GTID 事务集的表示。 如果在副本上禁用二进制日志记录,则 mysql.gtid_execulated 表也是副本上应用的事务的完整记录。 如果在副本上启用了二进制日志记录,这意味着某些 GTID 仅记录在二进制日志中,则 gtid_execulated 系统变量中的 GTID 集是唯一完整的记录。

在源上完全过滤掉的客户端事务不会分配 GTID,因此它们不会添加到 gtid_execulated 系统变量中的事务集中,也不会添加到 mysql.gtid_execulated 表中。 但是,在副本上完全过滤掉的复制事务的 GTID 仍会保留。 如果在副本上启用了二进制日志记录,则过滤出的事务将作为 Gtid_log_event 写入二进制日志,后跟仅包含 BEGIN 和 COMMIT 语句的空事务。 如果禁用二进制日志记录,则过滤出的事务的 GTID 将写入 mysql.gtid_executed 表。 保留已过滤事务的 GTID 可确保可以压缩 mysql.gtid_execulated 表和 gtid_execulated 系统变量中的 GTID 集。 它还确保如果副本重新连接到源,则不会再次检索已过滤的事务。

在多线程副本上(replica_parallel_workers > 0),事务可以并行应用,因此复制的事务可以无序提交(除非replica_preserve_commit_order = 1)。 当发生这种情况时,gtid_execulated 系统变量中的 GTID 集包含多个 GTID 范围,并且它们之间有间隙。 (在源或单线程副本上,GTID 单调递增,数字之间没有间隙。)多线程副本上的间隙仅出现在最近应用的事务中,并随着复制的进行而填充。 当使用 STOP REPLICA 语句完全停止复制线程时,将应用正在进行的事务,以便填补间隙。 如果发生关闭(例如服务器故障)或使用 KILL 语句停止复制线程,间隙可能仍然存在。

哪些更改被分配了 GTID?

典型的场景是服务器为已提交的事务生成新的 GTID。 然而,除了事务之外,GTID还可以被分配给其他变化,并且在某些情况下,单个事务可以被分配多个GTID。

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

非事务性更新和事务性更新都分配有 GTID。 此外,对于非事务性更新,如果在尝试写入二进制日志缓存时发生磁盘写入失败,并因此在二进制日志中创建间隙,则会为生成的事件日志事件分配 GTID。

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

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

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

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

  • 调用提交多个事务的存储过程。 为过程提交的每个事务生成一个 GTID。
  • 多表 DROP TABLE 语句删除不同类型的表。 如果任何表使用不支持原子DDL的存储引擎,或者任何表是临时表,则可以生成多个GTID。
  • 当使用基于行的复制时 (binlog_format=ROW),会发出 CREATE TABLE ... SELECT 语句。 为 CREATE TABLE 操作生成 1 个 GTID,为行插入操作生成 1 个 GTID。

gtid_next 系统变量

默认情况下,对于用户会话中提交的新事务,服务器会自动生成并分配新的 GTID。 当事务应用于副本时,来自源服务器的 GTID 将被保留。 可以通过设置 gtid_next 系统变量的会话值来更改此行为:

当 gtid_next 设置为 AUTOMATIC(默认值)并且提交事务并将其写入二进制日志时,服务器会自动生成并分配新的 GTID。 如果事务被回滚或由于其他原因未写入二进制日志,服务器不会生成并分配 GTID。

如果将 gtid_next 设置为 AUTOMATIC:TAG,则会将包含指定标签的新 GTID 分配给每个新事务。

如果将 gtid_next 设置为有效的 GTID(由 UUID、可选标签和事务序列号组成,以冒号分隔),服务器会将该 GTID 分配给的事务。 即使事务未写入二进制日志或事务为空时,也会分配此 GTID 并将其添加到 gtid_executed 中。

请注意,将 gtid_next 设置为特定 GTID(采用 UUID:NUMBER 或 UUID:TAG:NUMBER 格式)并且事务已提交或回滚后,必须在任何其他语句之前发出显式 SET @@SESSION.gtid_next 语句声明.声明. 如果不想显式分配更多 GTID,可以使用它将 GTID 值设置回 AUTOMATIC。

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

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

gtid_purged 系统变量

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

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

写入二进制日志文件(现已被清除)的事务的 GTID。

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

可以更改 gtid_purged 的值,以便在服务器上记录已应用某个 GTID 集中的事务,尽管它们不存在于服务器上的任何二进制日志中。 当将 GTID 添加到 gtid_purged 时,它们也会添加到 gtid_execulated。 此操作的一个示例用例是当正在恢复服务器上一个或多个数据库的备份,但没有包含服务器上事务的相关二进制日志时。 还可以选择是否将 gtid_purged 中的整个 GTID 集替换为指定的 GTID 集,或者将指定的 GTID 集添加到 gtid_purged 中已有的 GTID 中。

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

  • gtid_execulated 计算为最新二进制日志文件中 Previous_gtids_log_event 中的 GTID、该二进制日志文件中事务的 GTID 以及存储在 mysql.gtid_execulated 表中的 GTID 的并集。 该 GTID 集包含服务器上已使用(或显式添加到 gtid_purged)的所有 GTID,无论它们当前是否位于服务器上的二进制日志文件中。 它不包括当前正在服务器上处理的事务的 GTID (@@GLOBAL.gtid_owned)。
  • gtid_purged 的计算方法是首先将最新二进制日志文件中 Previous_gtids_log_event 中的 GTID 与该二进制日志文件中事务的 GTID 相加。 此步骤给出当前或曾经记录在服务器上的二进制日志 (gtids_in_binlog) 中的 GTID 集。 接下来,从gtids_in_binlog中减去最旧的二进制日志文件中的Previous_gtids_log_event中的GTID。 此步骤给出当前记录在服务器上的二进制日志中的 GTID 集 (gtids_in_binlog_not_purged)。 最后,从gtid_execulated中减去gtids_in_binlog_not_purged。 结果是服务器上已使用但当前未记录在服务器上的二进制日志文件中的 GTID 集合,并且此结果用于初始化 gtid_purged。

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

重置 GTID 执行历史记录

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

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

当发出 RESET BINARY LOGS AND GTIDS 时,将执行以下重置操作:

  • gtid_purged 系统变量的值设置为空字符串 ('')。
  • gtid_execulated 系统变量的全局值(但不是会话值)设置为空字符串。
  • mysql.gtid_execulated 表被清除(请参阅 mysql.gtid_execulated 表)。
  • 如果服务器启用了二进制日志记录,则会删除现有的二进制日志文件并清除二进制日志索引文件。

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

GTID自动定位

GTID 取代了之前确定源和副本之间数据流的启动、停止或恢复点所需的文件偏移对。 当使用 GTID 时,副本与源同步所需的所有信息都直接从复制数据流中获取。

要使用基于 GTID 的复制启动副本,需要在 CHANGE REPLICATION SOURCE TO 语句中启用 SOURCE_AUTO_POSITION 选项。 备用 SOURCE_LOG_FILE 和 SOURCE_LOG_POS 选项指定日志文件的名称和文件内的起始位置,但使用 GTID 时,副本不需要此非本地数据。

默认情况下禁用 SOURCE_AUTO_POSITION 选项。 如果在副本上启用了多源复制,需要为每个适用的复制通道设置该选项。 再次禁用 SOURCE_AUTO_POSITION 选项会导致副本恢复为基于文件的复制; 这意味着,当 GTID_ONLY=ON 时,某些位置可能会被标记为无效,在这种情况下,还必须在禁用 SOURCE_AUTO_POSITION 时同时指定 SOURCE_LOG_FILE 和 SOURCE_LOG_POS。

当副本启用了 GTID(GTID_MODE=ON、ON_PERMISSIVE 或 OFF_PERMISSIVE )并启用了 SOURCE_AUTO_POSITION 选项时,将激活自动定位以连接到源。 源必须设置 GTID_MODE=ON 才能使连接成功。 在初始握手中,副本发送一个 GTID 集,其中包含它已接收、提交或两者的事务。 此 GTID 集等于 gtid_execulated 系统变量 (@@GLOBAL.gtid_execulated) 中的 GTID 集与性能架构中的replication_connection_status 表中记录为已接收事务的 GTID 集的并集(语句 SELECT RECEIVED_TRANSACTION_SET FROM 的结果) PERFORMANCE_SCHEMA.replication_connection_status)。

源通过发送其二进制日志中记录的所有事务进行响应,这些事务的 GTID 不包含在副本发送的 GTID 集中。 为此,源首先通过检查每个二进制日志文件标头中的 Previous_gtids_log_event(从最新的二进制日志文件开始)来识别要开始使用的适当的二进制日志文件。 当源发现第一个 Previous_gtids_log_event 不包含副本丢失的事务时,它将从该二进制日志文件开始。 此方法非常高效,并且仅在副本落后于源大量二进制日志文件的情况下才会花费大量时间。 然后,源读取该二进制日志文件和后续文件(直到当前文件)中的事务,发送具有副本缺少的 GTID 的事务,并跳过副本发送的 GTID 集中的事务。 副本收到第一个丢失事务之前所经过的时间取决于其在二进制日志文件中的偏移量。 此交换确保源仅发送具有副本尚未接收或提交的 GTID 的事务。 如果副本从多个源接收事务(如钻石拓扑的情况),自动跳过功能可确保事务不会应用两次。

如果应由源发送的任何事务已从源的二进制日志中清除,或通过其他方法添加到 gtid_purged 系统变量中的 GTID 集中,则源会向副本发送错误 ER_MASTER_HAS_PURGED_REQUIRED_GTIDS,并且复制不会不开始。 已识别丢失的已清除事务的 GTID,并在警告消息 ER_FOUND_MISSING_GTIDS 中的源错误日志中列出。 副本无法自动从此错误中恢复,因为赶上源所需的部分事务历史记录已被清除。 在未启用 SOURCE_AUTO_POSITION 选项的情况下尝试重新连接只会导致副本上已清除事务的丢失。 从这种情况中恢复的正确方法是让副本从另一个源复制 ER_FOUND_MISSING_GTIDS 消息中列出的丢失事务,或者将该副本替换为从较新的备份创建的新副本。 考虑修改源上的二进制日志过期期限 (binlog_expire_logs_seconds),以确保这种情况不会再次发生。

如果在事务交换过程中,发现副本已接收或提交了 GTID 中包含源 UUID 的事务,但源本身没有这些事务的记录,则源会向副本发送错误 ER_SLAVE_HAS_MORE_GTIDS_THAN_MASTER,并且复制不会开始。 如果未设置sync_binlog=1 的源遇到电源故障或操作系统崩溃,并丢失尚未同步到二进制日志文件但已由副本接收的已提交事务,则可能会出现这种情况。 如果任何客户端在重新启动后在源上提交事务,则源和副本可能会出现分歧,这可能会导致源和副本对不同事务使用相同的 GTID 的情况。 从这种情况中恢复的正确方法是手动检查源和副本是否存在分歧。 如果现在不同的事务使用相同的 GTID,则需要根据需要为各个事务执行手动冲突解决,或者从复制拓扑中删除源或副本。 如果问题只是源上缺少事务,可以将源设为副本,允许其赶上复制拓扑中的其他服务器,然后根据需要再次将其设为源。

对于菱形拓扑中的多源副本(其中副本从两个或多个源进行复制,而这些源又从公共源进行复制),当使用基于 GTID 的复制时,请确保所有复制过滤器或其他通道配置都已启用。多源副本上的所有通道都相同。 对于基于 GTID 的复制,过滤器仅应用于事务数据,并且 GTID 不会被过滤掉。 发生这种情况是为了使副本的 GTID 集与源的 GTID 集保持一致,这意味着可以使用 GTID 自动定位,而无需每次重新获取过滤掉的事务。 在下游副本是多源的并且从菱形拓扑中的多个源接收相同事务的情况下,下游副本现在具有事务的多个版本,并且结果取决于哪个通道首先应用事务。 尝试使用 GTID 自动跳过功能的第二个通道会跳过该事务,因为该事务的 GTID 已添加到第一个通道设置的 gtid_executed 中。 在通道上进行相同的过滤时,没有问题,因为所有版本的交易都包含相同的数据,因此结果是相同的。 但是,通过对通道进行不同的过滤,数据库可能会变得不一致并且复制可能会挂起。