14、MySQL基于GTID的数据同步

基于binlog位点主从复制痛点分析

痛点 1:首次开启主从复制的步骤复杂

  • 第一次开启主从同步时,要求从库和主库是一致的。
  • 找到主库的 binlog 位点。
  • 设置从库的 binlog 位点。
  • 开启从库的复制线程。
    痛点 2: 恢复主从复制的步骤复杂
  • 找到从库复制线程停止时的位点。
  • 解决复制异常的事务。无法解决时就需要手动跳过指定类型的错误,比如通过设置 slave_skip_errors=1032,1062。当然这个前提条件是跳过这类错误是无损的。(1062 错误是插入数据时唯一键冲突;1032 错误是删除数据时找不到行)
    不论是首次开启同步时需要找位点和设置位点,还是恢复主从复制时,设置位点和忽略错误, 这些步骤都显得过于复杂,而且容易出错 。 所以 MySQL 5.6 版本引入了 GTID,彻底解决了这个困难。

基于全局事务标识符(GTID)复制

官网: https://dev.mysql.com/doc/refman/8.0/en/replication-gtids.html
GTID是一个基于原始mysql服务器 生成的一个已经被成功执行的全局事务ID,它由服务器ID以及事务ID组合而成。这个全局事务ID不仅仅在原始服务器器上唯一,在所有存在主从关系 的mysql服务器上也是唯一的。正是因为这样一个特性使得mysql的主从复制变得更加简单,以及数据库一致性更可靠。

  • 一个GTID在一个服务器上只执行一次,避免重复执行导致数据混乱或者主从不一致。
  • GTID用来代替传统复制方法,不再使用MASTER_LOG_FILE+MASTER_LOG_POS开启复制。而是使用MASTER_AUTO_POSTION=1的方式开始复制。
  • 在传统的replica端,binlog是不用开启的,但是在GTID中replica端的binlog是必须开启的,目的是记录执行过的GTID(强制)。

GTID 的优势

  • 更简单的实现 failover,不用以前那样在需要找位点(log_file 和 log_pos)。
  • 更简单的搭建主从复制。
  • 比传统的复制更加安全。
  • GTID 是连续的没有空洞的,保证数据的一致性,零丢失。

GTID结构

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

复制代码
GTID = source_id:transaction_id
  • source_id标识source服务器,即源服务器唯一的server_uuid,由于GTID会传递到replica,所以也可以理解为源ID。

  • transaction_id是一个序列号,由事务在源上提交的顺序决定。序列号的上限是有符号64位整数(2^63-1)
    例如,最初要在UUID为3E11FA47-71CA-11E1-9E33-C80AA9429562的服务器上提交的第23个事务具有此GTID

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

GTID集合是由一个或多个GTID或GTID范围组成的集合。来自同一服务器的一系列gtid可以折叠成单个表达式,如下所示:

复制代码
3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5

源自同一服务器的多个单一gtid或gtid范围也可以包含在单个表达式中,gtid或范围以冒号分隔,如下例所示:

复制代码
3E11FA47-71CA-11E1-9E33-C80AA9429562:1-3:11:47-49

GTID集合可以包括单个GTID和GTID范围的任意组合,也可以包括来自不同服务器的GTID。

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

GTID存储在mysql数据库中名为gtid_executed的表中。该表中的一行包含它所代表的每个GTID或GTID集合的起始服务器的UUID,以及该集合的开始和结束事务id。

GTID工作原理

主库计算主库 GTID 集合和从库 GTID 的集合的差集,主库推送差集 binlog 给从库。
当从库设置完同步参数后,主库 A 的 GTID 集合记为集合 x,从库 B 的 GTID 集合记为 y。从库同步的逻辑如下:

  • 从库 B 指定主库 A,基于主备协议建立连接。
  • 从库 B 把集合 y 发给主库 A。
  • 主库 A 计算出集合 x 和集合 y 的差集,也就是集合 x 中存在,集合 y 中不存在的 GTID 集合。比如集合 x 是 1~100,集合 y 是 1~90,那么这个差集就是 91~100。这里会判断集合 x 是不是包含有集合 y 的所有 GTID,如果不是则说明主库 A 删除了从库 B 需要的 binlog,主库 A 直接返回错误。
  • 主库 A 从自己的 binlog 文件里面,找到第一个不在集合 y 中的事务GTID,也就是找到了 91。
  • 主库 A 从 GTID = 91 的事务开始,往后读 binlog 文件,按顺序取 binlog,然后发给 B。从库 B 的 I/O 线程读取 binlog 文件生成 relay log,SQL 线程解析 relay log,然后执行 SQL 语句。

GTID 同步方案和位点同步的方案区别是:

  • 位点同步方案是通过人工在从库上指定哪个位点,主库就发哪个位点,不做日志的完整性判断。
  • 而 GTID 方案是通过主库来自动计算位点的,不需要人工去设置位点,对运维人员友好。

GTID的配置

1) 修改主库配置
修改主库的配置文件

复制代码
#GTID:
#启用全局事务标识符(GTID)模式    
gtid_mode=on    
# 强制GTID的一致性。这意味着在执行事务时,MySQL将确保所有涉及的服务器都使用相同的GTID集。
enforce_gtid_consistency=on  

2)修改从库配置

修改从库配置文件

复制代码
#GTID:
gtid_mode=on  
enforce_gtid_consistency=on

从节点设置主库信息

复制代码
# 从库配置同步参数
mysql> CHANGE MASTER TO
     >     MASTER_HOST = host,
     >     MASTER_PORT = port,
     >     MASTER_USER = user,
     >     MASTER_PASSWORD = password,
     >     MASTER_AUTO_POSITION = 1;  

Or from MySQL 8.0.23:
mysql> CHANGE REPLICATION SOURCE TO
     >     SOURCE_HOST = host,
     >     SOURCE_PORT = port,
     >     SOURCE_USER = user,
     >     SOURCE_PASSWORD = password,
     >     SOURCE_AUTO_POSITION = 1;

SOURCE_AUTO_POSITION = 1: 这告诉从服务器使用自动位置跟踪功能,以便它可以自动从主服务器获取最新的二进制日志事件,而无需手动指定位置。

基于GTID主从复制示例

文档:https://dev.mysql.com/doc/refman/8.0/en/replication-gtids-howto.html

在前面基于binlog日志主从复制的mysql服务上配置GTID复制。

  1. 同步所有的mysql服务器

在主从服务器上都执行下面的命令:

复制代码
# 设置MySQL服务器的全局只读模式
mysql> SET @@GLOBAL.read_only = ON;

注意:只有在使用已经在进行复制而不使用gtid的服务器时才需要此步骤。对于新服务器,请继续执行步骤3。

  1. 停止所有的服务器

    docker stop mysql-source mysql-replica1 mysql-replica2

  2. 主从节点都启用GTID

修改custom.cnf

复制代码
# 启用GTID
gtid_mode=ON
enforce-gtid-consistency=ON

启动主从节点

复制代码
docker start mysql-source mysql-replica1 mysql-replica2

4)从节点配置基于GTID的自动定位

进入从节点,执行下面命令:

复制代码
mysql> stop replica;
mysql> change replication source to source_host='192.168.65.185', source_user='fox', source_password='123456', source_port=3307,source_auto_position=1;
  1. 开启从库复制,并禁用只读模式

    开启从库复制

    mysql> start replica;

    只有在步骤1中将服务器配置为只读时,才需要执行以下步骤。要允许服务器再次开始接受更新

    mysql> SET @@GLOBAL.read_only = OFF;

查看从库状态是否正常

复制代码
mysql> show replica status \G
主从切换演练

场景1: 模拟主库down机、从库1数据同步完成、从库2数据未同步完成

1)从库2停止复制

复制代码
mysql> stop replica;

2)主库创建测试数据

复制代码
INSERT INTO `test`.`user` VALUES (12, 'fox', NULL, NULL, NULL);
  1. 查询数据

从库1

从库2

很显然,从库1同步了最新数据,比从库2数据新

场景2:将主库宕机,从库1升级为主库、从库2切换主库为从库1(新的主库),观察从库2是否同步未完成的事务

1)停止主库

复制代码
docker stop mysql-source

2)设置新主库

设置replica1 为replica2的主库,因为replica1的数据是完整的。

按照普通复制方法,需要计算主库的log_pos和从库设置成主库的log_pos,可能出现错误

因为同一事务的GTID在所有节点上的值一致,那么根据replica2当前停止点的GTID就能定位到要主库的GTID,所以直接在replica2上执行change即可

复制代码
# replica1上创建复制用户
CREATE USER 'fox'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
GRANT REPLICATION SLAVE ON *.* TO 'fox'@'%';
flush privileges;

# replica2上执行 从replica1查询
mysql> stop replica;
mysql> change replication source to source_host='192.168.65.185',source_port=3308,source_user='fox',source_password='123456',source_auto_position=1;
 mysql> start replica; 

查询同步结果

场景3:模拟从库删除测试表,主库对表进行插入操作。观察从库复制是否报错

1)replica1删除test.user表,主库插入新记录

复制代码
# 从库1 删除user表
drop table test.user;
# 主库插入新记录
INSERT INTO `test`.`user` VALUES (14, 'AAA', NULL, NULL, NULL);
  1. 查看从库同步情况

    从库1执行

    mysql> show replica status\G

报错信息:事务aac92b21-b6a4-11ee-bab5-0242ac120002:6执行失败

Coordinator stopped because there were error(s) in the worker(s). The most recent failure being: Worker 1 failed executing transaction 'aac92b21-b6a4-11ee-bab5-0242ac120002:6' at master log mysql-bin.000008, end_log_pos 1335. See error log and/or performance_schema.replication_applier_status_by_worker table for more details about this failure or others, if any.

3)在主库继续进行其他事务,观察gitd是否复制成功

复制代码
# 主库插入新记录
INSERT INTO `test`.`user` VALUES (15, 'BBB', NULL, NULL, NULL);

从库状态

可以看出从库复制中断 (注意:删除了表,无法插入)

4)复制中断修复:采用从库跳过错误事务修复

因为从库user表已经删了(user表中部分数据不是利用gtid复制过去的),先从主库将表数据拷贝到从库

获取从库最新状态

从库执行

复制代码
# 1.停止从库1复制进程
mysql> stop replica;
# 2.设置事务号,事务号从 Retrieved_Gtid_Set 获取,在session里设置gtid_next,即跳过这个GTID
# 注意,选择跳过出现错误的事务
mysql> SET @@SESSION.GTID_NEXT= 'aac92b21-b6a4-11ee-bab5-0242ac120002:7';
# 3.设置空事物
mysql> BEGIN; COMMIT;
# 4.恢复自增事物号
mysql> SET SESSION GTID_NEXT = AUTOMATIC;
# 5.启动从库1复制进程
mysql> start replica;
# 再次查询会发现主库数据已经同步过来了
mysql> select * from test.user;

如果需要一次跳过多条,找出需要跳过的gtid,批量执行

复制代码
stop replica; 
SET @@SESSION.GTID_NEXT= 'd58e6bad-ef3e-11ee-8285-0242ac120003:4';begin;commit;
SET @@SESSION.GTID_NEXT= 'd58e6bad-ef3e-11ee-8285-0242ac120003:5';begin;commit;
SET @@SESSION.GTID_NEXT= 'd58e6bad-ef3e-11ee-8285-0242ac120003:6';begin;commit;
SET SESSION GTID_NEXT = AUTOMATIC;
start replica;

完整的演示步骤:

1.从库删除user表

复制代码
mysql> drop table user;

2.主库连续删除两条数据

复制代码
mysql> select * from user;
+----+--------+-----------------+--------------+------------+
| id | name   | address         | last_updated | is_deleted |
+----+--------+-----------------+--------------+------------+
|  2 | 张三   | 广州白云山      |   1691563465 |          0 |
|  3 | fox    | NULL            |         NULL |       NULL |
|  4 | 李四   | NULL            |         NULL |       NULL |
|  5 | 111    | NULL            |         NULL |       NULL |
| 12 | fox    | NULL            |         NULL |       NULL |
| 14 | AAA    | NULL            |         NULL |       NULL |
| 15 | BBB    | NULL            |         NULL |       NULL |
| 16 | CCC    | NULL            |         NULL |       NULL |
| 20 | DDD    | NULL            |         NULL |       NULL |
| 22 | EEE    | NULL            |         NULL |       NULL |
+----+--------+-----------------+--------------+------------+
10 rows in set (0.00 sec)

mysql> delete from user where id=20;
Query OK, 1 row affected (0.19 sec)

mysql> select * from user;
+----+--------+-----------------+--------------+------------+
| id | name   | address         | last_updated | is_deleted |
+----+--------+-----------------+--------------+------------+
|  2 | 张三   | 广州白云山      |   1691563465 |          0 |
|  3 | fox    | NULL            |         NULL |       NULL |
|  4 | 李四   | NULL            |         NULL |       NULL |
|  5 | 111    | NULL            |         NULL |       NULL |
| 12 | fox    | NULL            |         NULL |       NULL |
| 14 | AAA    | NULL            |         NULL |       NULL |
| 15 | BBB    | NULL            |         NULL |       NULL |
| 16 | CCC    | NULL            |         NULL |       NULL |
| 22 | EEE    | NULL            |         NULL |       NULL |
| 23 | 333    | NULL            |         NULL |       NULL |
+----+--------+-----------------+--------------+------------+
10 rows in set (0.00 sec)

mysql> delete from user where id=22;
Query OK, 1 row affected (0.29 sec)

mysql> select * from user;
+----+--------+-----------------+--------------+------------+
| id | name   | address         | last_updated | is_deleted |
+----+--------+-----------------+--------------+------------+
|  2 | 张三   | 广州白云山      |   1691563465 |          0 |
|  3 | fox    | NULL            |         NULL |       NULL |
|  4 | 李四   | NULL            |         NULL |       NULL |
|  5 | 111    | NULL            |         NULL |       NULL |
| 12 | fox    | NULL            |         NULL |       NULL |
| 14 | AAA    | NULL            |         NULL |       NULL |
| 15 | BBB    | NULL            |         NULL |       NULL |
| 16 | CCC    | NULL            |         NULL |       NULL |
| 23 | 333    | NULL            |         NULL |       NULL |
+----+--------+-----------------+--------------+------------+
9 rows in set (0.00 sec)
  1. 借助Navicat将主库user表复制到从库,包括数据。 这样一来从库就有了当前的全量数据,只需要跳过错误的gtid,从最新的gtid开始执行就可以了。

    查询从库,确定数据是否和主库一致

    mysql> select * from user;
    +----+--------+-----------------+--------------+------------+
    | id | name | address | last_updated | is_deleted |
    +----+--------+-----------------+--------------+------------+
    | 2 | 张三 | 广州白云山 | 1691563465 | 0 |
    | 3 | fox | NULL | NULL | NULL |
    | 4 | 李四 | NULL | NULL | NULL |
    | 5 | 111 | NULL | NULL | NULL |
    | 12 | fox | NULL | NULL | NULL |
    | 14 | AAA | NULL | NULL | NULL |
    | 15 | BBB | NULL | NULL | NULL |
    | 16 | CCC | NULL | NULL | NULL |
    | 23 | 333 | NULL | NULL | NULL |
    +----+--------+-----------------+--------------+------------+
    9 rows in set (0.00 sec)

  2. 获取从库需要配置跳过错误的事务

    获取到错误的gtid为d58e6bad-ef3e-11ee-8285-0242ac120003:12,

    因为连续删除过两条数据,所以d58e6bad-ef3e-11ee-8285-0242ac120003:13也是错误的事务

    mysql> show replica status\G;
    *************************** 1. row ***************************
    Replica_IO_State: Waiting for source to send event
    Source_Host: 192.168.65.223
    Source_User: fox
    Source_Port: 3308
    Connect_Retry: 30
    Source_Log_File: mysql-bin.000004
    Read_Source_Log_Pos: 4229
    Relay_Log_File: 3c946c781110-relay-bin.000012
    Relay_Log_Pos: 790
    Relay_Source_Log_File: mysql-bin.000004
    Replica_IO_Running: Yes
    Replica_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: 1146
    Last_Error: Coordinator stopped because there were error(s) in the worker(s). The most recent failure being: Worker 1 failed executing transaction 'd58e6bad-ef3e-11ee-8285-0242ac120003:12' at master log mysql-bin.000004, end_log_pos 3906. See error log and/or performance_schema.replication_applier_status_by_worker table for more details about this failure or others, if any.
    Skip_Counter: 0
    Exec_Source_Log_Pos: 3645
    Relay_Log_Space: 1932
    Until_Condition: None
    Until_Log_File:
    Until_Log_Pos: 0
    Source_SSL_Allowed: No
    Source_SSL_CA_File:
    Source_SSL_CA_Path:
    Source_SSL_Cert:
    Source_SSL_Cipher:
    Source_SSL_Key:
    Seconds_Behind_Source: NULL
    Source_SSL_Verify_Server_Cert: No
    Last_IO_Errno: 0
    Last_IO_Error:
    Last_SQL_Errno: 1146
    Last_SQL_Error: Coordinator stopped because there were error(s) in the worker(s). The most recent failure being: Worker 1 failed executing transaction 'd58e6bad-ef3e-11ee-8285-0242ac120003:12' at master log mysql-bin.000004, end_log_pos 3906. See error log and/or performance_schema.replication_applier_status_by_worker table for more details about this failure or others, if any.
    Replicate_Ignore_Server_Ids:
    Source_Server_Id: 11
    Source_UUID: d58e6bad-ef3e-11ee-8285-0242ac120003
    Source_Info_File: mysql.slave_master_info
    SQL_Delay: 0
    SQL_Remaining_Delay: NULL
    Replica_SQL_Running_State:
    Source_Retry_Count: 86400
    Source_Bind:
    Last_IO_Error_Timestamp:
    Last_SQL_Error_Timestamp: 240401 14:17:40
    Source_SSL_Crl:
    Source_SSL_Crlpath:
    Retrieved_Gtid_Set: d58e6bad-ef3e-11ee-8285-0242ac120003:1-13,
    dfb1f706-ef3e-11ee-82cc-0242ac120004:2
    Executed_Gtid_Set: ce6baf4a-ef3e-11ee-99fb-0242ac120002:1-13,
    d58e6bad-ef3e-11ee-8285-0242ac120003:1-11,
    dfb1f706-ef3e-11ee-82cc-0242ac120004:1-2
    Auto_Position: 1
    Replicate_Rewrite_DB:
    Channel_Name:
    Source_TLS_Version:
    Source_public_key_path:
    Get_Source_public_key: 0
    Network_Namespace:
    1 row in set (0.00 sec)

  3. 在主库再次执行删除操作

    mysql> delete from user where id=12;
    Query OK, 1 row affected (0.11 sec)

    mysql> select * from user;
    +----+--------+-----------------+--------------+------------+
    | id | name | address | last_updated | is_deleted |
    +----+--------+-----------------+--------------+------------+
    | 2 | 张三 | 广州白云山 | 1691563465 | 0 |
    | 3 | fox | NULL | NULL | NULL |
    | 4 | 李四 | NULL | NULL | NULL |
    | 5 | 111 | NULL | NULL | NULL |
    | 14 | AAA | NULL | NULL | NULL |
    | 15 | BBB | NULL | NULL | NULL |
    | 16 | CCC | NULL | NULL | NULL |
    | 23 | 333 | NULL | NULL | NULL |
    +----+--------+-----------------+--------------+------------+

6.在从库

复制代码
mysql> select * from user;
+----+--------+-----------------+--------------+------------+
| id | name   | address         | last_updated | is_deleted |
+----+--------+-----------------+--------------+------------+
|  2 | 张三   | 广州白云山      |   1691563465 |          0 |
|  3 | fox    | NULL            |         NULL |       NULL |
|  4 | 李四   | NULL            |         NULL |       NULL |
|  5 | 111    | NULL            |         NULL |       NULL |
| 12 | fox    | NULL            |         NULL |       NULL |
| 14 | AAA    | NULL            |         NULL |       NULL |
| 15 | BBB    | NULL            |         NULL |       NULL |
| 16 | CCC    | NULL            |         NULL |       NULL |
| 23 | 333    | NULL            |         NULL |       NULL |
+----+--------+-----------------+--------------+------------+
9 rows in set (0.00 sec)

mysql> stop replica;
Query OK, 0 rows affected (0.07 sec)
# 如果需要一次跳过多条,找出需要跳过的gtid,批量执行
mysql> SET @@SESSION.GTID_NEXT= 'd58e6bad-ef3e-11ee-8285-0242ac120003:12';begin;commit;
Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.12 sec)

mysql> SET @@SESSION.GTID_NEXT= 'd58e6bad-ef3e-11ee-8285-0242ac120003:13';begin;commit;
Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

mysql> SET SESSION GTID_NEXT = AUTOMATIC;
Query OK, 0 rows affected (0.00 sec)

mysql> start replica;
Query OK, 0 rows affected (1.10 sec)

mysql> select * from user;
+----+--------+-----------------+--------------+------------+
| id | name   | address         | last_updated | is_deleted |
+----+--------+-----------------+--------------+------------+
|  2 | 张三   | 广州白云山      |   1691563465 |          0 |
|  3 | fox    | NULL            |         NULL |       NULL |
|  4 | 李四   | NULL            |         NULL |       NULL |
|  5 | 111    | NULL            |         NULL |       NULL |
| 14 | AAA    | NULL            |         NULL |       NULL |
| 15 | BBB    | NULL            |         NULL |       NULL |
| 16 | CCC    | NULL            |         NULL |       NULL |
| 23 | 333    | NULL            |         NULL |       NULL |
+----+--------+-----------------+--------------+------------+
8 rows in set (0.01 sec)
相关推荐
Mr_Xuhhh2 小时前
MySQL表的内连接与外连接详解
java·前端·数据库
kyle-fang2 小时前
阿里云服务器部署MySQL
服务器·mysql·阿里云
l1t2 小时前
DeepSeek辅助总结postgresql wiki提供的数独求解器
数据库·sql·postgresql
appearappear2 小时前
大数据量处理
数据库
万行2 小时前
SQL进阶&索引篇
开发语言·数据库·人工智能·sql
我是黄骨鱼2 小时前
【零基础学数据库|第二篇】MySql启动!!!
数据库·mysql
陌上丨2 小时前
什么是Redis的大Key和热Key?项目中一般是怎么解决的?
数据库·redis·缓存
Remember_9932 小时前
Spring 事务深度解析:实现方式、隔离级别与传播机制全攻略
java·开发语言·数据库·后端·spring·leetcode·oracle
小园子的小菜2 小时前
深入剖析HBase HFile原理:文件结构、Block协作与缓存机制
数据库·缓存·hbase