binlog那些事兒(二)
讀完上一篇文章:binlog那些事兒,我們應(yīng)該知道:
?binlog日志用于主從復(fù)制以及數(shù)據(jù)恢復(fù)。
?啟動選項(xiàng)--log-bin[=basename]可以控制MySQL服務(wù)器是否生成binlog,并且控制binlog日志文件所在路徑以及文件名稱。
?為了記錄binlog,MySQL服務(wù)器在文件系統(tǒng)上創(chuàng)建了一系列存儲真實(shí)binlog數(shù)據(jù)的文件(這些文件都以數(shù)字編號),以及binlog索引文件。
?binlog日志文件中記載了數(shù)據(jù)庫發(fā)生更改的若干事件。
?使用SHOW BINLOG EVENTS語句可以查看某個binlog日志文件中存儲的各種事件。
?mysqlbinlog實(shí)用工具可以用文本形式查看某個binlog日志文件所記載各種事件。
掌握了上述內(nèi)容之后,我們可以繼續(xù)展開了。
binlog日志版本
binlog是自MySQL 3.23.14版本開始誕生的,到現(xiàn)在為止,共經(jīng)歷了4個版本:
?v1?v2?v3?v4
其中的v4版本從MySQL 5.0就開始使用,直到今天。
所以本文著重介紹v4版本的binlog格式,其他版本就不關(guān)注了。
binlog日志文件結(jié)構(gòu)概覽
廢話少說,先看一下一個binlog日志文件的基本格式:

從上圖中可以看出:
?每個binlog日志文件的前4個字節(jié)是固定的,即:0xfe626963。
小貼士:
0xfe626963中的0x626963的ascii碼是'bin',0xfe626963也被稱作魔數(shù)(magic number),如果一個文件不以0xfe626963開頭,那這個文件肯定不算是一個binlog日志。很多軟件都會在磁盤文件的某個地方添加一個類似的魔數(shù)來表明該文件是本軟件處理的文件格式,比方說Intel處理器的BIOS會將磁盤上的第一個扇區(qū)加載到內(nèi)存中,這個扇區(qū)的最后兩個字節(jié)必須為魔數(shù)0x55aa,Java的class文件字節(jié)碼的開頭四個字節(jié)為魔數(shù)0xCAFEBABE。
?每個binlog日志文件都是由若干事件構(gòu)成的。
?每個binlog日志文件所存儲的第1個事件都是一個稱作格式描述事件(format description event)的特殊事件,我們稍后詳細(xì)嘮叨一下這個特殊事件。
其中,每個事件都可以被分成event header和event data兩個部分,我們以上圖的事件2為例展示一下:

其中:
?event header部分描述了該事件是什么類型、什么時候生成的、由哪個服務(wù)器生成的等信息。
?event data部分描述了該事件所特有的一些信息,比方說在插入一條記錄時,需要將這條記錄的內(nèi)容記錄在event data中。
event header結(jié)構(gòu)
每個事件都會包括一個通用的event header,我們看一下這個event header的結(jié)構(gòu):

event header中包含了如下幾部分內(nèi)容:
?timestamp(4字節(jié)):產(chǎn)生該事件時的時間戳。?typecode(1字節(jié)):該事件的類型,事件的類型在枚舉結(jié)構(gòu)Log_event_type中列舉出來(上一篇文章或者本文后續(xù)部分都有提到這個結(jié)構(gòu))。比方說格式描述事件的typecode就是15。?server_id(4字節(jié)):產(chǎn)生該事件的主機(jī)的server_id。?event_length(4字節(jié)):該事件總大小(包括event header + event data)。?next_position(4字節(jié)):下一個事件的位置。?flags(2字節(jié)):該事件的一些附加屬性(稱作flags)。?extra_headers(不確定大小):目前這個字段尚未使用(也就是占用的大小為0),可能在將來的版本中使用,大家目前忽略這個字段就好了。
event data
event data由2部分組成,分別是:
?固定大小部分?可變大小部分

不過并不是所有事件都有這兩個部分,有的事件可以僅有其中的一個部分或者兩個部分都沒有。
上一篇文章中嘮叨過,MySQL中支持幾十種binlog事件,不同事件具有不同的event data部分。
我們先看一下binlog的事件類型有多少(上一篇文章中引用MySQL internal文檔中的內(nèi)容,有點(diǎn)陳舊,所以這次直接從MySQL5.7.22的源碼中獲取Log_event_type結(jié)構(gòu)):
enum Log_event_type{/**Every time you update this enum (when you add a type), you have tofix Format_description_event::Format_description_event().*/UNKNOWN_EVENT= 0,START_EVENT_V3= 1,QUERY_EVENT= 2,STOP_EVENT= 3,ROTATE_EVENT= 4,INTVAR_EVENT= 5,LOAD_EVENT= 6,SLAVE_EVENT= 7,CREATE_FILE_EVENT= 8,APPEND_BLOCK_EVENT= 9,EXEC_LOAD_EVENT= 10,DELETE_FILE_EVENT= 11,/**NEW_LOAD_EVENT is like LOAD_EVENT except that it has a longerallowing multibyte TERMINATED BY etc; both types share thesame class (Load_event)*/NEW_LOAD_EVENT= 12,RAND_EVENT= 13,USER_VAR_EVENT= 14,FORMAT_DESCRIPTION_EVENT= 15,XID_EVENT= 16,BEGIN_LOAD_QUERY_EVENT= 17,EXECUTE_LOAD_QUERY_EVENT= 18,TABLE_MAP_EVENT = 19,/**The PRE_GA event numbers were used for 5.1.0 to 5.1.15 and aretherefore obsolete.*/PRE_GA_WRITE_ROWS_EVENT = 20,PRE_GA_UPDATE_ROWS_EVENT = 21,PRE_GA_DELETE_ROWS_EVENT = 22,/**The V1 event numbers are used from 5.1.16 until mysql-trunk-xx*/WRITE_ROWS_EVENT_V1 = 23,UPDATE_ROWS_EVENT_V1 = 24,DELETE_ROWS_EVENT_V1 = 25,/**Something out of the ordinary happened on the master*/INCIDENT_EVENT= 26,/**Heartbeat event to be send by master at its idle timeto ensure master's online status to slave*/HEARTBEAT_LOG_EVENT= 27,/**In some situations, it is necessary to send over ignorabledata to the slave: data that a slave can handle in case thereis code for handling it, but which can be ignored if it is notrecognized.*/IGNORABLE_LOG_EVENT= 28,ROWS_QUERY_LOG_EVENT= 29,Version 2 of the Row events */WRITE_ROWS_EVENT = 30,UPDATE_ROWS_EVENT = 31,DELETE_ROWS_EVENT = 32,GTID_LOG_EVENT= 33,ANONYMOUS_GTID_LOG_EVENT= 34,PREVIOUS_GTIDS_LOG_EVENT= 35,TRANSACTION_CONTEXT_EVENT= 36,VIEW_CHANGE_EVENT= 37,Prepared XA transaction terminal event similar to Xid */XA_PREPARE_LOG_EVENT= 38,/**Add new events here - right above this comment!Existing events (except ENUM_END_EVENT) should never change their numbers*/ENUM_END_EVENT /* end marker */};
可見在MySQL 5.7.22這個版本中,共支持38種不同的binlog事件類型。把每一種事件格式都嘮叨清楚要花費(fèi)很多篇幅,并且沒有多大的必要,我們下邊只舉一個具體的例子進(jìn)行描述。
舉一個具體的例子——格式描述事件
每個binlog日志文件都以格式描述事件作為第一個事件,它對應(yīng)的Log_event_type就是FORMAT_DESCRIPTION_EVENT。我們看一下這種事件的結(jié)構(gòu):

從圖中我們可以知道,格式描述事件共占用119字節(jié),是由event header和event data兩部分構(gòu)成的,其中event header是各個事件都有的部分,我們上邊詳細(xì)嘮叨過event header中各個字段的含義,這里就不贅述了。另外,在event data部分,格式描述事件的event data中只有固定長度部分,沒有可變長度部分,其中的各個字段含義如下:
?binlog_version:使用的binlog版本。?server_version:產(chǎn)生此事件的MySQL服務(wù)器的版本。?create_timestamp:產(chǎn)生此事件時的時間戳,該字段的值和event header中timestamp中的值一樣。?header_length:此事件的event header占用的存儲空間大小。?post-header length:使用1個字節(jié)來表示每個事件的event data部分占用的存儲空間大小(不包括校驗(yàn)和相關(guān)字段),當(dāng)前我使用的MySQL版本為5.7.22,共包含38種不同的事件,post-header length字段就占用了38個字節(jié)。?checksum_alg:表示計(jì)算事件校驗(yàn)和的算法(該字段為1時表示采用CRC32算法)。?checksum:表示本事件的校驗(yàn)和。
嘮叨了很多,大家真正打開一個binlog日志文件來看一下:
魔數(shù): FE62696Etimestamp: 8AB5A861typecode: 0Fserver_id: 03000000event_length: 77000000next_postion: 7B000000flags: 0000binlog_version: 0400server_version: 352E37 2E32312D 6C6F6700 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 000000create_timestamp: 8AB5A861header_length: 13post-header length(共38種): 380D0008 00120004 04040412 00005F00 041A0800 00000808 08020000 000A0A0A 2A2A0012 3400checksum_alg: 01checksum:????? 5B7108A3
?
小貼士:
其他事件的event data部分大家可以參考一下MySQL internal文檔。另外,也可以使用mysqlbinlog,配合--hexdump啟動選項(xiàng)來直接分析binlog的二進(jìn)制格式。
基于語句(Statement)和基于行(Row)的binlog
同一條SQL語句,隨著啟動選項(xiàng)binlog-format的不同,可能生成不同類型的binlog事件:
?當(dāng)以啟動選項(xiàng)--binlog-format=STATEMENT啟動MySQL服務(wù)器時,生成的binlog稱作基于語句的日志。此時只會將一條SQL語句將會被完整的記錄到binlog中,而不管該語句影響了多少記錄。
?當(dāng)以啟動選項(xiàng)--binlog-format=ROW啟動MySQL服務(wù)器時,生成的binlog稱作基于行的日志。此時會將該語句所改動的記錄的全部信息都記錄上。
?當(dāng)以啟動選項(xiàng)--binlog-format=MIXED啟動MySQL服務(wù)器時,生成的binlog稱作基于行的日志。此時在通常情況下采用基于語句的日志,在某些特殊情況下會自動轉(zhuǎn)為基于行的日志(這些具體情況請參考:https://dev.mysql.com/doc/refman/8.0/en/binary-log-mixed.html)。
小貼士:
我們也可以通過修改會話級別的binlog_format系統(tǒng)變量的形式來修改只針對本客戶端執(zhí)行語句生成的binlog日志的格式。
基于語句的binlog
假如服務(wù)器啟動時添加了--binlog-format=STATEMENT啟動選項(xiàng),我們執(zhí)行如下語句:
UPDATE s1 SET common_field = 'xx' WHERE id > 9990;然后使用mysqlbinlog實(shí)用工具查看一下相應(yīng)的binlog內(nèi)容:
mysqlbinlog --verbose xiaohaizi-bin.000007...這里省略了很多內(nèi)容# at 308#211207 21:00:27 server id 3 end_log_pos 440 CRC32 0x713f80ae Query thread_id=2 exec_time=0 error_code=0use `xiaohaizi`/*!*/;SET TIMESTAMP=1638882027/*!*/;update s1 set common_field= 'xx' where id > 9990/*!*/;...這里省略了很多內(nèi)容
?
可見,基于語句的binlog只將更新語句是什么記錄下來了。
基于行的binlog
假如服務(wù)器啟動時添加了--binlog-format=ROW啟動選項(xiàng),我們執(zhí)行如下語句:
UPDATE s1 SET common_field = 'xxx' WHERE id > 9990;然后使用mysqlbinlog實(shí)用工具查看一下相應(yīng)的binlog內(nèi)容:
mysqlbinlog --verbose xiaohaizi-bin.000008...這里省略了很多內(nèi)容## UPDATE `xiaohaizi`.`s1`## WHERE## @1=9991## @2='7cgwfh14w6nql61pvult6ok0ccwe'## @3='799105223'## @4='c'## @5='gjjiwstjysv1lgx'## @6='zg1hsvqrtyw2pgxgg'## @7='y244x02'## @8='xx'## SET## @1=9991## @2='7cgwfh14w6nql61pvult6ok0ccwe'## @3='799105223'## @4='c'## @5='gjjiwstjysv1lgx'## @6='zg1hsvqrtyw2pgxgg'## @7='y244x02'## @8='xxx'## UPDATE `xiaohaizi`.`s1`## WHERE## @1=9992## @2='2sfq3oftc'## @3='815047282'## @4='ub'## @5='73hw14kbaaoa'## @6='fxnqzef3rrpc7qzxcjsvt14nypep4rqi'## @7='10vapb6'## @8='xx'## SET## @1=9992## @2='2sfq3oftc'## @3='815047282'## @4='ub'## @5='73hw14kbaaoa'## @6='fxnqzef3rrpc7qzxcjsvt14nypep4rqi'## @7='10vapb6'## @8='xxx'...這里省略了很多內(nèi)容
?
可見,基于行的binlog將更新語句執(zhí)行過程中每一條記錄更新前后的值都記錄下來了。
基于語句的binlog的問題
在有主從復(fù)制的場景中,使用基于語句的日志可能會造成主服務(wù)器和從服務(wù)器維護(hù)的數(shù)據(jù)不一致的情況。
比方說我們有一個表t:
CREATE TABLE t (id INT UNSIGNED NOT NULL AUTO_INCREMENT,c VARCHAR(100),PRIMARY KEY(ID));
如果我們執(zhí)行如下語句:
INSERT INTO t(c) SELECT c FROM other_table;這個語句是想將other_table表中列c的值都插入到表t的列c中,而表t的id列是自增列,可以自動生成。
如果主庫和從庫的服務(wù)器執(zhí)行SELECT c FROM other_table返回記錄的順序不同的話(不同服務(wù)器版本、不同的系統(tǒng)變量配置都可能導(dǎo)致同一條語句返回結(jié)果的順序不同),那么針對表t相同id值的記錄來說,列c就可能具有不同的值,這就會造成主從之間數(shù)據(jù)的不一致。
而如果將binlog的格式改為基于行的日志的話,由于主庫在執(zhí)行完語句后將該語句插入的每條完整的記錄都寫入binlog日志,就不會造成主從之間不一致了。
