MySQL 5.6 全局事务 ID(GTID)实现原理(三)

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 这是 MySQL 5.6 全局事务 ID(GTID) 系列的第三篇博客。   在之前的两篇博客中,第一篇​ 介绍了全局事务 ID 的定义与数据结构。第二篇​ 介绍了 MySQL 5.6 新增的全局事务状态(Gtid_state)。

这是 MySQL 5.6 全局事务 ID(GTID) 系列的第三篇博客。

 
在之前的两篇博客中, 第一篇​ 介绍了全局事务 ID 的定义与数据结构。 第二篇​ 介绍了 MySQL 5.6 新增的全局事务状态(Gtid_state)。
 
这里准备介绍的是全局事务 ID 如何参与 MySQL 的主备复制流程。
 
MySQL 5.6 引入全局事务 ID 的首要目的,是保证 Slave 在复制的时候不会重复执行相同的事务操作;其次,是用全局事务 IDs 代替由文件名和物理偏移量组成的复制位点,定位 Slave 需要复制的 binlog 内容。
 
因此,MySQL 必须在写 binlog 时记录每个事务的全局 GTID,保证 Master / Slave 可以根据这些 GTID 忽略或者执行相应的事务。在实现上,MySQL 没有修改旧的 binlog 事件,而是新增了两类事件:
 
+----------------------------+----------------------------------------+
| 名称                                    | 功能                                                      |
+----------------------------+----------------------------------------+
| Previous_gtids_log_event | 该事件之前的全局事务 ID 集合。          |
+----------------------------+----------------------------------------+
|                  Gtid_log_event | 标记之后的事务对应的全局事务 ID。    |
+----------------------------+----------------------------------------+
 
Gtid_log_event
 
在 MySQL 5.6 的 binlog 文件中,每个事务的开始不是 "BEGIN" ,而是 Gtid_log_event 事件:
 
(图片来源: MySQL_Innovation_Day_Replication_HA.pdf​)
 
它里面只包含一条 GTID,记录结构如下:
 
Gtid_log_event := (commit_flag, sid, gno)   // commit_flag 目前总是 true
 
里面 sid 就是产生该事务的 server_uuid,gno 是顺序编号的 transaction_id。
 
把 Gtid 记录在事务的开头是为了便于 MySQL 过滤 binlog:检查到某个 Gtid 不需要时,可以直接忽略后面的整段事务。
 
MySQL 5.6 保证同时写入 Gtid_log_event 和全局 logged_gtids 状态:
 
第一步,在向 binlog_cache_data 写入第一条 binlog 前,MySQL 会在缓存的 buffer 中写入一个空的 Gtid_log_event 占位。
 
第二步,当 binlog_cache_data 的内容刷到 binlog 文件时,MySQL 会把位于缓存 buffer 的 Gtid_log_event 内容替换成实际的 GTID,重新写入缓存。
 
最后,MySQL 调用 Gtid_state 的 update_on_flush() 把 GTID 写入 logged_gtids,再调用 sync_binlog_file()  保证内容更新到磁盘。 
 
在主备复制中,Slave 不像 Master 那样自动产生 GTID,而是直接拷贝 Gtid_log_event 中包含的 GTID。这个特性是这样实现的 —— MySQL 5.6 维护了一个线程(Session)级别的变量  gtid_next,类型为 Gtid_specification:
 
Gtid_specification := (enum_group_type, Gtid)
 
enum_group_type :=enum(AUTOMATIC_GROUP, GTID_GROUP, ANONYMOUS_GROUP, INVALID_GROUP, UNDEFINED_GROUP)
 
在 Master 执行事务时,gtid_next 的类型默认是 AUTOMATIC_GROUP,表示应该调用 generate_automatic_gno() 自动产生全局事务 ID。
 
而在 Slave 执行事务时,先用 Gtid_log_event 内的 Gtid 覆盖 gtid_next,使它的类型为 GTID_GROUP。这样的话,MySQL 会使用 gtid_next 内设置的 Gtid  值作为下一个全局事务 ID。
 
Previous_gtids_log_event
 
这个事件出现在 MySQL 5.6 每个 binlog 文件的开始处。
 
MySQL 创建一个新的 binlog 文件后,首先写入一个 Format_description_log_event 描述,接着写入一个 Previous_gtids_log_event,内容是在创建这个 binlog 文件之前执行的全局事务 GTIDs。 
 
事件的格式很简单,就是字符串编码的 Gdit_set:(编码格式参考本文  第一篇​)
 
Previous_gtids_log_event := buffer of Gtid_set
(例如:3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5)
 
这个事件只是作为记录。在主备复制时,Slave 会忽略 binlog 里的 Previous_gtids_log_event 事件。
 
Binlog 与持久化全局事务状态
 
在  上一篇​ 没有讲到 MySQL 5.6 如何持久化全局事务状态的 —— logged_gtids 和 lost_gtids 状态里存储了这台数据库  有史以来 执行的所有 GTIDs(包括删除 binlog 中的 GTIDs)—— 如果数据库停机或崩溃前不做持久化,之后肯定丢失信息。
 
MySQL 的解决方案很简单,在启动时扫描剩余的 binlog 文件,用文件存储的 Previous_gtids_log_event 和 Gtid_log_event 事件内容恢复全局 logged_gtids 和 lost_gtids 状态。
 
具体的代码如下:(源代码:mysql-5.6.9-rc\sql\binlog.cc,line 2558)
 
第一步,找到最后一个 binlog 文件,读出 Previous_gtids_log_event 记录;再遍历 binlog 文件中所有的 Gtid_log_event,  把找到的 GTID 记录合并起来,作为这台数据库历史上执行的所有 GTIDs 放入全局 logged_gtids 记录;
 
第二步,找到第一个 binlog 文件,用它的 Previous_gtids_log_event 信息代替全局 lost_gtids 的内容。因为这是第一个未删除的 binlog 文件,这里记录的就是之前已经删除的 binlog 文件所包含的全部 GTIDs。
 
由于 MySQL 在提交事务中是最后才写入真实 Gtid_log_event 信息的,从 binlog 恢复信息,可以保证读到的 GTIDs 与成功执行的事务一致。
 
CHANGE MASTER TO ... 
 
MySQL 5.6 主备复制的一个改变,是新增了 COM_BINLOG_DUMP_GTID 协议,支持在 Slave 切换到新 Master 时,用 MASTER_AUTO_POSITION = 1 (auto_position 方式)代替原来的 binlog 文件名和物理偏移量。
 
COM_BINLOG_DUMP_GTID 协议并不复杂,请求格式如下:

 

Request = { server_id, binlog_name, binlog_offset, gtids_executed }  
 
如果采用 auto_position 方式连接 Master,现在 Slave 发送的 binlog_name 和 binlog_offset 都是空白,Master 只使用 gtids_executed 定位 Slave 上需要执行的 binlog。
 
实现逻辑是这样的:Master 从第一个文件开始读取 binlog,逐个检查 Gdit_log_event 事件的全局事务 ID 是不是包含在 Slave 发送的 gtids_executed 集合中。如果发现这个 GTID 已经包含在 gtids_executed 集合内,就忽略后面的整段事务,不向 Slave 发送 binlog 内容。
 
其实这个过程还不是很优化,因为如果是正常情况,Master 需要遍历若干 G 的 binlog 才能找到 Slave 需要复制的 binlog 内容 —— 这应该是一个改进点。
 
全局事务 ID 与并发复制
 
MySQL 5.6 主备复制的另一个改变,是实现了多线程并行复制。这个功能必须有全局事务 ID 的支持,原因是:
 
1) 在并行复制方式下,有些操作是不按照记录在 binlog 中的顺序执行的。这样的话,如果按照文件名 + 物理偏移量的方式记录复制位点,则停止 / 恢复主备复制时,可能会有一些操作被重复执行。
 
2) 我们知道,即使是 Mixed / Row 模式下记录的 binlog,仍有些 DDL 操作是用 Statement 的方式编码的,这些 DDL 操作不能在 Slave 重复执行(因为非幂等)。一旦操作在 Slave 执行出错,结果就是复制中断。
 
因此,Slave 必须依赖 binlog 中的全局事务 ID,在停止 / 恢复主备复制时,精确的记录哪些事务在 Slave 执行过,哪些没有。
 
现在,MySQL 5.6 可以用 COM_BINLOG_DUMP_GTID 来保证这一点:在恢复主备复制时,Slave 向 Master 发送自己所有执行过的 GTIDs(logged_gtids),在上次中断主备复制时,已经执行过的 binlog 被 Master 直接滤掉,不向 Slave 传送。
 
总结
 
在主备复制上,MySQL 5.6 新增了三个特性:
 
1)使用 GTIDs 作为主备复制的位点,在写 binlog 时用 Gtid_log_event 标记事务。
 
2)支持 auto_position 方式进行主备切换。在新增的协议中,使用 GTIDs 作为复制位点向主库请求 binlog 信息。
 
3)多线程并发复制,使用 GTIDs 防止事务重复执行。
 
全局事务 ID(GTID)可以很好的支持这几个功能。而且,使用 GTIDs 避免了在传送 binlog 逻辑上依赖文件名和物理偏移量,能够更好的支持自动容灾切换。
 
但是个人感觉,全局事务 ID 这里还有些待解决的问题:
 
1)GTID 是局部有序的,不能记录事务的全局顺序。因此在双写 / 快速主备切换场景下,不能根据 GTID 顺序来解决更新冲突的问题。
 
2)容灾切换时,MASTER_AUTO_POSITION 只能解决记录位点的问题。为了保证一致性,停写和等待主备 Caught up 仍然是必须的,通常这是服务无法快速恢复的主要原因。
 
补充:参考资料
 
这篇博客用到的参考资料:
 
MySQL 5.6 Manual:Replication with Global Transaction Identifiers( link​)

​​WL#4677: Unique Server Ids for Replication Topology (UUIDs)​​(link​)

WL#3584: Global Transaction Identifiers (GTIDs)( link​)

​​

顺便提下, MySQL Worklog​ 是个好地方,你可以从这里了解 MySQL 的原始需求,开发人员的想法,还有值得关注的问题。
相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
3天前
|
缓存 关系型数据库 MySQL
MySQL 索引优化与慢查询优化:原理与实践
通过本文的介绍,希望您能够深入理解MySQL索引优化与慢查询优化的原理和实践方法,并在实际项目中灵活运用这些技术,提升数据库的整体性能。
22 5
|
2天前
|
存储 SQL 关系型数据库
MySQL进阶突击系列(03) MySQL架构原理solo九魂17环连问 | 给大厂面试官的一封信
本文介绍了MySQL架构原理、存储引擎和索引的相关知识点,涵盖查询和更新SQL的执行过程、MySQL各组件的作用、存储引擎的类型及特性、索引的建立和使用原则,以及二叉树、平衡二叉树和B树的区别。通过这些内容,帮助读者深入了解MySQL的工作机制,提高数据库管理和优化能力。
|
15天前
|
SQL 存储 关系型数据库
MySQL进阶突击系列(01)一条简单SQL搞懂MySQL架构原理 | 含实用命令参数集
本文从MySQL的架构原理出发,详细介绍其SQL查询的全过程,涵盖客户端发起SQL查询、服务端SQL接口、解析器、优化器、存储引擎及日志数据等内容。同时提供了MySQL常用的管理命令参数集,帮助读者深入了解MySQL的技术细节和优化方法。
|
16天前
|
SQL 缓存 关系型数据库
MySQL Limit实现原理
本文深入解析了MySQL中`LIMIT`子句的实现原理及其在分页、性能优化等场景下的应用技巧。文章详细介绍了`LIMIT`的基本语法、MySQL内部处理流程,以及如何通过索引优化、覆盖索引等策略提升分页查询的性能,并提供了实践建议。
|
2月前
|
存储 SQL 关系型数据库
MySQL的事务隔离级别
【10月更文挑战第17天】MySQL的事务隔离级别
122 43
|
21天前
|
关系型数据库 MySQL
mysql事务特性
原子性:一个事务内的操作统一成功或失败 一致性:事务前后的数据总量不变 隔离性:事务与事务之间相互不影响 持久性:事务一旦提交发生的改变不可逆
|
2月前
|
存储 关系型数据库 MySQL
MySQL主从复制原理和使用
本文介绍了MySQL主从复制的基本概念、原理及其实现方法,详细讲解了一主两从的架构设计,以及三种常见的复制模式(全同步、异步、半同步)的特点与适用场景。此外,文章还提供了Spring Boot环境下配置主从复制的具体代码示例,包括数据源配置、上下文切换、路由实现及切面编程等内容,帮助读者理解如何在实际项目中实现数据库的读写分离。
MySQL主从复制原理和使用
|
1月前
|
监控 关系型数据库 MySQL
MySQL自增ID耗尽应对策略:技术解决方案全解析
在数据库管理中,MySQL的自增ID(AUTO_INCREMENT)属性为表中的每一行提供了一个唯一的标识符。然而,当自增ID达到其最大值时,如何处理这一情况成为了数据库管理员和开发者必须面对的问题。本文将探讨MySQL自增ID耗尽的原因、影响以及有效的应对策略。
105 3
|
1月前
|
存储 监控 关系型数据库
MySQL自增ID耗尽解决方案:应对策略与实践技巧
在MySQL数据库中,自增ID(AUTO_INCREMENT)是一种特殊的属性,用于自动为新插入的行生成唯一的标识符。然而,当自增ID达到其最大值时,会发生什么?又该如何解决?本文将探讨MySQL自增ID耗尽的问题,并提供一些实用的解决方案。
38 1
|
20天前
|
关系型数据库 MySQL 数据库
MySQL事务隔离级别及默认隔离级别的设置
在数据库系统中,事务隔离级别是一个关键的概念,它决定了事务在并发执行时如何相互隔离。MySQL提供了四种事务隔离级别,每种级别都解决了不同的并发问题。本文将详细介绍这些隔离级别以及MySQL的默认隔离级别。