一、前言
對,沒錯,我又水了好一陣子,深刻反思寄幾。前段時間,工作項目上出於對excel等批量操作可能出現誤操作的問題,要求提供一個能夠根據操作批次進行數據回滾的能力。在開發的過程中接觸到了MySQL的Binary Log,感覺有些收獲,記錄一下。
二、Binary Log的概念
首先我們要了解一下什么是Binary Log(詳情點進去看):
Binary Log(二進制文件),包含了描述數據庫更改的“事件”,例如創建表的操作或者改變表的數據。如果采用基於行的日志,它還能包含已經發生更改的語句事件(比如,沒有對應行的DELETE事件)。
也就是說你對數據庫的操作,包括INSERT、DELETE在內的CRUD,binlog(命令里簡稱)都會包含進去,那么,如果我們能夠解析(因為從binlog的名字可以知道,這是一個二進制文件,不是人類能夠閱讀的)出它的內容,就可以對執行的語句進行反向操作,對誤操作的數據進行恢復。
這也是binlog的目的之一:數據恢復
而binlog的另一個用途就是用於主從復制。我們都知道在現在的大數據背景下,常規的單數據庫已經無法滿足訪問量的需求,於是出現了數據庫集群:主數據庫進行寫操作,從數據庫進行讀操作,從而降低數據庫的訪問壓力,而為了保證數據庫的內容一致,就要用到binlog來保證了,如下圖:這里不具體展開。
三、通過shell查看Binary Log
了解了binlog的概念之后,我們來通過shell查看一下binlog。
首先要在my.cnf中添加如下配置:
[mysqld]
log-bin=mysql-bin
binlog-format=ROW #選擇row模式
server_id=1 #避免和slave機器重復
log_bin_basename=xxx 可選
log_bin_index=xxx 可選
保存后重啟MySQL。
進入MySQL Command:
mysql> show variables like '%log_bin%'; 查看binglog路徑
+---------------------------------+---------------------------------------+
| Variable_name | Value |
+---------------------------------+---------------------------------------+
| log_bin | ON |
| log_bin_basename | /usr/local/mysql/data/mysql-bin |
| log_bin_index | /usr/local/mysql/data/mysql-bin.index |
| log_bin_trust_function_creators | OFF |
| log_bin_use_v1_row_events | OFF |
| sql_log_bin | ON |
+---------------------------------+---------------------------------------+
- log_bin:on 表示開啟了Binary Log
- log_bin_basename:binary log的基本文件名,可以在my.cnf指定
- log_bin_index:binlog文件的索引文件,可以在my.cnf指定
mysql> show binary logs;
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000001 | 9309624 |
| mysql-bin.000002 | 9008629 |
| mysql-bin.000003 | 229080 |
| mysql-bin.000004 | 15410010 |
| mysql-bin.000005 | 177 |
| mysql-bin.000006 | 5798399 |
| mysql-bin.000007 | 177 |
+------------------+-----------+
顯示當前數據庫所有的binary log文件和文件大小
知道這些之后,退出MySQL Command,在shell中進行查看:
> sudo -u mysql mysqlbinlog /usr/local/mysql/data/mysql-bin.000030
由於我的/usr/local/mysql/data的在安裝MySQL的時候默認只給了mysql用戶,所以要加-u切換成mysql。
至此便可以查看到二進制文件中的內容(截取了部分):
# at 1341475
#180416 15:58:45 server id 1 end_log_pos 1341582 CRC32 0x0ca6c030 Table_map: `user-center`.`t_management_entity_role` mapped to number 127
# at 1341582
#180416 15:58:45 server id 1 end_log_pos 1341686 CRC32 0x33552cef Write_rows: table id 127 flags: STMT_END_F
BINLOG '
tVfUWhMBAAAAawAAAI54FAAAAH8AAAAAAAEADnNoLXVzZXItY2VudGVyABh0X21hbmFnZW1lbnRf
ZW50aXR5X3JvbGUADAMPDw8PDwEPDxIPEhJgADYAYAC0AAMAAwDAAADAAAASADDApgw=
tVfUWh4BAAAAaAAAAPZ4FAAAAH8AAAAAAAEAAgAM//8Q8IkAAAARc3ViX2VtcGxveWVlX2RlcHQG
5qCh5belDXNjaG9vbF93b3JrZXIBMQIBMAN6a2qZn6D7wAN6a2qZn6D7wO8sVTM=
'/*!*/;
# at 1341686
#180416 15:58:45 server id 1 end_log_pos 1341717 CRC32 0x1fdc2123 Xid = 22495
COMMIT/*!*/;
# at 1341717
#180416 16:41:12 server id 1 end_log_pos 1341740 CRC32 0xca0bf05c Stop
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;
看到這里,覺得BINLOG具體里面的還是非人類能夠閱讀的。想要知道其中的秘密,看來還是要閱讀MySQL的開發手冊才行。
四、binlog的解析原理以及GitHub上的開源解析工具
4.1 binlog的幾種格式
要了解MySQL的解析原理,當然要從頭到尾仔細閱讀MySQL的開發手冊,想了解的可以點這里。里面詳細介紹了MySQL的信息。筆者撿其中介紹binlog的部分來簡單說一說。
首先要了解binlog的格式,binlog的格式分為三種:STATEMENT,ROW,MIXED,下面來一一介紹一下:
- STATEMENT
- 從字面上看就是描述的意思。記錄了相對於操作的SQL語句,比如在控制台執行了
DELETE FROM foo WHERE id = 1
,那么在binlog上就會添加上這條語句。好處很明顯:直觀。 - 對,總是有個但是~~,但是不保證日志記錄的正確性。 - 客戶端可能不生成行事件
- ROW
- 保證日志記錄的正確性 - DML的改變可能只記錄在ROW模式中,不會記錄在STATEMENT模式中。每行內容的改變由之前的圖像(Before Image,BI)和之后的圖像(After Image,AI)組成。BI記錄了該行改變前的每列數據,而AI則是改變后的每列數據。有三種類型的log_event:
- Write_rows_log_event:在表中添加新的一行,還有AI。
- Update_rows_log_event:修改表中已經存在的行,AI和BI都有。
- Delete_rows_log_event:刪除表中的存在的行,只有BI。
- MIXED
- 保證日志記錄的正確性,首先采用STATEMENT記錄,如果不能正確記錄,則采用ROW模式記錄。 - 增加了處理的難度,要寫兩種實現。
由上可見,STATEMENT模式是不可用的,因為它不能保證日志的正確性,而MIXED模式會增加代碼的復雜度,要考慮到兩種情況,增加了代碼的工作量,所以實現上采用ROW模式是普遍的做法。
###4.2 binlog的事件格式
The Binary Log是閱讀的主要內容。里面着重介紹了binlog的消息體格式,事件格式等內容。筆者挑部分說一下。
前面提到對數據庫的操作是以event事件的形式以二進制寫入binlog的,那么event是什么樣的格式呢?所有的event事件都有一個共同的通用結構,由一個事件標題和事件數據組成:
+===================+
| event header |
+===================+
| event data |
+===================+
而event header和data的部分在不同的MySQL版本下面有不同的變化。具體表現為:
- v1:在MySQL 3.23中使用
- v3:是的沒錯,沒有v2,在MySQL 4.0.2 到 4.1使用
- v4:在 MySQL 5.0以及以上版本中使用
5.0版本以前的就不介紹了,直接來看v4版本的event結構:
+=====================================+
| event | timestamp 0 : 4 |
| header +----------------------------+
| | type_code 4 : 1 |
| +----------------------------+
| | server_id 5 : 4 |
| +----------------------------+
| | event_length 9 : 4 |
| +----------------------------+
| | next_position 13 : 4 |
| +----------------------------+
| | flags 17 : 2 |
| +----------------------------+
| | extra_headers 19 : x-19 |
+=====================================+
| event | fixed part x : y |
| data +----------------------------+
| | variable part |
+=====================================+
- 字節為單位
- header的長度=x 字節
- data的長度 = (event的長度 - x) 字節
- fixed 部分的長度 = y字節變量長度。
- x是由格式描述時間(format description event-FDE)中定義的,目前x是19,即extra_headers是空的
- y指定的是事件的類型,也是FDE中定義的,相同事件的fixed part 長度相同, 不同事件的長度不同
我們來看一下event data部分的格式,以插入行事件的格式為例(Write_rows_log_event/WRITE_ROWS_EVENT):
- Fixed數據部分
- 6字節:表的id - 2字節:留着備用
- variable數據部分
- 打包整數(一種特殊格式的無符號整型,能夠存儲8字節的整數,表示方法詳見這里):表中列的數量。 - 可變大小:用bit來表示是否每列被使用,一個bit一列,如果N個列,要用
INT((N+7)/8)
字節 - 可變大小(針對UPDATE_ROWS_LOG_EVENT),與上面相同,表示的是更新后每列是否被使用 - 可變大小:零行或多行,截止位置由event頭部的event_length決定,每行的格式如下:- 可變大小:bit來表示在行中的每個字段是否為NULL,1表示為null,0表示不為null,只有在數據部分第二部分的列才會出現在這里。需要占用
INT((N+7)/8)
字節 - 可變大小:行圖像,包含所有表格字段的值。 這只會列出使用的表格字段(根據變量數據部分的第二個字段)和非NULL(根據前一個字段)。
- 針對UPDATE_ROWS_LOG_EVENT,上述兩個重復一遍,表示更新后的值
- 可變大小:bit來表示在行中的每個字段是否為NULL,1表示為null,0表示不為null,只有在數據部分第二部分的列才會出現在這里。需要占用
這也就是為什么下面提到的幾個開源項目里對事件(event)進行轉換的時候,出現莫名其妙的對不同字節轉化成不同字段。
4.3 GitHub上的開源解析binlog工具
這里簡單介紹一下已知的幾個解析binlog的項目:
canal 阿里巴巴mysql數據庫binlog的增量訂閱&消費組件。在了解binlog能解析出來的內容后,覺得canal做的是真的好,原生的binlog解析出來是沒有列名信息、列編碼、列類型的,canal在此基礎上多加了一層,補全對應的列信息,完善了大眾業務理解binlog的基本訴求。事實上是添加列名並不是簡單的發送
show create table xxx
這么簡單,考慮到列可能會被增加、刪除等,之前t0時刻消費的列可能會對應不上此時t1時刻的列,中間會出現很多問題。mysql-binlog-connector-java 前身是open-replicator,在作者不再更新代碼后,該作者完全重寫了該項目,添加了很多MySQL5.x的新特性。解析結果相對於canal就很原生了。不過也讓筆者膜拜。
binlog2sql 前面兩個都是java語言的項目,這個是python寫的,從MySQL binlog解析出你要的SQL。根據不同選項,你可以得到原始SQL、回滾SQL、去除主鍵的INSERT SQL等。算是最接近筆者需求的一個項目,基本上加上項目代碼就直接能用,但是筆者的強迫症發作,由於寫的項目是java的項目,雖然jython能實現,但是筆者還是想折騰一下其他的,就沒采用。o(TωT)o
五、總結
第一部分先記錄一下整個操作的過程,第二部分寫具體的實現過程。謝謝各位園友觀看,如果有描述不對的地方歡迎指正,與大家共同進步!