文章目錄
- 前言
- Binlog的結構
- 恢復誤洗掉的記錄
- 找出 Binlog 中的大事務
- 切割 Binlog 中的大事務
- 后記
前言
MySQL 的 Binlog 記錄著 MySQL 資料庫的所有變更資訊,了解 Binlog 的結構可以幫助我們決議Binlog,甚至對 Binlog 進行一些修改,或者說是“篡改”,例如實作類似于 Oracle 的 flashback 的功能,恢復誤洗掉的記錄,把 update 的記錄再還原回去等,本文將帶您探討一下這些神奇功能的實作,您會發現比您想象地要簡單得多,本文指的 Binlog 是 ROW 模式的 Binlog,這也是 MySQL 8 里的默認模式,STATEMENT 模式因為使用中有很多限制,現在用地越來越少了,
Binlog的結構
Binlog由事件(event)組成,請注意是事件(event)不是事務(transaction),一個事務可以包含多個事件,事件描述對資料庫的修改內容,
從 MySQL 5 版本開始,Binlog 采用的是 v4 版本,事件的型別根據 MySQL 的內部檔案,有下面36類:
enum Log_event_type {
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= 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,
PRE_GA_WRITE_ROWS_EVENT = 20,
PRE_GA_UPDATE_ROWS_EVENT = 21,
PRE_GA_DELETE_ROWS_EVENT = 22,
WRITE_ROWS_EVENT = 23,
UPDATE_ROWS_EVENT = 24,
DELETE_ROWS_EVENT = 25,
INCIDENT_EVENT= 26,
HEARTBEAT_LOG_EVENT= 27,
IGNORABLE_LOG_EVENT= 28,
ROWS_QUERY_LOG_EVENT= 29,
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,
ENUM_END_EVENT
/* end marker */
};
每個 Binlog 檔案總是以 Format Description Event 作為開始,以 Rotate Event 結束作為結束,我們來看一個 Binlog 的例子:
mysql> show binlog events in 'scut.000023';
+-------------+-----+----------------+-----------+-------------+--------------------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+-------------+-----+----------------+-----------+-------------+--------------------------------------------------------+
| scut.000023 | 4 | Format_desc | 1024 | 123 | Server ver: 5.7.31-0ubuntu0.16.04.1-log, Binlog ver: 4 |
| scut.000023 | 123 | Previous_gtids | 1024 | 154 | |
| scut.000023 | 154 | Anonymous_Gtid | 1024 | 219 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
| scut.000023 | 219 | Query | 1024 | 291 | BEGIN |
| scut.000023 | 291 | Rows_query | 1024 | 330 | # delete from tt1 |
| scut.000023 | 330 | Table_map | 1024 | 378 | table_id: 111 (test.tt1) |
| scut.000023 | 378 | Delete_rows | 1024 | 434 | table_id: 111 flags: STMT_END_F |
| scut.000023 | 434 | Xid | 1024 | 465 | COMMIT /* xid=216 */ |
| scut.000023 | 465 | Rotate | 1024 | 507 | scut.000024;pos=4 |
+-------------+-----+----------------+-----------+-------------+--------------------------------------------------------+
9 rows in set (0.00 sec)
關于“show binlog events”語法顯示的每一列的作用說明如下:
| 列名 | 說明 |
|---|---|
| Log_name | 當前事件所在的 binlog 檔案名稱 |
| Pos | 當前事件的開始位置,每個事件都占用固定的位元組大小,結束位置(End_log_position)減去Pos,就是這個事件占用的位元組數, |
| Event_type | 表示事件的型別 |
| Server_id | 表示產生這個事件的 MySQL server_id |
| End_log_position | 下一個事件的開始位置 |
| Info | 當前事件的描述資訊 |
每個事件型別的說明可以參考 MySQL 的內部檔案,我們這里說明一下這里遇到的幾個事件型別:
| 事件名 | 說明 |
|---|---|
| Format_desc | 是 binlog 檔案的第一個事件,在 Info 列,我們可以看到,其標明了 MySQL Server 的版本是5.7.31, Binlog 版本是4, |
| Previous_gtids | 這是表示之前的 Binlog 檔案中,已經執行過的 GTID,需要我們開啟 GTID 選項,這個事件才會有值, |
| Anonymous_Gtid | 沒有開啟 GTID 選項時,每個事務開始的事件; |
| Query | 是向 Binlog 發生一個陳述句,這里是事務的開始陳述句 begin, |
| Rows_query | 記錄SQL,這個事件只有當引數 binlog_rows_query_log_events 為 TRUE 的情況下才會產生,這個引數的默認為值為 FALSE, |
| Table_map | 記錄將要被修改的表的結構 |
| Delete_rows | 從表中洗掉一個記錄 |
| Xid | 事務 commit 的時候寫入事務 ID |
| Rotate | Rotate Event是每個Binlog檔案的結束事件,在Info列中,我們看到了其指定了下一個 Binlog檔案的名稱是 mysql-bin.000018, |
根據官方檔案,事件(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 |
+=====================================+
恢復誤洗掉的記錄
現在我們已經了解了 Binlog 的結構,我們可以試著修改 Binlog 里的資料,例如前面舉例的 Binlog 洗掉了一條記錄,我們可以試著把這條記錄恢復,Binlog 里面有個洗掉行(DELETE_ROWS_EVENT)的事件,就是這個事件洗掉了記錄,這個事件和寫行(WRITE_ROWS_EVENT)的事件的資料結構是完全一樣的, 只是洗掉行事件的型別是32,寫行事件的型別是30,我們把對應的 Binlog 位置的32改成30即可把已經洗掉的記錄再插入回去,從前面的“show binlog events”里面可看到這個 DELETE_ROWS_EVENT 是從位置378開始的,這里的位置就是 Binlog 檔案的實際位置(以位元組為單位),從事件(event)的結構里面可以看到 type_code 是在 event 的第5個位元組,我們寫個 Python 小程式把把第383(378+5=383)位元組改成30即可,當然您也可以用二進制編輯工具來改,下面是這個 Python 小程式的例子:
#! /usr/bin/python3
import sys
if len(sys.argv) != 3:
print ('Please run chtype.py inputType changedType.')
sys.exit()
inputType=open(sys.argv[1],"rb")
changedType=open(sys.argv[2],"wb")
changedType.write(inputType.read(382))
changedType.write(chr(30).encode())
inputType.seek(1,1)
while True:
line = inputType.readline()
if not line:
break
changedType.write(line)
inputType.close()
changedType.close()
我們把原來的 Binlog 和修改后的 Binlog 進行一個對比:

發現這兩個 Binlog只有一個位元組有區別,也就是 type_code 從32變成了30,注意 Binlog 里面顯示的是16進制的數字,
我們分別應用一下原來的 Binlog 和修改后的 Binlog,看看效果如何?
$ mysql -e "select * from test.tt1";
$ mysqlbinlog ./scut.000023_ch |mysql
$ mysql -e "select * from test.tt1";
+---------------------+
| col1 |
+---------------------+
| aaaaaaaaaaaaaaaaaaa |
+---------------------+
$ mysqlbinlog ./scut.000023 |mysql
$ mysql -e "select * from test.tt1";
$ mysqlbinlog ./scut.000023_ch |mysql
$ mysql -e "select * from test.tt1";
+---------------------+
| col1 |
+---------------------+
| aaaaaaaaaaaaaaaaaaa |
+---------------------+
我們發現這兩個 Binlog 可以分別把對應的記錄洗掉和插入到 MySQL 資料庫中,這樣我們就成功地實作了類似于 Oracle 的 flashback 功能,
找出 Binlog 中的大事務
由于 ROW 模式的 Binlog 是每一個變更都記錄一條日志,因此一個簡單的 SQL,在 Binlog 里可能會產生一個巨無霸的事務,例如一個不帶 where 的 update 或 delete 陳述句,修改了全表里面的所有記錄,每條記錄都在 Binlog 里面記錄一次,結果是一個巨大的事務記錄,這樣的大事務經常是產生麻煩的根源,我的一個客戶有一次向我抱怨,一個 Binlog 前滾,滾了兩天也沒有動靜,我把那個 Binlog 決議了一下,發現里面有個事務產生了 1.4G 的記錄,修改了66萬條記錄!下面是一個簡單的找出 Binlog 中大事務的 Python 小程式,我們知道用 mysqlbinlog 決議的 Binlog,每個事務都是以BEGIN 開頭,以 COMMIT 結束,我們找出 BENGIN 前面的“# at”的位置,檢查 COMMIT 后面的“# at”位置,這兩個位置相減即可計算出這個事務的大小,下面是這個 Python程式的例子,
$ cat ./checkBigTran.py
#! /usr/bin/python3
import sys
position=0
beginPosition=0
endPosition=0
maxSize=0
isEnd=0
for line in sys.stdin:
if line[: 4]=='# at':
position=int(line[5:])
if isEnd:
endPosition=position
isEnd=0
if line[: 5]=='BEGIN':
beginPosition=position
if line[: 6]=='COMMIT':
isEnd=1
if endPosition-beginPosition>maxSize:
maxBeginPosition= beginPosition
maxEndPosition=endPosition
maxSize=endPosition-beginPosition
print("The largest transaction size is %d, the begion position is %d, the end position is %d." % (maxSize,maxBeginPosition,maxEndPosition))
用這個小程式檢查一下可能包含大事務的 Binlog:
$ mysqlbinlog binlog1|./checkBigTran.py
The largest transaction size is 1468183501, the begion position is 5737766, the end position is 1473921267.
發現里面果然包含了一個1.4G的大事務,
切割 Binlog 中的大事務
對于大的事務, MySQL 會把它分解成多個事件(注意一個是事務TRANSACTION,另一個是事件EVENT),事件的大小由引數 binlog-row-event-max-size 決定,這個引數默認是8K,因此我們可以把若干個事件切割成一個單獨的略小的事務,例如下面這個 Binlog:
mysql> show binlog events in 'scut.000025';
+-------------+-----+----------------+-----------+-------------+--------------------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+-------------+-----+----------------+-----------+-------------+--------------------------------------------------------+
| scut.000025 | 4 | Format_desc | 1024 | 123 | Server ver: 5.7.31-0ubuntu0.16.04.1-log, Binlog ver: 4 |
| scut.000025 | 123 | Previous_gtids | 1024 | 154 | |
| scut.000025 | 154 | Anonymous_Gtid | 1024 | 219 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
| scut.000025 | 219 | Query | 1024 | 291 | BEGIN |
| scut.000025 | 291 | Rows_query | 1024 | 343 | # insert into tt1 values ('1') |
| scut.000025 | 343 | Table_map | 1024 | 391 | table_id: 111 (test.tt1) |
| scut.000025 | 391 | Write_rows | 1024 | 429 | table_id: 111 flags: STMT_END_F |
| scut.000025 | 429 | Rows_query | 1024 | 481 | # insert into tt1 values ('2') |
| scut.000025 | 481 | Table_map | 1024 | 529 | table_id: 111 (test.tt1) |
| scut.000025 | 529 | Write_rows | 1024 | 567 | table_id: 111 flags: STMT_END_F |
| scut.000025 | 567 | Xid | 1024 | 598 | COMMIT /* xid=397 */ |
| scut.000025 | 598 | Rotate | 1024 | 640 | scut.000026;pos=4 |
+-------------+-----+----------------+-----------+-------------+--------------------------------------------------------+
12 rows in set (0.01 sec)
這個 Binlog 的兩個 insert 是在一個事務里面完成的,我們可以兩個事務之間插入 xid、Anonymous_Gtid, Query 等三個事件把一個事務切割成兩個事務, Rows_query 這個事件不用插入,這個事件是注釋掉了的,記錄的是執行的 SQL,這個事件只有在引數 binlog_rows_query_log_events 為 on 時才會有,默認是 off ,相應的 Python 程式如下:
# cat splitTran.py
#! /usr/bin/python3
import sys
if len(sys.argv) != 3:
print ('Please run splitTrans.py inputBinlog changedBinlog.')
sys.exit()
inputBinlog=open(sys.argv[1],"rb")
changedBinlog=open(sys.argv[2],"wb")
changedBinlog.write(inputBinlog.read(429)) # read from the head of input binlog file to the first insert, then write into the changed binlog file.
firstInsert=inputBinlog.tell()
inputBinlog.seek(567,0) # locate to the xid event
changedBinlog.write(inputBinlog.read(31)) # read from 567 to 598, write xid event, into the changed binlog file.
inputBinlog.seek(154,0) # locate to the Anonymous_Gtid, Query events.
changedBinlog.write(inputBinlog.read(137)) # read from 154 to 291, write Anonymous_Gtid, Query events into changed binlog file.
inputBinlog.seek(firstInsert)
while True:
line = inputBinlog.readline()
if not line:
break
changedBinlog.write(line)
inputBinlog.close()
changedBinlog.close()
我們執行這個 Python 程式,生成一個新的 Binlog ,然后把新的 Binlog 應用到 MySQL,
$ ./splitTran.py scut.000025 scut.000025_ch
$ mysqlbinlog scut.000025_ch |mysql
$
我們看看執行地效果:
mysql> show binlog events in 'scut.000026';
+-------------+-----+----------------+-----------+-------------+------------------------------------------------------------------------------------------------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+-------------+-----+----------------+-----------+-------------+------------------------------------------------------------------------------------------------------------------------------------+
| scut.000026 | 4 | Format_desc | 1024 | 123 | Server ver: 5.7.31-0ubuntu0.16.04.1-log, Binlog ver: 4 |
| scut.000026 | 123 | Previous_gtids | 1024 | 154 | |
| scut.000026 | 154 | Anonymous_Gtid | 1024 | 219 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
| scut.000026 | 219 | Query | 1024 | 287 | BEGIN |
| scut.000026 | 287 | Rows_query | 1024 | 439 | # BINLOG '
EbA7XxMABAAAMAAAAIcBAAAAAG8AAAAAAAEABHRlc3QAA3R0MQAB/gL+QAEYYumN
EbA7Xx4ABAAAJgAAAK0BAAAAAG8AAAAAAAEAAgAB//4BMeeyFcw=
' |
| scut.000026 | 439 | Table_map | 1024 | 487 | table_id: 111 (test.tt1) |
| scut.000026 | 487 | Write_rows | 1024 | 525 | table_id: 111 flags: STMT_END_F |
| scut.000026 | 525 | Xid | 1024 | 556 | COMMIT /* xid=425 */ |
| scut.000026 | 556 | Anonymous_Gtid | 1024 | 621 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
| scut.000026 | 621 | Query | 1024 | 689 | BEGIN |
| scut.000026 | 689 | Rows_query | 1024 | 841 | # BINLOG '
F7A7XxMABAAAMAAAABECAAAAAG8AAAAAAAEABHRlc3QAA3R0MQAB/gL+QAE+WKbj
F7A7Xx4ABAAAJgAAADcCAAAAAG8AAAAAAAEAAgAB//4BMmfP2Zk=
' |
| scut.000026 | 841 | Table_map | 1024 | 889 | table_id: 111 (test.tt1) |
| scut.000026 | 889 | Write_rows | 1024 | 927 | table_id: 111 flags: STMT_END_F |
| scut.000026 | 927 | Xid | 1024 | 958 | COMMIT /* xid=432 */ |
+-------------+-----+----------------+-----------+-------------+------------------------------------------------------------------------------------------------------------------------------------+
14 rows in set (0.00 sec)
我們看到兩個 insert 已經分開到兩個事務里面了,
后記
ROW模式下,即使我們只更新了一條記錄的其中某個欄位,也會記錄每個欄位變更前后的值,這個行為是 binlog_row_image 引數控制的,這個引數有3個值,默認為FULL,也就是記錄列的所有修改,即使欄位沒有發生變更也會記錄,這樣我們就可以實作類似 Oracle 的 flashback 的功能,我個人估計 MySQL 未來的版本從可能會基于 Binlog 推出這樣的功能,
了解了 Binlog 的結構,再加上 Python 這把瑞士軍刀,我們還可以實作很多功能,例如我們可以統計哪個表被修改地最多? 我們還可以把 Binlog 切割成一段一段的,然后再重組,可以靈活地進行 MySQL 資料庫的修改和遷移等作業,
CSDN認證博客專家
10G OCM
12C OCM
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/142084.html
標籤:其他
下一篇:plc程式圖
