参考 Java 全栈技术的原创文章《有深度有温度的MySQL主从搭建教程》 📖
有视频教程
GTID 的基本概念
GTID 的作用
GTID 全称为 Global Transaction Identifier,是 MySQL 的一个强大的特性。MySQL 会为每一个 DML / DDL 操作都增加一个唯一标记,叫做 GTID。这个标记在整个复制环境中都是唯一的。
主从环境中主库的 DUMP 线程可以直接通过 GTID 定位到需要发送的 binlog 位置,而不再需要指定 binlog 的文件名和位置,因而切换极为方便。
GTID 的基本表示
为了严谨,笔者尽量使用源码的术语解释,后面也会沿用这些术语。
- GTID:单个 GTID,比如
24985463-a536-11e8-a30c-5254008138e4:5
。对应源码中的类结构 Gtid。注意源码中使用 sid 代表前面的 server_uuid,gno 则用来表示 GTID 后面的序号。 - gno:单个 GTID 后面的序号,比如上面的 GTID 的 gno 就是 5。这个 gno 实际上是从一个全局计数器 next_free_gno 中获得的。
- GTID SET:一个 GTID 的集合,可以包含多个 server_uuid,比如常见的 gtid_executed 变量,gtid_purged 变量就是一个 GTID SET。类似的,
24985463-a536-11e8-a30c-5254008138e4:1-5:7-10
就是一个 GTID SET,对应源码中的类结构 Gtid_set,其中还包含一个 sid_map,用于表示多个 server_uuid。 - GTID SET Interval:代表 GTID SET 中的一个区间,GTID SET 中的某个 server_uuid 可能包含多个区间,例如,1-5:7-10 中就有 2 个 GTID SET Interval,分别是 1-5 和 7-10,对应源码中的结构体
Gtid_set::Interval
。
server_uuid 的生成
在 GTID 中包含了一个 server_uuid。server_uuid 实际上是一个 32 字节 +1(/0)字节的字符串。MySQL 启动时会调用 init_server_auto_options 函数读取 auto.cnf 文件。如果 auto.cnf 文件丢失,则会调用 generate_server_uuid 函数生成一个新的 server_uuid,但是需要注意,这样 GTID 必然会发生改变。
在 generate_server_uuid 函数中可以看到,server_uuid 至少和下面 3 部分有关。
- 程序的启动时间;
- 线程的 LWP ID,其中, LWP 是轻量级进程(Light-Weight Process)的简称,我们在 5.1 节会进行描述;
- 一个随机的内存地址。
下面是部分源码:
const time_t save_server_start_time= server_start_time; //获取MySQL启动时间
server_start_time+= ((ulonglong)current_pid << 48) + current_pid;//加入Lwp号运算
thd->status_var.bytes_sent= (ulonglong)thd;//这是一个内存指针,即线程结构体的内存地址
lex_start(thd);
func_uuid= new (thd->mem_root) Item_func_uuid();
func_uuid->fixed= 1;
func_uuid->val_str(&uuid); //这个函数是具体的运算过程
server_uuid 的内部表示是 binary_log::Uuid
,核心是一个 16 字节的内存空间,在 GTID 相关的 Event 中会包含这个信息,2.3 节会进行详细解析。
GTID 的生成
在发起 commit 命令后,当 order commit 执行到 FLUSH 阶段,需要生成 GTID Event 时,会获取 GTID。MySQL 内部维护了一个全局的 GTID 计数器 next_free_gno,用于生成 gno。可以参考 Gtid_state::get_automatic_gno
函数,部分代码如下:
// 1、定义:
Gtid next_candidate = {sidno,sidno == get_server_sidno() ? next_free_gno:1};
// 2、赋值:
while (true)
{
const Gtid_set::Interval *iv= ivit.get();
// 定义 Interval 指针,通过迭代器 ivit 来迭代每个 Interval
rpl_gno next_interval_start= iv != NULL ? iv->start : MAX_GNO;
/**
一般情况下不会为 NULL,因此 next_interval_start 等于第一个 interval
的 start,当然如果为 NULL,则说明 Interval->next = NULL,表示
没有区间了,那么这个时候取 next_interval_start 为 MAX_GNO,
此时条件 next_candidate.gno < next_interval_start 必然成立
**/
while (next_candidate.gno < next_interval_start &&
DBUG_EVALUATE_IF("simulate_gno_exhausted", false, true))
{
if (owned_gtids.get_owner(next_candidate) == 0)
DBUG_RETURN(next_candidate.gno);
// 返回 gno,那么 GTID 就生成了
next_candidate.gno++; // 如果本 GTID 已经被其他线程占用
// 则 next_candidate.gno 自增后继续判断
}
......
}
GTID_EVENT 和 PREVIOUS_GTIDS_LOG_EVENT 简介
这里先解释一下它们的作用,因为后面会用到。
1.GTID_EVENT
GTID_EVENT 作为 DML/DDL 的第一个 Event,用于描述这个操作的 GTID 是多少。在 MySQL 5.7 中,为了支持从库基于 LOGICAL_CLOCK 的并行回放,封装了 last commit 和 seq number 两个值,可以称其为逻辑时钟。
在 MySQL 5.7 中,即便不开启 GTID 也会包含一个匿名的 ANONYMOUS_GTID_EVENT,但是其中不会携带 GTID 信息,只包含 last commit 和 seq number 两个值。
2.PREVIOUS_GTIDS_LOG_EVENT
PREVIOUS_GTIDS_LOG_EVENT 包含在每一个 binlog 的开头,用于描述直到上一个 binlog 所包含的全部 GTID(包括已经删除的 binlog)。在 MySQL 5.7 中,即便不开启 GTID,也会包含这个 PREVIOUS_GTIDS_LOG_EVENT,实际上这一点意义是非常大的。
简单地说,它为快速扫描 binlog 获得正确的 gtid_executed 变量提供了基础,否则可能扫描大量的 binlog 才能得到正确的 binlog 变量(比如 MySQL 5.6 中关闭 GTID 的情况)。这一点将在1.3节详细描述。
gtid_executed 表的作用
官方文档这样描述 gtid_executed 表:
Beginning with MySQL 5.7.5, GTIDs are stored in a table named gtid_executed, in the mysql database. A row in this table contains, for each GTID or set of GTIDs that it represents, the UUID of the originating server, and the starting and ending transaction IDs of the set; for a row referencing only a single GTID, these last two values are the same.
也就是说,gtid_executed 表是 GTID 持久化的一个介质。实例重启后所有的内存信息都会丢失,GTID 模块初始化需要读取 GTID 持久化介质。
可以发现,gtid_executed 表是 InnoDB 表,建表语句如下,并且可以手动更改它,但是除非是测试,否则千万不要修改它。
Table: gtid_executed
Create Table: CREATE TABLE `gtid_executed` (
`source_uuid` char(36) NOT NULL COMMENT 'uuid of the source where the transaction was originally executed.',
`interval_start` bigint(20) NOT NULL COMMENT 'First number of interval.',
`interval_end` bigint(20) NOT NULL COMMENT 'Last number of interval.',
PRIMARY KEY (`source_uuid`,`interval_start`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 STATS_PERSISTENT=0
除了 gtid_executed 表,还有一个 GTID 持久化的介质,那就是 binlog 中的 GTID_EVENT。
既然有了 binlog 中的 GTID_EVENT 进行 GTID 的持久化,为什么还需要 gtid_executed 表呢?
笔者认为,这是 MySQL 5.7.5 之后的一个优化,可以反过来思考,在 MySQL 5.6 中,如果使用 GTID 做从库,那么从库必须开启 binlog,并且设置参数 log_slave_ updates=ture
,因为从库执行过的 GTID 操作都需要保留在 binlog 中,所以当 GTID 模块初始化的时候会读取它获取正确的 GTID SET。
接下来,看一段 MySQL 5.6 官方文档对于搭建 GTID 从库的说明。
Step 3: Restart both servers with GTIDs enabled. To enable binary logging with globaltransaction identifiers, each server must be started with GTID mode, binary logging, slave update logging enabled, and with statements that are unsafe for GTID-based replication disabled. In addition,you should prevent unwanted or accidental updates from being performed on either server by starting both in read-only mode. This means that both servers must be started with (at least) the options shown in the following invocation of mysqld_safe:
shell> mysqld_safe --gtid_mode=ON --log-bin --log-slave-updates --enforce-gtid-consistency &
然而,开启 binlog 的同时设置参数 log_slave_updates=ture
必然会造成一个问题。很多时候,从库是不需要做级联的,设置参数 log_slave_updates=ture
会造成额外的空间和性能开销。因此需要另外一种 GTID 持久化介质,而并不是 binlog 中的 GTID_EVENT,gtid_executed 表正是这样一种 GTID 持久化的介质。