深入了解MySQL主從復(fù)制的原理
本文轉(zhuǎn)載自微信公眾號「SH的全棧筆記」,作者SH。轉(zhuǎn)載本文請聯(lián)系SH的全棧筆記公眾號。SH
0. 主從復(fù)制
首先主從復(fù)制是什么?簡單來說是讓一臺MySQL服務(wù)器去復(fù)制另一臺MySQL的數(shù)據(jù),使兩個服務(wù)器的數(shù)據(jù)保持一致。
這種方式與Redis的主從復(fù)制的思路沒有太大的出入。如果你對Redis的主從復(fù)制感興趣可以去看看《Redis的主從復(fù)制》。那既然Redis和MySQL都采用了復(fù)制這種方式,主從復(fù)制所帶來的意義是什么呢?
通過復(fù)制功能,構(gòu)建一個或者多個從庫,可以提高數(shù)據(jù)庫的高可用性、可擴(kuò)展性,同時實(shí)現(xiàn)負(fù)載均衡。當(dāng)主庫發(fā)生故障時,可以快速的切到其某一個從庫,并將該從庫提升為主庫,因?yàn)閿?shù)據(jù)都一樣,所以不會影響系統(tǒng)的運(yùn)行;當(dāng)MySQL服務(wù)器需要扛住更多的讀請求時,可以把讀請求的流量分流到各個從庫上去,寫請求則轉(zhuǎn)發(fā)給主庫,形成讀寫分離的架構(gòu),來提供更好的讀擴(kuò)展和請求的負(fù)載均衡。
讀寫分離的架構(gòu)應(yīng)用的其實(shí)非常廣泛,就比如MySQL,還有Redis,以及我們熟悉的Zookeeper,Zookeeper的Follower收到讀請求不會自己處理,而是會將讀請求轉(zhuǎn)發(fā)給Leader,感興趣的可以自己下來了解一下,這里就不偏題了。
1. 復(fù)制原理
MySQL的主從復(fù)制支持兩種方式:
- 基于行
- 基于語句
基于語句的復(fù)制在MySQL3.23中就已經(jīng)有了,而基于語句的方式則在5.1中才實(shí)現(xiàn)。其本質(zhì)都是基于主庫的binlog來實(shí)現(xiàn)的,主庫記錄binlog,然后從庫將binlog在自己的服務(wù)器上重放,從而保證了主、從的數(shù)據(jù)一致性。
1.1 binlog
MySQL中日志分為兩個維度,一個是MySQL服務(wù)器的,一個是底層存儲引擎的。而上文提到的binlog就是屬于MySQL服務(wù)器的日志,binlog也叫二進(jìn)制日志,記錄了所有對MySQL所做的更改。
基于行、語句的復(fù)制方式跟binlog的存儲方式有關(guān)系。binlog有三種存儲格式,分別是Statement、Row和Mixed。
- Statement 基于語句,只記錄對數(shù)據(jù)做了修改的SQL語句,能夠有效的減少binlog的數(shù)據(jù)量,提高讀取、基于binlog重放的性能
- Row 只記錄被修改的行,所以Row記錄的binlog日志量一般來說會比Statement格式要多?;赗ow的binlog日志非常完整、清晰,記錄了所有數(shù)據(jù)的變動,但是缺點(diǎn)是可能會非常多,例如一條update語句,有可能是所有的數(shù)據(jù)都有修改;再例如alter table之類的,修改了某個字段,同樣的每條記錄都有改動。
- Mixed Statement和Row的結(jié)合,怎么個結(jié)合法呢。例如像update或者alter table之類的語句修改,采用Statement格式。其余的對數(shù)據(jù)的修改例如update和delete采用Row格式進(jìn)行記錄。
為什么會有這么多方式呢?因?yàn)镾tatement只會記錄SQL語句,但是并不能保證所有情況下這些語句在從庫上能夠正確的被重放出來。因?yàn)榭赡茼樞虿粚Α?/p>
MySQL什么時候會記錄binlog呢?是在事務(wù)提交的時候,并不是按照語句的執(zhí)行順序來記錄,當(dāng)記錄完binlog之后,就會通知底層的存儲引擎提交事務(wù),所以有可能因?yàn)檎Z句順序錯誤導(dǎo)致語句出錯。
1.2 查看binlog
這里拿MySQL 5.6舉例子,binlog默認(rèn)是處于關(guān)閉狀態(tài)的。我們可以通過命令show variables like '%log_bin%' 來查看關(guān)于binlog的配置。
默認(rèn)配置
log_bin代表是否開啟了binlog,其默認(rèn)值為OFF。
- log_bin 代表是否開啟了binlog,其默認(rèn)值為OFF
- log_bin_basename binlog存儲文件的完整名稱,會在默認(rèn)的文件名后面添加上遞增的序號,就例如mysql-bin.000001
- log_bin_index binlog索引文件名稱,例如mysql-bin.index
- sql_log_bin 在binlog開啟的時候,可以禁用當(dāng)前session的binlog
你可以在MySQL中通過命令show binary logs查看所有的binlog文件
圖片
查看binlog
知道了有哪些文件之后我們可以來看看binlog文件中的內(nèi)容,可以在MySQL通過show binlog events命令來查看。
show binglog events 查看第一個binlog文件,我們也可以通過in參數(shù)來指定,假設(shè)我們想看的文件名是mysql-bin.000001,那么可以使用命令show binlog events in 'mysql-bin.000001'來查看指定的binlog文件
查看binlog
接下來我們來看看我們在MySQL中的操作所對應(yīng)的binlog內(nèi)容分別是什么。
初始化
我們上面提到過,binlog是由一個一個的event組成的。從MySQL 5.0開始,binlog的第一個event都為Format_desc,位于圖中的Event_type那一列??梢钥吹絻?nèi)容為Server ver;5.6.50-log, Binlog ver: 4,說明當(dāng)前使用的MySQL版本為5.6.50,Binlog的版本是V4。
創(chuàng)建數(shù)據(jù)庫
然后我創(chuàng)建了一個名為student的DB,其Event_type是Query,這個event的內(nèi)容為CREATE DATABASE student DEFAULT CHARACTER SET = utf8mb4,一個建庫語句。
新建表
然后我創(chuàng)建了一個名為student的表,Event_type也是Query,內(nèi)容為use student; CREATE TABLE student (id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT),一個建表語句。
插入數(shù)據(jù)
然后我們執(zhí)行INSERT語句給該表插入兩行數(shù)據(jù),再次查看binlog。
- INSERT INTO `student` (`id`, `name`) VALUES (NULL, '張三');
- INSERT INTO `student` (`id`, `name`) VALUES (NULL, '李四');
image-20210106123550397
可以看到每次INSERT都會開啟一個事務(wù),你可能會疑惑,我們只是簡單的執(zhí)行了INSERT語句,沒有顯示的開啟事務(wù)。那為什么會有事務(wù)產(chǎn)生呢?
這是因?yàn)镸ySQL采用了自動提交(AUTOCOMMIT)的機(jī)制,我使用的InnoDB存儲引擎,是支持事務(wù)的,所有的用戶活動都發(fā)生在事務(wù)中。我們可以通過show variables like '%AUTOCOMMIT%';命令查看,如果結(jié)果是ON則代表是開啟的。
1.3 復(fù)制的核心步驟
我們假設(shè)主庫已經(jīng)開啟了binlog,并正常的記錄binlog。
首先從庫啟動I/O線程,跟主庫建立客戶端連接。
主庫啟動binlog dump線程,讀取主庫上的binlog event發(fā)送給從庫的I/O線程,I/O線程獲取到binlog event之后將其寫入到自己的Relay Log中。
然后從庫啟動SQL線程,將Relay中的數(shù)據(jù)進(jìn)行重放,完成從庫的數(shù)據(jù)更新。
總結(jié)來說,主庫上只會有一個線程,而從庫上則會有兩個線程。
主從復(fù)制流程
1.4 Relay Log
relay log其實(shí)和binlog沒有太大的區(qū)別,在MySQL 4.0 之前是沒有Relay Log這部分的,整個過程中只有兩個線程。但是這樣也帶來一個問題,那就是復(fù)制的過程需要同步的進(jìn)行,很容易被影響,而且效率不高。例如主庫必須要等待從庫讀取完了才能發(fā)送下一個binlog事件。這就有點(diǎn)類似于一個阻塞的信道和非阻塞的信道。
阻塞信道
阻塞信道就跟你在柜臺一樣,你要遞歸柜員一個東西,但是你和柜員之間沒有可以放東西的地方,你就只能一直把文件拿著,直到柜員接手;而非阻塞信道就像你們之間有個地方可以放文件,你就直接放上去就好了,不用等柜員接手。
引入了Relay Log之后,讓原本同步的獲取事件、重放事件解耦了,兩個步驟可以異步的進(jìn)行,Relay Log充當(dāng)了緩沖區(qū)的作用。Relay Log有一個relay-log.info的文件,用于記錄當(dāng)前復(fù)制的進(jìn)度,下一個事件從什么Pos開始寫入,該文件由SQL線程負(fù)責(zé)更新。
1.5 Relay Log核心參數(shù)
接下來讓我們了解一下Relay Log的核心參數(shù)。
- max_relay_log_size 中繼日志的最大size,默認(rèn)值0,如果為0就會取默認(rèn)的size 1G,否則就為設(shè)置的值
- relay_log 定義relay的名稱,默認(rèn)為主機(jī)名+relay-bin,例如像hostname-relay-bin
- relay_log_basename 中繼日志的全路徑,即路徑 + 文件名,例如/path/to/hostname-relay-bin,最大長度為256
- relay_log_index 定義中繼日志的索引文件的全路徑,同樣其最大的長度為256. 其默認(rèn)值為hostname + relay-bin.index,例如/path/to/hostname-relay-bin.index
- relay_log_info_file 定義relay-log.info文件的名稱
- relay_log_info_repository 存放relay log重放的數(shù)據(jù)的方式,可以設(shè)置為FILE和TABLE。FILE代表將中繼日志重放的數(shù)據(jù)記錄在relay-info.log中,TABLE則將其存放在slave_relay_log_info這張表里。
- relay_log_purge 是否自動清空不需要的中繼日志,默認(rèn)值為ON
- relay_log_recovery 當(dāng)從庫宕機(jī)后,如果relay log損壞了導(dǎo)致部分的中繼日志沒有進(jìn)行同步,則自動放棄所有未進(jìn)行重放的中繼日志,并從主庫重新獲取,默認(rèn)值為OFF
- relay_log_space_limit 設(shè)置中繼日志的最大值,防止寫滿磁盤。但是不建議設(shè)置這個值,建議還是給中繼日志需要的空間,0就是不限制,0也是默認(rèn)值
- sync_relay_log 用于控制中繼日志寫入磁盤的變量,假設(shè)值為n,那么在中繼日志每接受n次binlog事件之后就會調(diào)用fdatasync()函數(shù)將中繼日志強(qiáng)制的刷入磁盤;相反,如果值為0,則寫入OS的緩沖區(qū)內(nèi),由OS調(diào)度決定何時將中繼日志刷入磁盤,這樣一來如果在沒有刷入之前報錯了,那么中繼日志就會丟失。默認(rèn)值是10000,也就是每向中繼日志中寫入1w次binlog事件就將中繼日志強(qiáng)制的刷入磁盤。
- sync_relay_log_info 該參數(shù)的影響跟參數(shù)relay_log_info_repository有一定關(guān)系,同時也跟是否使用支持事務(wù)的存儲引擎有關(guān)系。該值默認(rèn)也是10000.
- relay_log_info_repository為FILE,假設(shè)設(shè)置的值為N,那么每N次事務(wù)都會都會調(diào)用fdatasync()強(qiáng)制將relay-log.info刷入磁盤
- relay_log_info_repository為TABLE,如果使用了支持事務(wù)的引擎,則該表每次事務(wù)結(jié)束都會被更新;如果沒有使用事務(wù)引擎則會在寫入N個binlog事件的時候更新該表。
- relay_log_info_repository為FILE,MySQL不會調(diào)用fdatasync(),而是將刷入磁盤的調(diào)度交給OS;
- relay_log_info_repository為TABLE,如果使用了支持事務(wù)的存儲引擎,則每次事務(wù)的時候該表都會被更新;如果沒有使用事務(wù)引擎,則永遠(yuǎn)不會被更新
- 當(dāng)sync_relay_log_info為0時
- 當(dāng)sync_relay_log_info大于0時
2. 復(fù)制模型
平常的開發(fā)中,其實(shí)很少說一上來就直接搞主從架構(gòu)的。費(fèi)時間、費(fèi)錢還引入了額外的復(fù)雜度,最后發(fā)現(xiàn)投入了這么多一個單MySQL服務(wù)器就完全能handle。
這就跟一個產(chǎn)品的架構(gòu)迭代是一樣的,剛剛起步的時候一個單體應(yīng)用足夠了。當(dāng)你的業(yè)務(wù)擴(kuò)展,請求膨脹,單體無法抗住壓力了,就會考慮開始部署多實(shí)例,開始采用微服務(wù)架構(gòu)去做橫向擴(kuò)展、負(fù)載均衡。
2.1 一主多從
當(dāng)然你也可以把它當(dāng)成一主一從。
這是最簡單的模型,特別適合少量寫、大量讀的情況。讀請求被分到了各個從庫上,有效的幫主庫分散了壓力,能夠提升讀并發(fā)。當(dāng)然,你也可以只是把從庫當(dāng)成一個災(zāi)備庫,除了主從復(fù)制之外,沒有其他任何的請求和數(shù)據(jù)傳輸。
甚至你可以把其中一個備庫作為你的預(yù)發(fā)環(huán)境的數(shù)據(jù)庫,當(dāng)然,這說到底還是直接動了生產(chǎn)環(huán)境的數(shù)據(jù)庫,是一種過于理想的用途,因?yàn)檫@還涉及到生產(chǎn)環(huán)境數(shù)據(jù)庫的數(shù)據(jù)敏感性。不是所有人都能夠接觸到的,需要有完善的權(quán)限機(jī)制。
MySQL一主多從
值得注意的是,如果有n個從庫,那么主庫上就會有n個binlog dump線程。如果這個n比較大的話在復(fù)制的時候可能會造成主庫的性能抖動。所以在從庫較多的情況下可以采用級聯(lián)復(fù)制。
2.2 級聯(lián)復(fù)制
級聯(lián)復(fù)制用大白話說就是套娃。
本來從庫B、C、D、E、F、G都是復(fù)制的主庫A,但是現(xiàn)在由于A的壓力比較大,就不這么干了,調(diào)整成了如下的模式。
B、C復(fù)制A
D、E復(fù)制B
F、G復(fù)制C
MySQL級聯(lián)復(fù)制
這就叫級聯(lián)復(fù)制,開啟瘋狂套娃模式。你甚至?xí)X得這種套娃很眼熟,在Redis主從復(fù)制中也可以采用級聯(lián)模式, slave去復(fù)制另一個slave。
級聯(lián)復(fù)制的好處在于很大程度上減輕了主庫的壓力,主庫只需要關(guān)心與其有直接復(fù)制關(guān)系的從庫,剩下的復(fù)制則交給從庫即可。相反,由于是這種層層嵌套的關(guān)系,如果在較上層出現(xiàn)了錯誤,會影響到掛在該服務(wù)器下的所有子庫,這些錯誤的影響效果被放大了。
2.3 主主復(fù)制
顧名思義,就是兩個主庫相互復(fù)制,客戶端可以對任意一臺主庫進(jìn)行寫操作。任何一臺主庫服務(wù)器上的數(shù)據(jù)發(fā)生了變化都會同步到另一臺服務(wù)器上去。有點(diǎn)類似于Eureka Server的雙節(jié)點(diǎn)模式,兩個注冊中心相互注冊。這樣一來,任何一臺掛了都不會對系統(tǒng)產(chǎn)生影響。
而且主主復(fù)制可以打破數(shù)據(jù)庫性能瓶頸,一個很酷的功能——橫向擴(kuò)展。為什么說很酷呢,如果DB能做到橫向擴(kuò)展,那很多被數(shù)據(jù)庫并發(fā)所限制的瓶頸都可以被突破,然而...
但是主主復(fù)制其實(shí)并不可靠,兩邊的數(shù)據(jù)沖突的可能性很大。例如復(fù)制停止了,系統(tǒng)仍然在向兩個主庫中寫入數(shù)據(jù),也就是說一部分?jǐn)?shù)據(jù)在A,另一部分的數(shù)據(jù)在B,但是沒有相互復(fù)制,且數(shù)據(jù)也不同步了。要修復(fù)這部分?jǐn)?shù)據(jù)的難度就會變得相當(dāng)大。
所以我認(rèn)為雙主的更多的意義在于HA,而不是負(fù)載均衡。
2.4 主、被動的主主復(fù)制
同樣還是雙主的結(jié)構(gòu),但是區(qū)別在于其中一臺是只讀的被動服務(wù)器,客戶端不會向該庫進(jìn)行寫操作。
其用途在哪里呢?例如我們要在不中斷服務(wù)的前提下對MySQL進(jìn)行維護(hù)、優(yōu)化,舉個例子——修改表結(jié)構(gòu)。假設(shè)我們有兩個數(shù)據(jù)庫,主庫A和被動主庫B,注意此處的被動主庫是只讀的,我們先停止A對B的復(fù)制,也就是停掉A上的SQL線程。
主主停止復(fù)制
這樣一來,我們之后在B上執(zhí)行的非常耗時、可能需要鎖表的操作就不會立即同步到A上來。因?yàn)榇藭rA正在對外提供服務(wù),所以不能使其收到影響,但是由于采用的是異步的復(fù)制模式,所以Relay Log還是繼續(xù)由I/O線程寫入,只是不去進(jìn)行重放。
然后我們在B上執(zhí)行此次的維護(hù)操作,注意,此時A上面發(fā)生的更新還是會正常的同步到B來。執(zhí)行完后交換讀寫的角色。也就是讓A變成只讀的被動主庫,而B變?yōu)橹鲃又鲙鞂ν馓峁┓?wù)。
重新開啟SQL線程
然后重新開啟SQL線程,A開始去對之前Relay Log中積累的event進(jìn)行重放。雖然A此時可能會阻塞住,但是A已經(jīng)沒有對外提供服務(wù)了,所以沒有問題。
主、被動下的主主模式的好處大家也就清楚了,可以在不停止服務(wù)的情況下去做數(shù)據(jù)庫的結(jié)構(gòu)更新,其次可以在主庫發(fā)生故障的情況下,快速的切換,保證數(shù)據(jù)庫的HA。
3. 復(fù)制方式
上文我們不止一次的提到了復(fù)制是異步的,接下來我們來了解一下MySQL的主從復(fù)制都有哪些方式。
3.1 異步復(fù)制
首先就是異步,這也是MySQL默認(rèn)的方式。在異步復(fù)制下,主庫不會主動的向從庫發(fā)送消息,而是等待從庫的I/O線程建立連接,然后主庫創(chuàng)建binlog dump線程,把binlog event發(fā)送給I/O線程,流程如下圖。
MySQL復(fù)制模式
主庫在執(zhí)行完自己的事務(wù)、記錄完binlog之后就會直接返回,不會與客戶端確認(rèn)任何結(jié)果。然后后續(xù)由binlog dump線程異步的讀取binlog,然后發(fā)送給從庫。處理請求和主從復(fù)制是兩個完全異步化的過程。
3.2 同步復(fù)制
同步模式則是,主庫執(zhí)行一個事務(wù),那么主庫必須等待所有的從庫全部執(zhí)行完事務(wù)返回commit之后才能給客戶端返回成功,
同步復(fù)制
值得注意的是,主庫會直接提交事務(wù),而不是等待所有從庫返回之后再提交。MySQL只是延遲了對客戶端的返回,并沒有延后事務(wù)的提交。
同步模式用腳趾頭想知道性能會大打折扣,它把客戶端的請求和主從復(fù)制耦合在了一起,如果有某個從庫復(fù)制線程執(zhí)行的慢,那么對客戶端的響應(yīng)也會慢很多。
3.3 半同步復(fù)制
半同步相對于同步的區(qū)別在于,同步需要等待所有的從庫commit,而半同步只需要一個從庫commit就可以返回了。如果超過默認(rèn)的時間仍然沒有從庫commit,就會切換為異步模式再提交??蛻舳艘膊粫恢比サ却恕?/p>
MySQL復(fù)制模式
因?yàn)榧词购竺嬷鲙戾礄C(jī)了,也能至少保證有一個從庫節(jié)點(diǎn)是可以用的,此外還減少了同步時的等待時間。
4. 復(fù)制中的數(shù)據(jù)一致性
我們在1.3中討論了復(fù)制的核心步驟,看似很簡單的一個流程,主庫的binlog dump去讀取binlog,然后從庫的I/O線程去讀取、寫入Relay Log,進(jìn)而從庫的SQL線程再讀取Relay Log進(jìn)行重放。
那如果I/O線程復(fù)制到一半自己突然掛掉了呢?又或者復(fù)制到一半主庫宕機(jī)了呢?如果和保證數(shù)據(jù)一致性的呢?
我們上面提到過,有一個relay-log.info的文件,用于記錄當(dāng)前從庫正在復(fù)制的binlog和寫入的Relay Log的Pos,只要這個文件還在,那么當(dāng)從庫意外重啟之后,就會重新讀取文件,從上次復(fù)制的地方開始繼續(xù)復(fù)制。這就跟Redis中的主從復(fù)制類似,雙方要維護(hù)一個offset,通過對比offset,來進(jìn)行psync增量數(shù)據(jù)同步。
但是在MySQL 5.5以及之前,都只能將復(fù)制的進(jìn)度記錄在relog-log.info文件中。換句話說,參數(shù)relay_log_info_repository只支持FILE,可以再回到上面的1.5 Relay Log核心參數(shù)看一下。所以只有在sync_relay_log_info次事務(wù)之后才會把relay-log.info文件刷入磁盤。
如果在刷入磁盤之前從庫掛了,那么重啟之后就會發(fā)現(xiàn)SQL線程實(shí)際執(zhí)行到位置和數(shù)據(jù)庫記錄的不一致,數(shù)據(jù)一致性的問題就這么產(chǎn)生了。
所以在MySQL 5.6時,參數(shù)relay_log_info_repository支持了TABLE,這樣一來我們就可以將復(fù)制的進(jìn)度放在系統(tǒng)的mysql.slave_relay_log_info表里去,并且把更新進(jìn)度、SQL線程執(zhí)行用戶事務(wù)綁定成一個事務(wù)執(zhí)行。即使slave宕機(jī)了,我們也可以通過MySQL內(nèi)建的崩潰恢復(fù)機(jī)制來使實(shí)際執(zhí)行的位置和數(shù)據(jù)庫保存的進(jìn)度恢復(fù)到一致。
其次還有上面提到的半同步復(fù)制,主庫會先提交事務(wù),然后等待從庫的返回,再將結(jié)果返回給客戶端,但是如果在主庫等待的時候,從庫掛了呢?
此時主庫上由于事務(wù)已經(jīng)提交了,但是從庫上卻沒有這個數(shù)據(jù)。所以在MySQL 5.7時引入了無損半同步復(fù)制,增加了參數(shù)rpl_semi_sync_master_wait_point的值,在MySQL 5.7中值默認(rèn)為after_sync,在MySQL 5.6中默認(rèn)值為after_commit。
- after_sync 主庫先不提交事務(wù),等待某一個從庫返回了結(jié)果之后,再提交事務(wù)。這樣一來,如果從庫在沒有任何返回的情況下宕機(jī)了,master這邊也無法提交事務(wù)。主從仍然是一致的
- after_commit 與之前討論的一樣,主庫先提交事務(wù),等待從庫返回結(jié)果再通知客戶端













































