mysql binlog應用場景與原理深度剖析

本文深入介紹Mysql Binlog的應用場景,以及如何與MQ、elasticsearch、redis等元件的保持資料最終一致。最後透過案例深入分析binlog中幾乎所有event是如何產生的,作用是什麼。

1 基於binlog的主從複製

Mysql 5。0以後,支援透過binary log(二進位制日誌)以支援主從複製。複製允許將來自一個MySQL資料庫伺服器(master) 的資料複製到一個或多個其他MySQL資料庫伺服器(slave),以實現災難恢復、水平擴充套件、統計分析、遠端資料分發等功能。

二進位制日誌中儲存的內容稱之為事件,每一個數據庫更新操作(Insert、Update、Delete,不包括Select)等都對應一個事件。

注意:本文不是講解mysql主從複製,而是講解binlog的應用場景,binlog中包含哪些型別的event,這些event的作用是什麼。你可以理解為,是對主從複製中關於binlog解析的細節進行深度剖析。而講解主從複製主要是為了理解binlog的工作流程。

下面以mysql主從複製為例,講解一個從庫是如何從主庫拉取binlog,並回放其中的event的完整流程。mysql主從複製的流程如下圖所示:

mysql binlog應用場景與原理深度剖析

主要分為3個步驟:

第一步:

master在每次準備提交事務完成資料更新前,將改變記錄到二進位制日誌(binary log)中(這些記錄叫做二進位制日誌事件,binary log event,簡稱event)

第二步:

slave啟動一個I/O執行緒來讀取主庫上binary log中的事件,並記錄到slave自己的中繼日誌(relay log)中。

第三步:

slave還會

起動

一個SQL執行緒,該執行緒從relay log中讀取事件並在備庫執行,從而實現備庫資料的更新。

2 binlog的應用場景

binlog本身就像一個螺絲刀,它能發揮什麼樣的作用,完全取決你怎麼使用。就像你可以使用螺絲刀來修電器,也可以用其來固定傢俱。

2.1 讀寫分離

最典型的場景就是透過Mysql主從之間透過binlog複製來實現橫向擴充套件,來實現讀寫分離。如下圖所示:

mysql binlog應用場景與原理深度剖析

在這種場景下:

有一個主庫Master,所有的更新操作都在master上進行

同時會有多個Slave,每個Slave都連線到Master上,獲取binlog在本地回放,實現資料複製。

在應用層面,需要對執行的sql進行判斷。所有的更新操作都透過Master(Insert、Update、Delete等),而查詢操作(Select等)都在Slave上進行。由於存在多個slave,所以我們可以在slave之間做負載均衡。通常業務都會藉助一些資料庫中介軟體,如tddl、sharding-jdbc等來完成讀寫分離功能。

因為工作性質的原因,筆者見過最多的一個業務,一個master,後面掛了20多個slave。筆者之前寫過一篇關於資料庫中介軟體實現原理的文章,感興趣的讀者可以參考:資料庫中介軟體詳解

2.2 資料恢復

一些同學可能有誤刪除資料庫記錄的經歷,或者因為誤操作導致資料庫存在大量髒資料的情況。例如筆者,曾經因為誤操作汙染了業務方几十萬資料記錄。

如何將髒資料恢復成原來的樣子?如果恢復已經被刪除的記錄?

這些都可以透過反解binlog來完成,筆者也是透過這個手段,來恢復業務方的記錄。

2.3 資料最終一致性

在實際開發中,我們經常會遇到一些需求,在資料庫操作成功後,需要進行一些其他操作,如:傳送一條訊息到MQ中、更新快取或者更新搜尋引擎中的索引等。

如何保證資料庫操作與這些行為的一致性,就成為一個難題

。以資料庫與redis快取的一致性為例:操作資料庫成功了,可能會更新redis失敗;反之亦然。很難保證二者的完全一致。

遇到這種看似無解的問題,最好的辦法是換一種思路去解決它:

不要同時去更新資料庫和其他元件,只是簡單的更新資料庫即可。

如果資料庫操作成功,必然會產生binlog。之後,我們透過一個元件,來模擬的mysql的slave,拉取並解析binlog中的資訊。

透過解析binlog的資訊,去非同步的更新快取、索引或者傳送MQ訊息,保證資料庫與其他元件中資料的最終一致。

在這裡,我們將模擬slave的元件,統一稱之為

binlog同步元件

。你並不需要自己編寫這樣的一個元件,已經有很多開源的實現,例如linkedin的databus,阿里巴巴的canal,美團點評的puma等。

當我們透過binlog同步元件完成資料一致性時,此時架構可能如下圖所示:

mysql binlog應用場景與原理深度剖析

增量索引

通常索引分為全量索引和增量索引。對於增量索引的部分,可以透過監聽binlog變化,根據binlog中包含的資訊,轉換成es語法,進行實時索引更新。當然,你可能並沒有使用es,而是solr,這裡只是以es舉例。

可靠訊息

可靠訊息是指的是:保證本地事務與傳送訊息到MQ行為的一致性。一些業務使用

本地事務表

或者

獨立訊息服務

,來保證二者的最終一致。Apache RocketMQ在4。3版本開源了

事務訊息

,也是用於完成此功能。事實上,這兩種方案,都有一定侵入性,對業務不透明。透過訂閱binlog來發送可靠訊息,則是一種解耦、無侵入的方案。關於可靠訊息,筆者最近寫了一篇文章, 感興趣的讀者可以參考:可靠訊息一致性的奇淫技巧。

快取一致性

業務經常遇到的一個問題是,如何保證資料庫中記錄和快取中資料的一致性。不妨換一種思路,只更新資料庫,資料庫更新成功後,透過拉取binlog來非同步的更新快取(通常是刪除,讓業務回源到資料庫)。如果資料庫更新失敗,沒有對應binlog,那麼也不會去更新快取,從而實現最終一致性。

可以看到,binlog是一把利器,可以保證資料庫與與其他任何元件(es、mq、redis等)的最終一致。這是一種優雅的、通用的、無業務入侵的、徹底的解決方案。我們沒有必要再單獨的研究某一種其他元件如何與資料庫保持最終一致,可以透過binlog來實現統一的解決方案。

在實際開發中,你可以簡單的像上圖那樣,每個應用場景都模擬一個slave,各自連線到Mysql上去拉取binlog,master會給每個連線上來的slave一份完整的binlog複製,業務拿到各自的binlog之後進行消費,彼此之間互不影響。但是這樣,有一些弊端,多個slave會給master帶來一些額外管理上的開銷,網絡卡流量也將翻倍的增長。

我們可以進行一些最佳化,之所以不同場景模擬多個slave來連線master獲取同一份binlog,本質上要滿足的是:一份binlog資料,同時提供給多個不同業務場景使用,彼此之間互不影響。

顯然,訊息中介軟體是一個很好的解決方案。現在很多主流的訊息中介軟體,都支援

consumer group

的概念,如kafka、rocketmq等。同一個topic中的資料,可以由多個不同consumer group來消費,且不同的consumer group之間是相互隔離的,例如:當前消費到的位置(offset)。

因此,我們完全可以將binlog,統一都發送到MQ中,不同的應用場景使用不同的consumer group來消費,彼此之間互不影響。此時架構如下圖所示:

mysql binlog應用場景與原理深度剖析

透過這樣方式,我們巧妙的達到了一份資料多個應用場景來使用。一般,一個Mysql例項中可能會建立多個庫(Database),通常我們會將一個庫的binlog放到一個對應的MQ中的Topic中。

當將binlog傳送到MQ中後,我們就可以利用MQ的一些高階特性了。例如binlog傳送到MQ過快,消費方來不及消費,可以利用MQ的訊息堆積能力進行流量削峰。還可以利用MQ的訊息回溯功能,例如一個業務需要消費歷史的binlog,此時MQ中如果還有儲存,那麼就可以直接進行回溯。

當然,有一些binlog同步元件可能實現了類似於MQ的功能,此時你就無序再單獨的使用MQ。

2.4 異地多活

一個更大的應用場景,異地多活場景下,跨資料中心之間的資料同步。這種場景的下,多個數據中心都需要寫入資料,並且往對方同步。以下是一個簡化的示意圖:

mysql binlog應用場景與原理深度剖析

這裡有一些特殊的問題需要處理。典型的包括:

資料衝突:

雙方同時插入了一個相同主鍵的值,那麼往對方同步時,就會出現主鍵衝突的錯誤。

資料迴環:

一個庫A中插入的資料,透過binlog同步到另外一個庫B中,依然會產生binlog。此時庫B的資料再次同步回庫A,如此反覆,就形成了一個死迴圈。

如何解決資料衝突、資料迴環,就變成了binlog同步元件要解決的問題。同樣,業界也有了成熟的實現,比較知名的有阿里開源的otter,以及摩拜(已經屬於美團)的DRC等。

筆者之前寫過一篇文章,介紹如何在多機房進行資料同步,感興趣的讀者可以參考以下文章:異地多活場景下的資料同步之道

2.5 小結

如前所屬,binlog的作用如此強大。因此,你可能想知道binlog檔案中到底包含了哪些內容,為什麼具有如此的魔力?在進行一些資料庫操作時,例如:Insert、Update、Delete等,到底會對binlog產生什麼樣的影響?這正是本文要下來要講解的內容。

3 Binlog事件詳解

Mysql已經經歷了多個版本的釋出,最新已經到8。x,然而目前企業中主流使用的還是Mysql 5。6或5。7。不同版本的Mysql中,binlog的格式和事件型別可能會有些細微的變化,不過暫時我們並不討論這些細節。

總的來說,binlog檔案中儲存的內容稱之為二進位制事件,簡稱事件。我們的每一個數據庫更新操作(Insert、Update、Delete等),都會對應的一個事件。

從大的方面來說,binlog主要分為2種格式:

Statement模式:

binlog中記錄的就是我們執行的SQL;

Row模式:

binlog記錄的是每一行記錄的每個欄位變化前後得到值。

熟悉主從複製的同學,應該知道,還有第三種模式

Mixed

(即混合模式),從嚴格意義上來說,這並不是一種新的binlog格式,只是結合了Statement和Row兩種模式而已。

當我們選擇不同的binlog模式時,在binlog檔案包含的事件型別也不相同,如:

1)

在Statement模式下,我們就看不到Row模式下獨有的事件型別。

2)

有一些型別的event,必須在我們開啟某些特定配置的情況下,才會出現;

3)

當然也會有一些公共的event型別,在任何模式下都會出現。

Mysql中定義了30多個event型別,這裡並不打算將所有的事件型別提前列出,這樣沒有意義,只會讓讀者茫然不知所措。筆者將會在必要的地方,介紹遇到的每一種event型別的作用。

目前我們先從宏觀的角度對binlog有一個感性的認知。

3.1 多檔案儲存

mysql 將資料庫更新操作對應的event記錄到本地的binlog檔案中,顯然在一個檔案中記錄所有的event是不可能的,過大的檔案會給我們的運維帶來麻煩,如刪除一個大檔案,在I/O排程方面會給我們帶來不可忽視的資源開銷。

因此,目前基本上所有支援本地檔案儲存的元件,如MQ、Mysql等,都會控制一個檔案的大小。在資料量較多的情況下,就分配到多個檔案進行儲存。

在mysql中,我們可以透過“show binary logs”語句,來檢視當前有多少個binlog檔案,以及每個binlog檔案的大小,如下:

mysql binlog應用場景與原理深度剖析

另外,mysql提供了:

max_binlog_size

配置項,用於控制一個binlog檔案的大小,預設是1G

expire_logs_days

配置項,可以控制binlog檔案保留天數,預設是0,也就是永久保留。

在實際生產環境中,一般無法保留所有的歷史binlog。因為一條記錄可能會變更多次,記錄依然是一條,但是對應的binlog事件就會有多個。在資料變更比較頻繁的情況下,就會產生大量的binlog檔案。此時,則無法保留所有的歷史binlog檔案。

在mysql的percona分支上,還提供了

max_binlog_files

配置項,用於設定可以保留的binlog檔案數量,以便我們更精確的控制binlog檔案佔用的磁碟空間。這是一個非常有用的配置,筆者曾經遇到一個庫,大約10分鐘就會產生一個binlog檔案,也就是1G,按照這種增長速度,1天下來產生的binlog檔案,就會佔用大概144G左右的空間,磁碟空間可能很快就會被使用完。透過此配置,我們可以顯示的控制binlog檔案的數量,例如指定50,binlog檔案最多隻會佔用50G左右的磁碟空間。

在更高版本的mysql中,支援按照秒級精度,來控制binlog檔案的保留時間。下面我們將對binlog檔案中的內容進行詳細的講解。

3.2 Binlog管理事件

所謂binlog管理事件,官方稱之為binlog managent events,你可以認為是一些在任何模式下都有可能會出現的事件,不管你的配置binlog_format是Row、Statement還是Mixed。

以下透過

"show binlog events"

語法進行檢視一個空的binlog檔案,也就是隻包含(部分)管理事件,沒有其他資料更新操作對應的事件。如下:

mysql binlog應用場景與原理深度剖析

在當前binlog v4版本中,每個binlog檔案總是以

Format Description Event

作為開始,以

Rotate Event

結束作為結束。如果你使用的是很古老的Mysql版本中,開始事件也有可能是

START EVENT V3,

而結束事件是

Stop Event。

在開始和結束之間,穿插著其他各種事件。

在Event_Type列中,我們看到了三個事件型別:

Format_desc:

也就是我們所說的Format Description Event,是binlog檔案的第一個事件。在Info列,我們可以看到,其標明瞭Mysql Server的版本是5。7。10,Binlog版本是4。

Previous_gtids:

該事件完整名稱為,PREVIOUS_GTIDS_LOG_EVENT。熟悉Mysql 基於GTID複製的同學應該知道,這是表示之前的binlog檔案中,已經執行過的GTID。需要我們開啟GTID選項,這個事件才會有值,在後文中,將會詳細的進行介紹。

Rotate:

Rotate Event是每個binlog檔案的結束事件。在Info列中,我們看到了其指定了下一個binlog檔案的名稱是mysql-bin。000004。

關於

"show binlog events"

語法顯示的每一列的作用說明如下:

Log_name:當前事件所在的binlog檔名稱

Pos:當前事件的開始位置,每個事件都佔用固定的位元組大小,結束位置(End_log_position)減去Pos,就是這個事件佔用的位元組數。細心的讀者可以看到了,第一個事件位置並不是從0開始,而是從4。Mysql透過檔案中的前4個位元組,來判斷這是不是一個binlog檔案。這種方式很常見,很多格式的檔案,如pdf、doc、jpg等,都會通常前幾個特定字元判斷是否是合法檔案。

Event_type:表示事件的型別

Server_id:表示產生這個事件的mysql server_id,透過設定my。cnf中的

server-id

選項進行配置。

End_log_position:下一個事件的開始位置

Info:當前事件的描述資訊

3.3 Statement模式下的事件

mysql5。0及之前的版本只支援基於語句的複製,也稱之為邏輯複製,也就是binary log檔案中,直接記錄的就是資料更新對應的sql。

假設有名為test庫中有一張user表,如下:

mysql binlog應用場景與原理深度剖析

現在,我們往user表中插入一條資料

insert into user(name) values(“tianbowen”);

之後,可以使用“

show binlog events

” 語法檢視binary log中的內容,如下:

mysql binlog應用場景與原理深度剖析

紅色框架中Event,是我們執行上面Insert語句產生的4個Event。下面進行詳細的說明:

(劃重點)首先,需要說明的是,每個事務都是以Query Event作為開始,其INFO列內容為"BEGIN",以Xid Event表示結束,其INFO列內容為COMMIT。即使對於單條更新SQL我們沒有開啟事務,Mysql也會預設的幫我們開啟事務

。因此在上面的紅色框中,儘管我們只是執行了一個INSERT語句,沒有開啟事務,但是Mysql 預設幫我們開啟了事務,所以第一個Event是Query Event,最後一個是Xid Event。

接著,是一個

Intvar Event

,因為我們的Insert語句插入的表中,主鍵是自增的(AUTO_INCREMENT)列,Mysql首先會自增一個值,這就是Intvar Event的作用,這裡我們看到INFO列的值為INSERT_ID=1,也就是說,這次的自增主鍵id為1。需要注意的是,這個事件,只會在Statement模式下出現。

然後,還是一個Query Event,這裡記錄的就是我們插入的SQL。這也體現了Statement模式的作用,就是記錄我們執行的SQL。

Statement模式下還有一些不常用的Event,如

USER_VAR_EVENT

,這是用於記錄使用者設定的變數,僅僅在Statement模式起作用。如:

執行以下SQL:

set @name = ‘tianshouzhi’;insert into user(name) values(@name);

這裡,我們插入sql的時候,透過引用一個變數。此時檢視binlog變化,這裡為了易於觀察,在執行show binlog events時,指定了binlog檔案和from的位置,即只檢視指定binlog檔案中從指定位置開始的event。如下:

mysql binlog應用場景與原理深度剖析

可以看到,依然符合我們所說的,對於這個插入語句,依然預設開啟了事務。主鍵自曾值INSERT_ID=2。

當然,我們也看到了User var這個事件,其記錄了我們的設定的變數值,只不過以16進位制顯示。

3.4 Row模式下的事件

mysql5。1開始支援基於行的複製,這種方式記錄的某條sql影響的所有行記錄

變更前

變更後

的值。Row模式下主要有以下10個事件:

mysql binlog應用場景與原理深度剖析

很直觀的,我們看到了INSERT、DELETE、UPDATE操作都有3個版本(v0、v1、v2),v0和v1已經過時,我們只需要關注V2版本。

此外,還有一個

TABLE_MAP_EVENT

,這個event我們需要特別關注,可以理解其作用就是記錄了INSERT、DELETE、UPDATE操作的表結構。

下面,我們透過案例演示,ROW模式是如何記錄變更前後記錄的值,而不是記錄SQL。這裡只演示UPDATE,INSERT和DELETE也是類似。

在前面的操作步驟中,我們已經插入了2條記錄,如下:

mysql binlog應用場景與原理深度剖析

現在需要從Statement模式切換到Row模式,重啟Mysql之後,執行以下SQL更新這兩條記錄:

update user set name=‘wangxiaoxiao’;

在binary log中,會把這2條記錄變更前後的值都記錄下來,以下是一個邏輯示意圖:

mysql binlog應用場景與原理深度剖析

該邏輯示意圖顯示了,在預設情況下,受到影響的記錄行,每個欄位變更前的和變更後的值,都會被記錄下來,即使這個欄位的值沒有發生變化

接著,我們還是透過“show binlog events”語法來驗證:

mysql binlog應用場景與原理深度剖析

首先我們可以看到的是,在Row模式下,單條SQL依然會預設開啟事務,透過Query Event(值為BEGIN)開始,以Xid Event結束。

接著,我們看到了一個Table_map 事件,就是前面提到的TABLE_MAP_EVENT,在INFO列,我們可以看到其記錄table_id為108,操作的是test庫中user表。

最後,是一個

Update_rows

事件,然而其INFO,並沒有像Statement模式那樣,顯示一條SQL,我們無法直接看到其變更前後的值是什麼。

由於儲存的都是二進位制內容,直接vim無法檢視,我們需要藉助另外一個工具

mysqlbinlog

來檢視其內容。如下:

mysql binlog應用場景與原理深度剖析

截圖中顯示了2個event,第一個紅色框就是Table_map事件,第二個是Update_rows事件。

在第二個紅色框架中,顯示了兩個Update sql,這是隻是mysqlbinlog工具為了方便我們檢視,反解成SQL而已。我們看到了WHERE以及SET子句中,並沒有直接列出欄位名,而是以

@1

@2

這樣的表示欄位位於資料庫表中的順序。

事實上,這裡顯示的內容,WHERE部分就是每個欄位修改前的值,而SET部分,則是每個欄位修改後的值,

也就是變更前後的值都會記錄。

這裡我們思考以下mysqlbinlog工具的工作原理,其可以將二進位制資料反解成SQL進行展示。那麼,

如果我們可以自己解析binlog,就可以做資料恢復,這並非是什麼難事

。例如使用者誤刪除的資料,執行的是DETELE語句,由於Row模式下會記錄變更之前的欄位的值,我們可以將其反解成一個INSERT語句,重新插入,從而實現資料恢復。

3.4.1 binlog_row_image引數

我們經常會看到一些Row模式和Statement模式的比較。ROW模式下,即使我們只更新了一條記錄的其中某個欄位,也會記錄每個欄位變更前後的值,binlog日誌就會變大,帶來磁碟IO上的開銷,以及網路開銷。

事實上,這個行為可以透過

binlog_row_image

控制其有3個值,預設為FULL:

FULL : 記錄列的所有修改,即使欄位沒有發生變更也會記錄。

MINIMAL :只記錄修改的列。

NOBLOB :如果是text型別或clob欄位,不記錄這些日誌。

mysql binlog應用場景與原理深度剖析

我們可以將其修改為MINIMAL,則可以只記錄修改的列的值。

3.4.2 binlog_rows_query_log_events引數

在Statement模式下,直接記錄SQL比較直觀,事實上,在Row模式下,也可以記錄。mysql提供了一個

binlog_rows_query_log_events

引數,預設為值為FALSE,如果為true的情況下,會透過

Rows Query Event

來記錄SQL。

mysql binlog應用場景與原理深度剖析

可以在my。cnf中新增以下配置,來開啟row模式下的原始sql記錄(需要重啟):

binlog-rows-query-log_events=1

之後,再插入資料資料時

insert into user(name) values(“maoxinyi”);

在binlog檔案中,我們將看到

Rows Query Event

mysql binlog應用場景與原理深度剖析

3.5 GTID相關事件

從MySQL 5。6開始支援GTID複製。要開啟GTID,修改my。cnf檔案,新增以下配置

gtid-mode=onenforce-gtid-consistency=true

在這種情況下,每當我們執行一個事務之前,都會記錄一個

GTID Event

insert into user(“name”) values(“zhuyihan”);

此時binlog內容如下:

mysql binlog應用場景與原理深度剖析

而當我們切換到下一個binlog檔案時,會記錄之前的已經執行過的GTID。這裡我們透過執行以下sql手工切換到一個新的binlog檔案。

mysql> flush logs;Query OK, 0 rows affected (0。00 sec)

之後在新的binlog檔案中,我們看到之前執行過的GTID在下一個檔案中出現了。

mysql binlog應用場景與原理深度剖析

本文不是專門講解GTID的文章,感興趣的讀者,可以自行檢視相關資料。

作者:田守枝