MySQL 作为一个后端开发中常用的数据库,我们经常使用。对于 MySQL 进行深入的了解,能够使我们在使用 MySQL 进行开发的过程中更加得心应手。在追求 MySQL 的高性能、高可用的过程中,避不开的便是搭建 MySQL 的集群。无论是为了高可用搭建 MySQL 的热备、还是为了高性能搭建 MySQL 读写副本(如下图),避不开的都要使用 MySQL 自带的复制功能,实现两个实例之间的数据同步。可以说复制是作为 MySQL 高可用的基石,那么它需要满足那些功能点?如何实现?当前实现下又有那些优缺点 ?接下来就让我们进入 MySQL 复制的世界,一探究竟~
2. 发展历史 2.1 时间线复制实现需要关注的点
- 数据一致性(如果两个实例之间数据不同,则其他特性再好也无用)
- 数据同步速度(如果两个实例之间数据同步太慢,无法满足热备切换)
- 对正常请求的影响(如果开启复制导致正常请求相应时间翻倍甚至更长,则代价太大)
基于复制发展的时间线引入的各种功能,以及复制功能需要关注的时间点,我对功能点做了一个简单的分类。
数据同步格式
- 基于行复制
- 基于语句复制
- 混合复制
数据同步方式
- 基础模式
- 基于库的并行复制
- 基于组提交的并行复制
- 基于记录集的并行复制
数据同步的时机
- 异步复制
- 全同步复制
- 半同步复制
- 无损复制
其他功能
3. 前置基础知识
- Delayed replication(延迟复制)
- Global Transaction ID (全局事务标示)
- Multi source replication(多源复制)
- MySQL Group Replication
在正式开始讲解复制功能之前,我们需要准备一些前置知识,才能更好的理解复制功能中一些实现的细节。
3.1 MySQL 架构MySQL 整体架构分为两层,一层是 Server 层包含连接器、查询缓存(即将取消)、分析器、优化器、执行器,一层是引擎层提供可插拔的各种引擎(InnoDB、MYISAM)。这个地方我们需要关注的是在 Server 层负责写入 BinLog,下层引擎层中的 InnoDB 写入 UndoLog 和 RedoLog。
3.2 Commit 二阶段流程在 MySQL 事务实现的过程中会使用到二阶段提交。
-
Execution Stage(内存中执行,会涉及到事务需要的各种加锁)
-
Prepare Stage(引擎层写入 RedoLog)
-
Binlog Stage (Server 层写入 BingLog)
-
Innodb Commit Stage(写入 commit log)
在事务中需要将 binLog 或者 redoLog 之类的日志持久化到磁盘,但是同步到磁盘是一个耗时的 *** 作。如果每个事务都执行一次同步,那么对于性能将产生较大的损耗,所以就产生了组提交技术。组提交将多个刷盘 *** 作合并成一个,如果说10个事务依次排队刷盘的时间成本是10,那么将这10个事务一次性一起刷盘的时间成本则近似于1。
组提交实现详情可以参照这篇文章:developer.aliyun.com/article/617…
4. 基本模型-
主节点必须启用二进制日志,记录任何修改了数据库数据的事件。
-
从节点开启一个线程(I/O Thread)把自己扮演成 mysql 的客户端,通过 mysql 协议,请求主节点的二进制日志文件中的事件
-
主节点启动一个线程(dump Thread),检查自己二进制日志中的事件,跟对方请求的位置对比,如果不带请求位置参数,则主节点就会从第一个日志文件中的第一个事件一个一个发送给从节点。
-
从节点接收到主节点发送过来的数据把它放置到中继日志(Relay log)文件中。并记录该次请求到主节点的具体哪一个二进制日志文件内部的哪一个位置(主节点中的二进制文件会有多个,在后面详细讲解)。
-
从节点启动另外一个线程(sql Thread ),把 Relay log 中的事件读取出来,并在本地再执行一次。
5. 数据同步格式在最开始的复制实现版本中,I/O thread 和 SQL thread 为一个线程,后续将 I/O thread 和 SQL Thread 拆开,但是 I/O thread 和 SQL Thread 依然是单线程。由于 Master 实例是多线程并发执行,而 Slave 仅仅是一个单线程执行,所以导致在这种模式下,在 Master 并发较高时,会出现比较严重的主从延迟。
数据同步的格式分为三种:
-
基于语句复制
-
基于行复制
-
混合复制
基于语句复制实现原理就是从库(slave)基于产生变化的SQL语句从主库(master)进行复制。在MySQL5.1.4版本之前是binlog和复制唯一支持的模式,也是MySQL5.5中默认的格式。
优点
-
久经考验、技术成熟
-
产生数据少
例如 delete 一张表的所有记录,假设有 10000 行,基于语句则只需要写入一行 SQL,而基于行则需要写入每一行的变更情况
- 由于 binLog 文件记录了所有变更,可以用来审计数据库
缺点
- 基于语句复制,可能产生数据不一致。
例如在主库上执行不确定函数 UUID,每次执行产生的数据都不一样,就会导致主从数据不一致。还有一些其他的情况可以参考 MySQL 官方文档。
- 对于复杂的语句,在 Slave 执行前还需要进行解析,优化。
基于行的复制不复制SQL语句,而是将插入,删除或更新 *** 作的各行进行复制。
优点
-
所有的改变都可复制,这是最安全的复制模式。
-
基于行复制的实现的技术和其它大多数的数据库是一样的。
-
在Master上需要的行锁会很少,从而获得更高的并发性。
缺点
-
需要写入更多的数据
-
大 SQL 产生大量行记录变更,会导致复制变慢
-
不能从 binLog 中看到执行过那些具体的语句(目前有工具可以翻译行记录)
-
对MyISAM表并发插入时,不支持基于行的模式。
混合模式复制背后的原理很简单:正常情况下使用基于语句的复制,对于不安全的语句切换为基于行的复制。
5.4 目前主流使用的复制模式现在使用最多的依然是行复制模式,虽然混合模式可以结合基于行和基于语句的优点。我理解是因为在我们日常的使用中,MySQL 的复制不仅仅是用来作为主从实例之间的复制,还有用来作为不同存储介质之间的数据同步。例如有时候我们会将 MySQL 的 BinLog 同步到 ES 中作为搜索,也会将 MySQL 同步到 kafka 作为流式计算的源头。所以需要统一的格式。
6. 数据同步方式数据同步的方式分为四类:
-
基础模式
-
基于库的并行复制
-
基于组提交的并行复制
-
基于记录集的并行复制
基础模式就是在第 4 节提到的基础模型,基础模型存在的问题,我们也提到了基础模型的缺点,即 Slave 实例的 I/O thread 以及 SQL thread 都是单线程,在 master 并发较高时,将和 Master 产生较大的主从延迟。
6.2 基于库的并行复制基于库的并行复制原理非常简单,即在 Slave 上执行变更时,不同库的变更可以并发执行。因为 MySQL 不存在跨库事务,不同库之间不存在竞争,所以可以直接并发执行。
基于库并行复制并没有解决多少问题,因为目前在高并发的情况下,我们时常将重要的库单独放在一个实例上面,这个时候基于库的并行复制就失去了作用。依然会存在较高主从延迟的风险。
6.3 基于组提交的并行复制理解基于组提交的并行复制就需要前面我们提到的 commit 二阶段流程以及组提交。即在 Master 上将 binLog 写入磁盘时,处于 commit 二阶段流程的 binLog Stage 阶段。在该阶段已经完成了锁竞争,所以此时在 Master 上并发写 binLog 的事务,在 Slava 上也可以并发执行。在写入 binLog 时会在行变更记录中写入一个 last_commit 字段,即上一次组提交的提交号。
例如:
初始 last_commit = 0
第一次组提交:
事务一和事务二并发提交,写入两条记录。
事务一: last_commit = 0
事务二: last_commit = 0
第一次组提交写入完成之后 last_commit = 1
第二次组提交:
事务三和事务四并发提交,写入两条记录。
事务三:last_commit = 1
事务四:last_commit = 1
在 Slave 上执行时,事务一和事务二 last_commit 相同可以并发执行。等到事务一和事务二执行完成之后,又可以开始并发执行事务三和事务四。
基于组提交的并发复制还有什么问题吗?
诚然基于组提交解决了在高并发场景下的一些问题,但是还是有一些问题没有解决。例如我们需要从 0 开始构建一个从库,这是时候主库上的一些 SQL 可能并没有并发竞争,可以并发执行。但是因为他是分散在不同的时间段执行的,所以并没有在同一个组提交的事务组里面。导致只能顺序执行。那么有什么办法能够解决这个问题吗?当然有,就是下面提到的基于记录集的并行复制。
6.4 基于记录集的并行复制基于记录集的并行复制在 Slave 执行阶段并没有任何变化,而是在 Master 中 binLog 写入时改变了 last_commit 的实现方式。从图中可以看到,在 Master 写入 BinLog 的时候会有一张 Hash 表,然后有一个 m_writeset_history_start 的值。当我们事务写入一个变更时,会依据当前事务所涉及的索引、主键等数据通过一个算法计算出一个哈希值。然后判断当前哈希值在哈希表中的槽位是否被占用,如果未被占用则使用 m_writeset_history_start 作为 last_commit 的值。
如图:
事务一:更新 id 为 1 的数据,占用槽位 1 ,不存在冲突,last_commit = 1
事务二:更新 id 为 2 的数据,占用槽位 2 ,不存在冲突,last_commit = 1
事务三:更新 id 为 3 的数据,占用槽位 3 ,不存在冲突,last_commit = 1
这样即使三个事务不在一个组提交事务里面,在从库执行时依然可以并发执行。
6.5 我应该用哪一种呢?参见复制性能比较:blogs.oracle.com/mysql/post/…
7. 数据同步时机数据同步的时机分为四类:
-
异步复制
-
全同步复制
-
半同步复制
-
无损复制
- 原理:在异步复制中,master写数据到binlog且sync,slave request binlog后写入relay-log并flush disk
- 优点:复制的性能最好
- 缺点:master挂掉后,slave可能会丢失事务
- 原理:在全同步复制中,master写数据到binlog且sync,所有slave request binlog后写入relay-log并flush disk,并且回放完日志且commit
- 优点:数据不会丢失
- 缺点:会阻塞master session,性能太差,非常依赖网络
- 原理: 在半同步复制中,master写数据到binlog且sync,且commit,然后一直等待ACK。当至少一个slave request bilog后写入到relay-log并flush disk,就返回ack(不需要回放完日志)
- 优点:会有数据丢失风险(低)
- 缺点:会阻塞master session,性能差,非常依赖网络,
- 重点:由于master是在三段提交的最后commit阶段完成后才等待,所以master的其他session是可以看到这个提交事务的,所以这时候master上的数据和slave不一致,master crash后,slave数据丢失
- 原理: 在半同步复制中,master写数据到binlog且sync,然后一直等待ACK. 当至少一个slave request bilog后写入到relay-log并flush disk,就返回ack(不需要回放完日志)
- 优点:数据零丢失(前提是让其一直是lossless replication),性能好
- 缺点:会阻塞master session,非常依赖网络
- 重点:由于master是在三段提交的第二阶段sync binlog完成后才等待, 所以master的其他session是看不见这个提交事务的,所以这时候master上的数据和slave一致,master crash后,slave没有丢失数据
还有一些没有找到分类的功能放在这里:
-
Delayed replication(延迟复制)
-
Global Transaction ID (全局事务标示)
-
Multi source replication(多源复制)
-
MySQL Group Replication
可以自定义从库延迟复制时间,例如设置延迟一天,则可以保留一天前数据库的副本,方便快速回滚数据库状态
8.2 Global Transaction ID (全局事务标示)在出现 GTID 之前,主从复制是依靠 binLog 的偏移来确定复制的进度。但是因为主库和从库 binLog 的偏移可能不同,所以在出现主从切换的时候很难确定当前复制进度。有了 GTID 就能全局唯一标示事务,方便切换。
GTID格式:
GTID = source_id:transaction_id(transaction_id 不可能为0,是一个正整数)
GTID 的生成和生命周期:
-
一个事务在 Master 上执行,生成一个 GTID,写入Master。
-
Master binary log 传播到其他 Slave 机器上,然后保存在 relay log 中。然后读取 GTID 作为 gtid_next
-
保证 Slave 在之前没有执行过的同样的 GTID,也没有同时执行的 GTID
-
GTID 写入 Slave 的 binary log
一个 Slave 可以从多个 Master 复制数据,解决副本机器成本。
8.4 MySQL Group Replication欢迎分享,转载请注明来源:内存溢出
评论列表(0条)