learn.lianglianglee.com/专栏/MySQL实战45讲/27 主库出问题了,从库怎么办?.md.html
2022-08-14 03:40:33 +08:00

489 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>27 主库出问题了,从库怎么办?.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/MySQL实战45讲/00 开篇词 这一次让我们一起来搞懂MySQL.md.html">00 开篇词 这一次让我们一起来搞懂MySQL</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/01 基础架构一条SQL查询语句是如何执行的.md.html">01 基础架构一条SQL查询语句是如何执行的</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/02 日志系统一条SQL更新语句是如何执行的.md.html">02 日志系统一条SQL更新语句是如何执行的</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/03 事务隔离:为什么你改了我还看不见?.md.html">03 事务隔离:为什么你改了我还看不见?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/04 深入浅出索引(上).md.html">04 深入浅出索引(上)</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/05 深入浅出索引(下).md.html">05 深入浅出索引(下)</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/06 全局锁和表锁 :给表加个字段怎么有这么多阻碍?.md.html">06 全局锁和表锁 :给表加个字段怎么有这么多阻碍?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/07 行锁功过:怎么减少行锁对性能的影响?.md.html">07 行锁功过:怎么减少行锁对性能的影响?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/08 事务到底是隔离的还是不隔离的?.md.html">08 事务到底是隔离的还是不隔离的?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/09 普通索引和唯一索引,应该怎么选择?.md.html">09 普通索引和唯一索引,应该怎么选择?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/10 MySQL为什么有时候会选错索引.md.html">10 MySQL为什么有时候会选错索引</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/11 怎么给字符串字段加索引?.md.html">11 怎么给字符串字段加索引?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/12 为什么我的MySQL会“抖”一下.md.html">12 为什么我的MySQL会“抖”一下</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/13 为什么表数据删掉一半,表文件大小不变?.md.html">13 为什么表数据删掉一半,表文件大小不变?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/14 count()这么慢,我该怎么办?.md.html">14 count()这么慢,我该怎么办?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/15 答疑文章(一):日志和索引相关问题.md.html">15 答疑文章(一):日志和索引相关问题</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/16 “order by”是怎么工作的.md.html">16 “order by”是怎么工作的</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/17 如何正确地显示随机消息?.md.html">17 如何正确地显示随机消息?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/18 为什么这些SQL语句逻辑相同性能却差异巨大.md.html">18 为什么这些SQL语句逻辑相同性能却差异巨大</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/19 为什么我只查一行的语句,也执行这么慢?.md.html">19 为什么我只查一行的语句,也执行这么慢?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/20 幻读是什么,幻读有什么问题?.md.html">20 幻读是什么,幻读有什么问题?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/21 为什么我只改一行的语句,锁这么多?.md.html">21 为什么我只改一行的语句,锁这么多?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/22 MySQL有哪些“饮鸩止渴”提高性能的方法.md.html">22 MySQL有哪些“饮鸩止渴”提高性能的方法</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/23 MySQL是怎么保证数据不丢的.md.html">23 MySQL是怎么保证数据不丢的</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/24 MySQL是怎么保证主备一致的.md.html">24 MySQL是怎么保证主备一致的</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/25 MySQL是怎么保证高可用的.md.html">25 MySQL是怎么保证高可用的</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/26 备库为什么会延迟好几个小时?.md.html">26 备库为什么会延迟好几个小时?</a>
</li>
<li>
<a class="current-tab" href="/专栏/MySQL实战45讲/27 主库出问题了,从库怎么办?.md.html">27 主库出问题了,从库怎么办?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/28 读写分离有哪些坑?.md.html">28 读写分离有哪些坑?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/29 如何判断一个数据库是不是出问题了?.md.html">29 如何判断一个数据库是不是出问题了?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/30 答疑文章(二):用动态的观点看加锁.md.html">30 答疑文章(二):用动态的观点看加锁</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/31 误删数据后除了跑路,还能怎么办?.md.html">31 误删数据后除了跑路,还能怎么办?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/32 为什么还有kill不掉的语句.md.html">32 为什么还有kill不掉的语句</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/33 我查这么多数据,会不会把数据库内存打爆?.md.html">33 我查这么多数据,会不会把数据库内存打爆?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/34 到底可不可以使用join.md.html">34 到底可不可以使用join</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/35 join语句怎么优化.md.html">35 join语句怎么优化</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/36 为什么临时表可以重名?.md.html">36 为什么临时表可以重名?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/37 什么时候会使用内部临时表?.md.html">37 什么时候会使用内部临时表?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/38 都说InnoDB好那还要不要使用Memory引擎.md.html">38 都说InnoDB好那还要不要使用Memory引擎</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/39 自增主键为什么不是连续的?.md.html">39 自增主键为什么不是连续的?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/40 insert语句的锁为什么这么多.md.html">40 insert语句的锁为什么这么多</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/41 怎么最快地复制一张表?.md.html">41 怎么最快地复制一张表?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/42 grant之后要跟着flush privileges吗.md.html">42 grant之后要跟着flush privileges吗</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/43 要不要使用分区表?.md.html">43 要不要使用分区表?</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/44 答疑文章(三):说一说这些好问题.md.html">44 答疑文章(三):说一说这些好问题</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/45 自增id用完怎么办.md.html">45 自增id用完怎么办</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/我的MySQL心路历程.md.html">我的MySQL心路历程</a>
</li>
<li>
<a href="/专栏/MySQL实战45讲/结束语 点线网面一起构建MySQL知识网络.md.html">结束语 点线网面一起构建MySQL知识网络</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>27 主库出问题了,从库怎么办?</h1>
<p>在前面的第[24]、[25]和[26]篇文章中,我和你介绍了 MySQL 主备复制的基础结构,但这些都是一主一备的结构。</p>
<p>大多数的互联网应用场景都是读多写少,因此你负责的业务,在发展过程中很可能先会遇到读性能的问题。而在数据库层解决读性能问题,就要涉及到接下来两篇文章要讨论的架构:一主多从。</p>
<p>今天这篇文章,我们就先聊聊一主多从的切换正确性。然后,我们在下一篇文章中再聊聊解决一主多从的查询逻辑正确性的方法。</p>
<p>如图 1 所示,就是一个基本的一主多从结构。</p>
<p><img src="assets/aadb3b956d1ffc13ac46515a7d619e79.png" alt="img" /></p>
<p>图 1 一主多从基本结构</p>
<p>图中,虚线箭头表示的是主备关系,也就是 A 和 A互为主备 从库 B、C、D 指向的是主库 A。一主多从的设置一般用于读写分离主库负责所有的写入和一部分读其他的读请求则由从库分担。</p>
<p>今天我们要讨论的就是,在一主多从架构下,主库故障后的主备切换问题。</p>
<p>如图 2 所示,就是主库发生故障,主备切换后的结果。</p>
<p><img src="assets/0014f97423bd75235a9187f492fb2453.png" alt="img" /></p>
<p>图 2 一主多从基本结构 -- 主备切换</p>
<p>相比于一主一备的切换流程一主多从结构在切换完成后A会成为新的主库从库 B、C、D 也要改接到 A。正是由于多了从库 B、C、D 重新指向的这个过程,所以主备切换的复杂性也相应增加了。</p>
<p>接下来,我们再一起看看一个切换系统会怎么完成一主多从的主备切换过程。</p>
<h1>基于位点的主备切换</h1>
<p>这里,我们需要先来回顾一个知识点。</p>
<p>当我们把节点 B 设置成节点 A的从库的时候需要执行一条 change master 命令:</p>
<pre><code>CHANGE MASTER TO
MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
MASTER_LOG_FILE=$master_log_name
MASTER_LOG_POS=$master_log_pos
</code></pre>
<p>这条命令有这么 6 个参数:</p>
<ul>
<li>MASTER_HOST、MASTER_PORT、MASTER_USER 和 MASTER_PASSWORD 四个参数,分别代表了主库 A的 IP、端口、用户名和密码。</li>
<li>最后两个参数 MASTER_LOG_FILE 和 MASTER_LOG_POS 表示,要从主库的 master_log_name 文件的 master_log_pos 这个位置的日志继续同步。而这个位置就是我们所说的同步位点,也就是主库对应的文件名和日志偏移量。</li>
</ul>
<p>那么,这里就有一个问题了,节点 B 要设置成 A的从库就要执行 change master 命令,就不可避免地要设置位点的这两个参数,但是这两个参数到底应该怎么设置呢?</p>
<p>原来节点 B 是 A 的从库,本地记录的也是 A 的位点。但是相同的日志A 的位点和 A的位点是不同的。因此从库 B 要切换的时候,就需要先经过“找同步位点”这个逻辑。</p>
<p>这个位点很难精确取到,只能取一个大概位置。为什么这么说呢?</p>
<p>我来和你分析一下看看这个位点一般是怎么获取到的,你就清楚其中不精确的原因了。</p>
<p>考虑到切换过程中不能丢数据,所以我们找位点的时候,总是要找一个“稍微往前”的,然后再通过判断跳过那些在从库 B 上已经执行过的事务。</p>
<p>一种取同步位点的方法是这样的:</p>
<ol>
<li>等待新主库 A把中转日志relay log全部同步完成</li>
<li>在 A上执行 show master status 命令,得到当前 A上最新的 File 和 Position</li>
<li>取原主库 A 故障的时刻 T</li>
<li>用 mysqlbinlog 工具解析 A的 File得到 T 时刻的位点。</li>
</ol>
<pre><code>mysqlbinlog File --stop-datetime=T --start-datetime=T
</code></pre>
<p><img src="assets/3471dfe4aebcccfaec0523a08cdd0ddd.png" alt="img" /></p>
<p>图 3 mysqlbinlog 部分输出结果</p>
<p>图中end_log_pos 后面的值“123”表示的就是 A这个实例在 T 时刻写入新的 binlog 的位置。然后,我们就可以把 123 这个值作为 $master_log_pos ,用在节点 B 的 change master 命令里。</p>
<p>当然这个值并不精确。为什么呢?</p>
<p>你可以设想有这么一种情况,假设在 T 这个时刻,主库 A 已经执行完成了一个 insert 语句插入了一行数据 R并且已经将 binlog 传给了 A和 B然后在传完的瞬间主库 A 的主机就掉电了。</p>
<p>那么,这时候系统的状态是这样的:</p>
<ol>
<li>在从库 B 上,由于同步了 binlog R 这一行已经存在;</li>
<li>在新主库 A R 这一行也已经存在,日志是写在 123 这个位置之后的;</li>
<li>我们在从库 B 上执行 change master 命令,指向 A的 File 文件的 123 位置,就会把插入 R 这一行数据的 binlog 又同步到从库 B 去执行。</li>
</ol>
<p>这时候,从库 B 的同步线程就会报告 Duplicate entry id_of_R for key PRIMARY 错误,提示出现了主键冲突,然后停止同步。</p>
<p>所以,<strong>通常情况下,我们在切换任务的时候,要先主动跳过这些错误,有两种常用的方法。</strong></p>
<p><strong>一种做法是</strong>,主动跳过一个事务。跳过命令的写法是:</p>
<pre><code>set global sql_slave_skip_counter=1;
start slave;
</code></pre>
<p>因为切换过程中,可能会不止重复执行一个事务,所以我们需要在从库 B 刚开始接到新主库 A持续观察每次碰到这些错误就停下来执行一次跳过命令直到不再出现停下来的情况以此来跳过可能涉及的所有事务。</p>
<p>**另外一种方式是,**通过设置 slave_skip_errors 参数,直接设置跳过指定的错误。</p>
<p>在执行主备切换时,有这么两类错误,是经常会遇到的:</p>
<ul>
<li>1062 错误是插入数据时唯一键冲突;</li>
<li>1032 错误是删除数据时找不到行。</li>
</ul>
<p>因此,我们可以把 slave_skip_errors 设置为 “1032,1062”这样中间碰到这两个错误时就直接跳过。</p>
<p>这里需要注意的是,这种直接跳过指定错误的方法,针对的是主备切换时,由于找不到精确的同步位点,所以只能采用这种方法来创建从库和新主库的主备关系。</p>
<p>这个背景是,我们很清楚在主备切换过程中,直接跳过 1032 和 1062 这两类错误是无损的,所以才可以这么设置 slave_skip_errors 参数。等到主备间的同步关系建立完成,并稳定执行一段时间之后,我们还需要把这个参数设置为空,以免之后真的出现了主从数据不一致,也跳过了。</p>
<h1>GTID</h1>
<p>通过 sql_slave_skip_counter 跳过事务和通过 slave_skip_errors 忽略错误的方法,虽然都最终可以建立从库 B 和新主库 A的主备关系但这两种操作都很复杂而且容易出错。所以MySQL 5.6 版本引入了 GTID彻底解决了这个困难。</p>
<p>那么GTID 到底是什么意思,又是如何解决找同步位点这个问题呢?现在,我就和你简单介绍一下。</p>
<p>GTID 的全称是 Global Transaction Identifier也就是全局事务 ID是一个事务在提交的时候生成的是这个事务的唯一标识。它由两部分组成格式是</p>
<pre><code>GTID=server_uuid:gno
</code></pre>
<p>其中:</p>
<ul>
<li>server_uuid 是一个实例第一次启动时自动生成的,是一个全局唯一的值;</li>
<li>gno 是一个整数,初始值是 1每次提交事务的时候分配给这个事务并加 1。</li>
</ul>
<p>这里我需要和你说明一下,在 MySQL 的官方文档里GTID 格式是这么定义的:</p>
<pre><code>GTID=source_id:transaction_id
</code></pre>
<p>这里的 source_id 就是 server_uuid而后面的这个 transaction_id我觉得容易造成误导所以我改成了 gno。为什么说使用 transaction_id 容易造成误解呢?</p>
<p>因为,在 MySQL 里面我们说 transaction_id 就是指事务 id事务 id 是在事务执行过程中分配的,如果这个事务回滚了,事务 id 也会递增,而 gno 是在事务提交的时候才会分配。</p>
<p>从效果上看GTID 往往是连续的,因此我们用 gno 来表示更容易理解。</p>
<p>GTID 模式的启动也很简单,我们只需要在启动一个 MySQL 实例的时候,加上参数 gtid_mode=on 和 enforce_gtid_consistency=on 就可以了。</p>
<p>在 GTID 模式下,每个事务都会跟一个 GTID 一一对应。这个 GTID 有两种生成方式,而使用哪种方式取决于 session 变量 gtid_next 的值。</p>
<ol>
<li>如果 gtid_next=automatic代表使用默认值。这时MySQL 就会把 server_uuid:gno 分配给这个事务。
a. 记录 binlog 的时候,先记录一行 SET @@SESSION.GTID_NEXT=server_uuid:gno;
b. 把这个 GTID 加入本实例的 GTID 集合。</li>
<li>如果 gtid_next 是一个指定的 GTID 的值,比如通过 set gtid_next='current_gtid指定为 current_gtid那么就有两种可能
a. 如果 current_gtid 已经存在于实例的 GTID 集合中,接下来执行的这个事务会直接被系统忽略;
b. 如果 current_gtid 没有存在于实例的 GTID 集合中,就将这个 current_gtid 分配给接下来要执行的事务,也就是说系统不需要给这个事务生成新的 GTID因此 gno 也不用加 1。</li>
</ol>
<p>注意,一个 current_gtid 只能给一个事务使用。这个事务提交后,如果要执行下一个事务,就要执行 set 命令,把 gtid_next 设置成另外一个 gtid 或者 automatic。</p>
<p>这样,每个 MySQL 实例都维护了一个 GTID 集合,用来对应“这个实例执行过的所有事务”。</p>
<p>这样看上去不太容易理解,接下来我就用一个简单的例子,来和你说明 GTID 的基本用法。</p>
<p>我们在实例 X 中创建一个表 t。</p>
<pre><code>CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t values(1,1);
</code></pre>
<p><img src="assets/28a5cab0079fb12fd5abecd92b3324c2.png" alt="img" /></p>
<p>图 4 初始化数据的 binlog</p>
<p>可以看到,事务的 BEGIN 之前有一条 SET @@SESSION.GTID_NEXT 命令。这时,如果实例 X 有从库,那么将 CREATE TABLE 和 insert 语句的 binlog 同步过去执行的话,执行事务之前就会先执行这两个 SET 命令, 这样被加入从库的 GTID 集合的,就是图中的这两个 GTID。</p>
<p>假设,现在这个实例 X 是另外一个实例 Y 的从库,并且此时在实例 Y 上执行了下面这条插入语句:</p>
<pre><code>insert into t values(1,1);
</code></pre>
<p>并且,这条语句在实例 Y 上的 GTID 是 “aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10”。</p>
<p>那么,实例 X 作为 Y 的从库,就要同步这个事务过来执行,显然会出现主键冲突,导致实例 X 的同步线程停止。这时,我们应该怎么处理呢?</p>
<p>处理方法就是,你可以执行下面的这个语句序列:</p>
<pre><code>set gtid_next='aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10';
begin;
commit;
set gtid_next=automatic;
start slave;
</code></pre>
<p>其中,前三条语句的作用,是通过提交一个空事务,把这个 GTID 加到实例 X 的 GTID 集合中。如图 5 所示,就是执行完这个空事务之后的 show master status 的结果。</p>
<p><img src="assets/c8d3299ece7d583a3ecd1557851ed157.png" alt="img" /></p>
<p>图 5 show master status 结果</p>
<p>可以看到实例 X 的 Executed_Gtid_set 里面,已经加入了这个 GTID。</p>
<p>这样,我再执行 start slave 命令让同步线程执行起来的时候,虽然实例 X 上还是会继续执行实例 Y 传过来的事务但是由于“aaaaaaaa-cccc-dddd-eeee-ffffffffffff:10”已经存在于实例 X 的 GTID 集合中了,所以实例 X 就会直接跳过这个事务,也就不会再出现主键冲突的错误。</p>
<p>在上面的这个语句序列中start slave 命令之前还有一句 set gtid_next=automatic。这句话的作用是“恢复 GTID 的默认分配行为”,也就是说如果之后有新的事务再执行,就还是按照原来的分配方式,继续分配 gno=3。</p>
<h1>基于 GTID 的主备切换</h1>
<p>现在,我们已经理解 GTID 的概念,再一起来看看基于 GTID 的主备复制的用法。</p>
<p>在 GTID 模式下,备库 B 要设置为新主库 A的从库的语法如下</p>
<pre><code>CHANGE MASTER TO
MASTER_HOST=$host_name
MASTER_PORT=$port
MASTER_USER=$user_name
MASTER_PASSWORD=$password
master_auto_position=1
</code></pre>
<p>其中master_auto_position=1 就表示这个主备关系使用的是 GTID 协议。可以看到,前面让我们头疼不已的 MASTER_LOG_FILE 和 MASTER_LOG_POS 参数,已经不需要指定了。</p>
<p>我们把现在这个时刻,实例 A的 GTID 集合记为 set_a实例 B 的 GTID 集合记为 set_b。接下来我们就看看现在的主备切换逻辑。</p>
<p>我们在实例 B 上执行 start slave 命令,取 binlog 的逻辑是这样的:</p>
<ol>
<li>实例 B 指定主库 A基于主备协议建立连接。</li>
<li>实例 B 把 set_b 发给主库 A</li>
<li>实例 A算出 set_a 与 set_b 的差集,也就是所有存在于 set_a但是不存在于 set_b 的 GTID 的集合,判断 A本地是否包含了这个差集需要的所有 binlog 事务。
a. 如果不包含,表示 A已经把实例 B 需要的 binlog 给删掉了,直接返回错误;
b. 如果确认全部包含A从自己的 binlog 文件里面,找出第一个不在 set_b 的事务,发给 B</li>
<li>之后就从这个事务开始,往后读文件,按顺序取 binlog 发给 B 去执行。</li>
</ol>
<p>其实,这个逻辑里面包含了一个设计思想:在基于 GTID 的主备关系里,系统认为只要建立主备关系,就必须保证主库发给备库的日志是完整的。因此,如果实例 B 需要的日志已经不存在A就拒绝把日志发给 B。</p>
<p>这跟基于位点的主备协议不同。基于位点的协议,是由备库决定的,备库指定哪个位点,主库就发哪个位点,不做日志的完整性判断。</p>
<p>基于上面的介绍,我们再来看看引入 GTID 后,一主多从的切换场景下,主备切换是如何实现的。</p>
<p>由于不需要找位点了,所以从库 B、C、D 只需要分别执行 change master 命令指向实例 A即可。</p>
<p>其实,严谨地说,主备切换不是不需要找位点了,而是找位点这个工作,在实例 A内部就已经自动完成了。但由于这个工作是自动的所以对 HA 系统的开发人员来说,非常友好。</p>
<p>之后这个系统就由新主库 A写入主库 A的自己生成的 binlog 中的 GTID 集合格式是server_uuid_of_A:1-M。</p>
<p>如果之前从库 B 的 GTID 集合格式是 server_uuid_of_A:1-N 那么切换之后 GTID 集合的格式就变成了 server_uuid_of_A:1-N, server_uuid_of_A:1-M。</p>
<p>当然,主库 A之前也是 A 的备库,因此主库 A和从库 B 的 GTID 集合是一样的。这就达到了我们预期。</p>
<h1>GTID 和在线 DDL</h1>
<p>接下来,我再举个例子帮你理解 GTID。</p>
<p>之前在第 22 篇文章[《MySQL 有哪些“饮鸩止渴”提高性能的方法?》])中,我和你提到业务高峰期的慢查询性能问题时,分析到如果是由于索引缺失引起的性能问题,我们可以通过在线加索引来解决。但是,考虑到要避免新增索引对主库性能造成的影响,我们可以先在备库加索引,然后再切换。</p>
<p>当时我说,在双 M 结构下,备库执行的 DDL 语句也会传给主库,为了避免传回后对主库造成影响,要通过 set sql_log_bin=off 关掉 binlog。</p>
<p>评论区有位同学提出了一个问题:这样操作的话,数据库里面是加了索引,但是 binlog 并没有记录下这一个更新,是不是会导致数据和日志不一致?</p>
<p>这个问题提得非常好。当时,我在留言的回复中就引用了 GTID 来说明。今天,我再和你展开说明一下。</p>
<p>假设,这两个互为主备关系的库还是实例 X 和实例 Y且当前主库是 X并且都打开了 GTID 模式。这时的主备切换流程可以变成下面这样:</p>
<ul>
<li>在实例 X 上执行 stop slave。</li>
<li>在实例 Y 上执行 DDL 语句。注意,这里并不需要关闭 binlog。</li>
<li>执行完成后,查出这个 DDL 语句对应的 GTID并记为 server_uuid_of_Y:gno。</li>
<li>到实例 X 上执行以下语句序列:</li>
</ul>
<pre><code>set GTID_NEXT=&quot;server_uuid_of_Y:gno&quot;;
begin;
commit;
set gtid_next=automatic;
start slave;
</code></pre>
<p>这样做的目的在于,既可以让实例 Y 的更新有 binlog 记录,同时也可以确保不会在实例 X 上执行这条更新。</p>
<ul>
<li>接下来,执行完主备切换,然后照着上述流程再执行一遍即可。</li>
</ul>
<h1>小结</h1>
<p>在今天这篇文章中,我先和你介绍了一主多从的主备切换流程。在这个过程中,从库找新主库的位点是一个痛点。由此,我们引出了 MySQL 5.6 版本引入的 GTID 模式,介绍了 GTID 的基本概念和用法。</p>
<p>可以看到,在 GTID 模式下,一主多从切换就非常方便了。</p>
<p>因此,如果你使用的 MySQL 版本支持 GTID 的话,我都建议你尽量使用 GTID 模式来做一主多从的切换。</p>
<p>在下一篇文章中,我们还能看到 GTID 模式在读写分离场景的应用。</p>
<p>最后,又到了我们的思考题时间。</p>
<p>你在 GTID 模式下设置主从关系的时候,从库执行 start slave 命令后,主库发现需要的 binlog 已经被删除掉了,导致主备创建不成功。这种情况下,你觉得可以怎么处理呢?</p>
<p>你可以把你的方法写在留言区,我会在下一篇文章的末尾和你讨论这个问题。感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。</p>
<h1>上期问题时间</h1>
<p>上一篇文章最后我给你留的问题是如果主库都是单线程压力模式在从库追主库的过程中binlog-transaction-dependency-tracking 应该选用什么参数?</p>
<p>这个问题的答案是,应该将这个参数设置为 WRITESET。</p>
<p>由于主库是单线程压力模式,所以每个事务的 commit_id 都不同,那么设置为 COMMIT_ORDER 模式的话,从库也只能单线程执行。</p>
<p>同样地,由于 WRITESET_SESSION 模式要求在备库应用日志的时候,同一个线程的日志必须与主库上执行的先后顺序相同,也会导致主库单线程压力模式下退化成单线程复制。</p>
<p>所以,应该将 binlog-transaction-dependency-tracking 设置为 WRITESET。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/MySQL实战45讲/26 备库为什么会延迟好几个小时?.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/MySQL实战45讲/28 读写分离有哪些坑?.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709972c30a643d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>