在前面的文章中,我不止一次地和你提到了binlog,你們知道binlog能夠用來歸檔,也能夠用來作主備同步,但它的內容是什麼樣的呢?爲何備庫執行了binlog就能夠跟主庫保持一致了呢?今天我就正式地和你介紹一下它。mysql
絕不誇張地說,MySQL可以成爲現下最流行的開源數據庫,binlog功不可沒。sql
在最開始,MySQL是以容易學習和方便的高可用架構,被開發人員青睞的。而它的幾乎全部的高可用架構,都直接依賴於binlog。雖然這些高可用架構已經呈現出愈來愈複雜的趨勢,但都是從最基本的一主一備演化過來的。數據庫
今天這篇文章我主要爲你介紹主備的基本原理。理解了背後的設計原理,你也能夠從業務開發的角度,來借鑑這些設計思想。多線程
如圖1所示就是基本的主備切換流程。架構
在狀態1中,客戶端的讀寫都直接訪問節點A,而節點B是A的備庫,只是將A的更新都同步過來,到本地執行。這樣能夠保持節點B和A的數據是相同的。函數
當須要切換的時候,就切成狀態2。這時候客戶端讀寫訪問的都是節點B,而節點A是B的備庫。工具
在狀態1中,雖然節點B沒有被直接訪問,可是我依然建議你把節點B(也就是備庫)設置成只讀(readonly)模式。這樣作,有如下幾個考慮:學習
有時候一些運營類的查詢語句會被放到備庫上去查,設置爲只讀能夠防止誤操做;spa
防止切換邏輯有bug,好比切換過程當中出現雙寫,形成主備不一致;線程
能夠用readonly狀態,來判斷節點的角色。
你可能會問,我把備庫設置成只讀了,還怎麼跟主庫保持同步更新呢?
這個問題,你不用擔憂。由於readonly設置對超級(super)權限用戶是無效的,而用於同步更新的線程,就擁有超級權限。
接下來,咱們再看看節點A到B這條線的內部流程是什麼樣的。圖2中畫出的就是一個update語句在節點A執行,而後同步到節點B的完整流程圖。
圖2中,包含了我在上一篇文章中講到的binlog和redo log的寫入機制相關的內容,能夠看到:主庫接收到客戶端的更新請求後,執行內部事務的更新邏輯,同時寫binlog。
備庫B跟主庫A之間維持了一個長鏈接。主庫A內部有一個線程,專門用於服務備庫B的這個長鏈接。一個事務日誌同步的完整過程是這樣的:
在備庫B上經過change master命令,設置主庫A的IP、端口、用戶名、密碼,以及要從哪一個位置開始請求binlog,這個位置包含文件名和日誌偏移量。
在備庫B上執行start slave命令,這時候備庫會啓動兩個線程,就是圖中的io_thread和sql_thread。其中io_thread負責與主庫創建鏈接。
主庫A校驗完用戶名、密碼後,開始按照備庫B傳過來的位置,從本地讀取binlog,發給B。
備庫B拿到binlog後,寫到本地文件,稱爲中轉日誌(relay log)。
sql_thread讀取中轉日誌,解析出日誌裏的命令,並執行。
這裏須要說明,後來因爲多線程複製方案的引入,sql_thread演化成爲了多個線程,跟咱們今天要介紹的原理沒有直接關係,暫且不展開。
分析完了這個長鏈接的邏輯,咱們再來看一個問題:binlog裏面究竟是什麼內容,爲何備庫拿過去能夠直接執行。
我在第15篇答疑文章中,和你提到過binlog有兩種格式,一種是statement,一種是row。可能你在其餘資料上還會看到有第三種格式,叫做mixed,其實它就是前兩種格式的混合。
爲了便於描述binlog的這三種格式間的區別,我建立了一個表,並初始化幾行數據。
mysql> CREATE TABLE `t` ( `id` int(11) NOT NULL, `a` int(11) DEFAULT NULL, `t_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `a` (`a`), KEY `t_modified`(`t_modified`) ) ENGINE=InnoDB; insert into t values(1,1,'2018-11-13'); insert into t values(2,2,'2018-11-12'); insert into t values(3,3,'2018-11-11'); insert into t values(4,4,'2018-11-10'); insert into t values(5,5,'2018-11-09');
若是要在表中刪除一行數據的話,咱們來看看這個delete語句的binlog是怎麼記錄的。
注意,下面這個語句包含註釋,若是你用MySQL客戶端來作這個實驗的話,要記得加-c參數,不然客戶端會自動去掉註釋。
mysql> delete from t /*comment*/ where a>=4 and t_modified<='2018-11-10' limit 1;
當binlog_format=statement時,binlog裏面記錄的就是SQL語句的原文。你能夠用
mysql> show binlog events in 'master.000001';
命令看binlog中的內容。
如今,咱們來看一下圖3的輸出結果。
爲了說明statement 和 row格式的區別,咱們來看一下這條delete命令的執行效果圖:
能夠看到,運行這條delete命令產生了一個warning,緣由是當前binlog設置的是statement格式,而且語句中有limit,因此這個命令多是unsafe的。
爲何這麼說呢?這是由於delete 帶limit,極可能會出現主備數據不一致的狀況。好比上面這個例子:
若是delete語句使用的是索引a,那麼會根據索引a找到第一個知足條件的行,也就是說刪除的是a=4這一行;
但若是使用的是索引t_modified,那麼刪除的就是 t_modified='2018-11-09’也就是a=5這一行。
因爲statement格式下,記錄到binlog裏的是語句原文,所以可能會出現這樣一種狀況:在主庫執行這條SQL語句的時候,用的是索引a;而在備庫執行這條SQL語句的時候,卻使用了索引t_modified。所以,MySQL認爲這樣寫是有風險的。
那麼,若是我把binlog的格式改成binlog_format=‘row’, 是否是就沒有這個問題了呢?咱們先來看看這時候binog中的內容吧。
能夠看到,與statement格式的binlog相比,先後的BEGIN和COMMIT是同樣的。可是,row格式的binlog裏沒有了SQL語句的原文,而是替換成了兩個event:Table_map和Delete_rows。
Table_map event,用於說明接下來要操做的表是test庫的表t;
Delete_rows event,用於定義刪除的行爲。
其實,咱們經過圖5是看不到詳細信息的,還須要藉助mysqlbinlog工具,用下面這個命令解析和查看binlog中的內容。由於圖5中的信息顯示,這個事務的binlog是從8900這個位置開始的,因此能夠用start-position參數來指定從這個位置的日誌開始解析。
mysqlbinlog -vv data/master.000001 --start-position=8900;
從這個圖中,咱們能夠看到如下幾個信息:
你能夠看到,當binlog_format使用row格式的時候,binlog裏面記錄了真實刪除行的主鍵id,這樣binlog傳到備庫去的時候,就確定會刪除id=4的行,不會有主備刪除不一樣行的問題。
基於上面的信息,咱們來討論一個問題:爲何會有mixed這種binlog格式的存在場景?推論過程是這樣的:
也就是說,mixed格式能夠利用statment格式的優勢,同時又避免了數據不一致的風險。
所以,若是你的線上MySQL設置的binlog格式是statement的話,那基本上就能夠認爲這是一個不合理的設置。你至少應該把binlog的格式設置爲mixed。
好比咱們這個例子,設置爲mixed後,就會記錄爲row格式;而若是執行的語句去掉limit 1,就會記錄爲statement格式。
固然我要說的是,如今愈來愈多的場景要求把MySQL的binlog格式設置成row。這麼作的理由有不少,我來給你舉一個能夠直接看出來的好處:恢復數據。
接下來,咱們就分別從delete、insert和update這三種SQL語句的角度,來看看數據恢復的問題。
經過圖6你能夠看出來,即便我執行的是delete語句,row格式的binlog也會把被刪掉的行的整行信息保存起來。因此,若是你在執行完一條delete語句之後,發現刪錯數據了,能夠直接把binlog中記錄的delete語句轉成insert,把被錯刪的數據插入回去就能夠恢復了。
若是你是執行錯了insert語句呢?那就更直接了。row格式下,insert語句的binlog裏會記錄全部的字段信息,這些信息能夠用來精肯定位剛剛被插入的那一行。這時,你直接把insert語句轉成delete語句,刪除掉這被誤插入的一行數據就能夠了。
若是執行的是update語句的話,binlog裏面會記錄修改前整行的數據和修改後的整行數據。因此,若是你誤執行了update語句的話,只須要把這個event先後的兩行信息對調一下,再去數據庫裏面執行,就能恢復這個更新操做了。
其實,由delete、insert或者update語句致使的數據操做錯誤,須要恢復到操做以前狀態的狀況,也時有發生。MariaDB的Flashback工具就是基於上面介紹的原理來回滾數據的。
雖然mixed格式的binlog如今已經用得很少了,但這裏我仍是要再借用一下mixed格式來講明一個問題,來看一下這條SQL語句:
mysql> insert into t values(10,10, now());
若是咱們把binlog格式設置爲mixed,你以爲MySQL會把它記錄爲row格式仍是statement格式呢?
先不要着急說結果,咱們一塊兒來看一下這條語句執行的效果。
能夠看到,MySQL用的竟然是statement格式。你必定會奇怪,若是這個binlog過了1分鐘才傳給備庫的話,那主備的數據不就不一致了嗎?
接下來,咱們再用mysqlbinlog工具來看看:
從圖中的結果能夠看到,原來binlog在記錄event的時候,多記了一條命令:SET TIMESTAMP=1546103491。它用 SET TIMESTAMP命令約定了接下來的now()函數的返回時間。
所以,不論這個binlog是1分鐘以後被備庫執行,仍是3天后用來恢復這個庫的備份,這個insert語句插入的行,值都是固定的。也就是說,經過這條SET TIMESTAMP命令,MySQL就確保了主備數據的一致性。
我以前看過有人在重放binlog數據的時候,是這麼作的:用mysqlbinlog解析出日誌,而後把裏面的statement語句直接拷貝出來執行。
你如今知道了,這個方法是有風險的。由於有些語句的執行結果是依賴於上下文命令的,直接執行的結果極可能是錯誤的。
因此,用binlog來恢復數據的標準作法是,用 mysqlbinlog工具解析出來,而後把解析結果整個發給MySQL執行。相似下面的命令:
mysqlbinlog master.000001 --start-position=2738 --stop-position=2973 | mysql -h127.0.0.1 -P13000 -u$user -p$pwd;
這個命令的意思是,將 master.000001 文件裏面從第2738字節到第2973字節中間這段內容解析出來,放到MySQL去執行。
經過上面對MySQL中binlog基本內容的理解,你如今能夠知道,binlog的特性確保了在備庫執行相同的binlog,能夠獲得與主庫相同的狀態。
所以,咱們能夠認爲正常狀況下主備的數據是一致的。也就是說,圖1中A、B兩個節點的內容是一致的。其實,圖1中我畫的是M-S結構,但實際生產上使用比較多的是雙M結構,也就是圖9所示的主備切換流程。
對比圖9和圖1,你能夠發現,雙M結構和M-S結構,其實區別只是多了一條線,即:節點A和B之間老是互爲主備關係。這樣在切換的時候就不用再修改主備關係。
可是,雙M結構還有一個問題須要解決。
業務邏輯在節點A上更新了一條語句,而後再把生成的binlog 發給節點B,節點B執行完這條更新語句後也會生成binlog。(我建議你把參數log_slave_updates設置爲on,表示備庫執行relay log後生成binlog)。
那麼,若是節點A同時是節點B的備庫,至關於又把節點B新生成的binlog拿過來執行了一次,而後節點A和B間,會不斷地循環執行這個更新語句,也就是循環複製了。這個要怎麼解決呢?
從上面的圖6中能夠看到,MySQL在binlog中記錄了這個命令第一次執行時所在實例的server id。所以,咱們能夠用下面的邏輯,來解決兩個節點間的循環複製的問題:
規定兩個庫的server id必須不一樣,若是相同,則它們之間不能設定爲主備關係;
一個備庫接到binlog並在重放的過程當中,生成與原binlog的server id相同的新的binlog;
每一個庫在收到從本身的主庫發過來的日誌後,先判斷server id,若是跟本身的相同,表示這個日誌是本身生成的,就直接丟棄這個日誌。
按照這個邏輯,若是咱們設置了雙M結構,日誌的執行流就會變成這樣:
從節點A更新的事務,binlog裏面記的都是A的server id;
傳到節點B執行一次之後,節點B生成的binlog 的server id也是A的server id;
再傳回給節點A,A判斷到這個server id與本身的相同,就不會再處理這個日誌。因此,死循環在這裏就斷掉了。
今天這篇文章,我給你介紹了MySQL binlog的格式和一些基本機制,是後面我要介紹的讀寫分離等系列文章的背景知識,但願你能夠認真消化理解。
binlog在MySQL的各類高可用方案上扮演了重要角色。今天介紹的能夠說是全部MySQL高可用方案的基礎。在這之上演化出了諸如多節點、半同步、MySQL group replication等相對複雜的方案。
我也跟你介紹了MySQL不一樣格式binlog的優缺點,和設計者的思考。但願你在作系統開發時候,也能借鑑這些設計思想。
最後,我給你留下一個思考題吧。
說到循環複製問題的時候,咱們說MySQL經過判斷server id的方式,斷掉死循環。可是,這個機制其實並不完備,在某些場景下,仍是有可能出現死循環。
你能構造出一個這樣的場景嗎?又應該怎麼解決呢?
你能夠把你的設計和分析寫在評論區,我會在下一篇文章跟你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
上期我留給你的問題是,你在何時會把線上生產庫設置成「非雙1」。我目前知道的場景,有如下這些:
業務高峯期。通常若是有預知的高峯期,DBA會有預案,把主庫設置成「非雙1」。
備庫延遲,爲了讓備庫儘快遇上主庫。@永恆記憶和@Second Sight提到了這個場景。
用備份恢復主庫的副本,應用binlog的過程,這個跟上一種場景相似。
批量導入數據的時候。
通常狀況下,把生產庫改爲「非雙1」配置,是設置innodb_flush_logs_at_trx_commit=二、sync_binlog=1000。