GreatSQL社區原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。 GreatSQL是MySQL的國產分支版本,使用上與MySQL一致。 用一個簡明、清晰的步驟來解析一下DML操作產生的binlog event。主要是 TABLE_MAP_EVENT 和 UPDATE_ROWS_EVENT ...
- GreatSQL社區原創內容未經授權不得隨意使用,轉載請聯繫小編並註明來源。
- GreatSQL是MySQL的國產分支版本,使用上與MySQL一致。
用一個簡明、清晰的步驟來解析一下DML操作產生的binlog event。主要是 TABLE_MAP_EVENT 和 UPDATE_ROWS_EVENT 類型的event。使用語法簡單易上手的Golang來編碼。資料庫使用的是MySQL 5.7.34版本, Golang 1.15版本。
獲取binlog event
獲取binlog一般是模擬成從庫封裝通訊 package 向主庫發送binlog dump命令(COM_BINLOG_DUMP或者COM_BINLOG_DUMP_GTID)來獲取binlog。在這篇文章中,是封裝的 COM_BINLOG_DUMP 向資料庫請求的指定位點的 event。
模擬操作產生event
查看自動自交參數:
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
簡單的表結構:
mysql> create table test (id int primary key, name char(10), addr varchar(10), birthdate date);
Query OK, 0 rows affected (0.03 sec)
準備數據:
mysql> insert into test value (1, 'tom', 'Hollywood', '1940-02-10');
Query OK, 1 row affected (0.02 sec)
mysql> insert into test value (2, 'Jerry', 'Hollywood', '1940-02-10');
Query OK, 1 row affected (0.01 sec)
mysql> select * from test;
+----+-------+-----------+------------+
| id | name | addr | birthdate |
+----+-------+-----------+------------+
| 1 | tom | Hollywood | 1940-02-10 |
| 2 | Jerry | Hollywood | 1940-02-10 |
+----+-------+-----------+------------+
2 rows in set (0.01 sec)
查看binlog:
mysql> show binary logs;
+--------------------+-----------+
| Log_name | File_size |
+--------------------+-----------+
| greatdb-bin.000001 | 98896 |
+--------------------+-----------+
1 row in set (0.00 sec)
查看event(部分輸出省略):
mysql> show binlog events in 'greatdb-bin.000001';
+--------------------+-------+----------------+-----------+-------------+-----------------------------------------------------------------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+--------------------+-------+----------------+-----------+-------------+-----------------------------------------------------------------------------------------------------+
| greatdb-bin.000001 | 98110 | Gtid | 13 | 98175 | SET @@SESSION.GTID_NEXT= '7950f91c-2f13-11ed-b2a8-52540099600c:420' |
| greatdb-bin.000001 | 98175 | Query | 13 | 98336 | use `test`; create table test (id int primary key, name char(10), addr varchar(10), birthdate date) |
| greatdb-bin.000001 | 98336 | Gtid | 13 | 98401 | SET @@SESSION.GTID_NEXT= '7950f91c-2f13-11ed-b2a8-52540099600c:421' |
| greatdb-bin.000001 | 98401 | Query | 13 | 98473 | BEGIN |
| greatdb-bin.000001 | 98473 | Table_map | 13 | 98527 | table_id: 455 (test.test) |
| greatdb-bin.000001 | 98527 | Write_rows | 13 | 98584 | table_id: 455 flags: STMT_END_F |
| greatdb-bin.000001 | 98584 | Xid | 13 | 98615 | COMMIT /* xid=43677697 */ |
| greatdb-bin.000001 | 98615 | Gtid | 13 | 98680 | SET @@SESSION.GTID_NEXT= '7950f91c-2f13-11ed-b2a8-52540099600c:422' |
| greatdb-bin.000001 | 98680 | Query | 13 | 98752 | BEGIN |
| greatdb-bin.000001 | 98752 | Table_map | 13 | 98806 | table_id: 455 (test.test) |
| greatdb-bin.000001 | 98806 | Write_rows | 13 | 98865 | table_id: 455 flags: STMT_END_F |
| greatdb-bin.000001 | 98865 | Xid | 13 | 98896 | COMMIT /* xid=43678192 */ |
+--------------------+-------+----------------+-----------+-------------+-----------------------------------------------------------------------------------------------------|
執行更新,生成 TABLE_MAP_EVENT 和 UPDATE_ROWS_EVENT :
mysql> update test set birthdate = '1940-02-11' where name = 'Jerry';
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from test;
+----+-------+-----------+------------+
| id | name | addr | birthdate |
+----+-------+-----------+------------+
| 1 | tom | Hollywood | 1940-02-10 |
| 2 | Jerry | Hollywood | 1940-02-11 |
+----+-------+-----------+------------+
2 rows in set (0.00 sec)
查看event(部分輸出省略):
mysql> show binlog events in 'greatdb-bin.000001';
+--------------------+-------+----------------+-----------+-------------+---------------------------------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+--------------------+-------+----------------+-----------+-------------+---------------------------------------------------------------------+
| greatdb-bin.000001 | 98896 | Gtid | 13 | 98961 | SET @@SESSION.GTID_NEXT= '7950f91c-2f13-11ed-b2a8-52540099600c:423' |
| greatdb-bin.000001 | 98961 | Query | 13 | 99033 | BEGIN |
| greatdb-bin.000001 | 99033 | Table_map | 13 | 99087 | table_id: 455 (test.test) |
| greatdb-bin.000001 | 99087 | Update_rows | 13 | 99171 | table_id: 455 flags: STMT_END_F |
| greatdb-bin.000001 | 99171 | Xid | 13 | 99202 | COMMIT /* xid=43691718 */ |
+--------------------+-------+----------------+-----------+-------------+---------------------------------------------------------------------+
因為 binlog_rows_query_log_events 參數是關閉的,所以不會產生 ROWS_QUERY_LOG_EVENT 類型的event,我們的一個更新語句在開啟GTID的情況下產生了五個event,分別是:GTID_LOG_EVENT、QUERY_EVENT、TABLE_MAP_EVENT、UPDATE_ROWS_EVENT、XID_EVENT。最後的 XID_EVENT 也標志著事務成功提交。
至此,獲取到了目標 TABLE_MAP_EVENT 和 UPDATE_ROWS_EVENT 兩個類型的event。通過計算也可以看到 Xid event 大小是固定的 32 位元組。
發送binlog dump請求event
封裝一個 COM_BINLOG_DUMP 數據包,然後建立連接發送到資料庫,就可以獲得 event 了。這個過程可另文詳說,這裡就不展開了。
我把兩個 event 轉成了base64串,主要是為了方便“攜帶”,拿著base64串去解析。
這裡有一個細節,就是我們向主庫發送binlog dump後,獲得的二進位位元組流,第 0 位是一個表示當前包是否正常的標誌位,第 0 位的值為 0,獲得的package就是正常的。
EVENT TYPE: TABLE_MAP_EVENT
Puk/YxMNAAAANgAAAA+DAQAAAMcBAAAAAAEABHRlc3QABHRlc3QABAP+DwoE/hQUAA7FA/Pg
EVENT TYPE: UPDATE_ROWS_EVENT_V2
Puk/Yx8NAAAAVAAAAGODAQAAAMcBAAAAAAEAAgAE///wAgAAAAVKZXJyeQlIb2xseXdvb2RKKA/wAgAAAAVKZXJyeQlIb2xseXdvb2RLKA/v9Mdc
通過mysqlbinlog解析(不使用--base64-output="decode-rows"選項)得到的文件中,也會輸出base64串,通常 TABLE_MAP_EVENT、UPDATE_ROWS_EVENT 會放在一起,這是因為 TABLE_MAP_EVENT 中有表中列信息(類型、非空等)。
上圖就是binlog file中的event顯示格式,可以看到和我們在上面轉換為base64串是一樣的,只是我們把它們分開來操作了。
解析event
獲取到binlog event的base64編碼,開始我們的解析,解析結果直接在控制台列印出來。這裡主要參看的是官方文檔描述的 event 格式。
https://dev.mysql.com/doc/internals/en/binlog-event-header.html
先把base64串轉為二進位,得到一個二進位的數組([]byte):
x := `Puk/YxMNAAAANgAAAA+DAQAAAMcBAAAAAAEABHRlc3QABHRlc3QABAP+DwoE/hQUAA7FA/Pg`
eventByte, _ := base64.StdEncoding.DecodeString(x)
解析event header
定義一個位置偏移常量,用來表示event位元組數組的偏移位置。
首先是解析 event header ,它的長度固定,是19個位元組。
然後解析前4位位元組,根據文檔,這4個位元組是timestamp格式的時間戳。解析後,偏移移動4個位元組。
pos := 0
fmt.Println("timestamp: " + strconv.Itoa(utils.Byte2Int(eventByte[pos:pos+4])) + " time: " + time.Unix(int64(utils.Byte2Int(eventByte[pos:pos+4])), 0).String()) // timestamp, 4 byte
pos += 4
接下來,是1個位元組長度的 event type 。
eventType := utils.Byte2Int(eventByte[pos : pos+1])
fmt.Println("event type: " + strconv.Itoa(eventType)) // event type, 1 byte
pos += 1
接下來是4個位元組長度的 server id,這個id就是實例的server id。binlog會記錄下這個信息。
fmt.Println("server id: " + strconv.Itoa(utils.Byte2Int(eventByte[pos:pos+4]))) // server id, 4 byte
pos += 4
這個輸出結果是 13。與我們在資料庫中查看是一致的。
mysql> select @@server_id;
+-------------+
| @@server_id |
+-------------+
| 13 |
+-------------+
1 row in set (0.00 sec)
接下來是4個位元組長度的event length,也就是當前這個event的二進位字元串數組的長度,包含 event header 和 event body 的總長度。
fmt.Println("event length: " + strconv.Itoa(utils.Byte2Int(eventByte[pos:pos+4]))) // event length, 4 byte
pos += 4
接下來是4個位元組長的 event log position ,也就是下一個 event 的 log position,而不是當前這個event。這裡輸出是:99087,正是下一個 UPDATE_ROWS_EVENT event的開始位置。
fmt.Println("log pos: " + strconv.Itoa(utils.Byte2Int(eventByte[pos:pos+4]))) // log pos, 4 byte
pos += 4
接下來是2個位元組長度的flags,flags詳細釋義可以參考:https://dev.mysql.com/doc/internals/en/binlog-event-flag.html ,這裡不再贅述。
fmt.Println("flags: " + strconv.Itoa(utils.Byte2Int(eventByte[pos:pos+2]))) // flags, 2 byte
pos += 2
至此,19個位元組長度的 event header 解析就完成了。最重要的信息當屬 event type ,因為我們要根據這個type信息才能繼續下麵的 event body 解析。不同類型的event, event header 佈局相同,但是 event body 卻不同,自然就要區別對待了。
TABLE_MAP_EVENT的event body
在解析 event header 時,獲得了 event type 。根據 event type 來解析不同的event。這個 event type 在源碼binlog_event.h文件中定義,其中 TABLE_MAP_EVENT = 19,UPDATE_ROWS_EVENT = 31。
TABLE_MAP_EVENT的佈局格式參考:
https://dev.mysql.com/doc/internals/en/table-map-event.html
代碼中判斷如果是 TABLE_MAP_EVENT ,就按照既定格式解析。很簡單的判斷:
if eventType == 19 {
......
}
在這個判斷中,增加解析邏輯。event body開頭就是6個位元組長度的table id ,這個值與我們執行
mysql> show binlog events in 'greatdb-bin.000001';
輸出中的 table_id: 455 是一致性。解析邏輯如下:
fmt.Println("table id:" + strconv.Itoa(byte2Int(eventByte[pos:pos+6]))) // table id, 6 byte
pos += 6
接下來是2個位元組長度的flags
fmt.Println("flags:" + strconv.Itoa(byte2Int(eventByte[pos : pos+2]))) // flag, 2 byte
pos += 2
接下來是schema信息,schema是變長的,無法確認用戶使用了多長的欄位,MySQL規定用戶的schema長度不得超過64個字元。所以這裡先用一個位元組來存儲scheme的長度信息。
lenSchemaN := byte2Int(eventByte[pos : pos+1])
fmt.Println("schema name length: " + strconv.Itoa(lenSchemaN)) // schema name length, 1 byte
pos += 1
scheme的長度信息確定了接下來的幾個位元組的schema name信息。
fmt.Println("schema name: " + bytesToString(eventByte[pos:pos+lenSchemaN])) // schema name, (schema name length) byte
pos += lenSchemaN
接下來是1個位元組的空閑位,用[00]填充,跳過不解析。
之後是1個位元組長度的table name length,這與上面的schema name格式相同。確定了table name length,就可以解析後面的table name了。
之後仍然是一個位元組的空閑位,用[00]填充。
pos += 1 //[00]
lenTableN := byte2Int(eventByte[pos : pos+1])
fmt.Println("table name length: " + strconv.Itoa(lenTableN)) // table name length, 1 byte
pos += 1
fmt.Println("table name: " + bytesToString(eventByte[pos:pos+lenTableN])) // table name, (table name length) byte
pos += lenTableN
pos += 1 //[00]
接下來是相對比較複雜的 column_count 信息,也就是執行的語句涉及了多少表中的列。首先是1個位元組長度的 lenenc-int 位,就是通過這1個位元組的值來判斷後面使用了多長位元組來存儲 column_count 值。
如果 lenenc-int 位數值小於251,則 column_count 通過1個位元組存儲;
如果 lenenc-int 位數值小於216,則 column_count 通過 1 + 2 = 3 個位元組存儲;
如果 lenenc-int 位數值小於224,則 column_count 通過 1 + 3 = 4 個位元組存儲;
如果 lenenc-int 位數值小於264,則 column_count 通過 1 + 8 = 9 個位元組存儲;
參考官方文檔的描述(地址:https://dev.mysql.com/doc/internals/en/integer.html#packet-Protocol::LengthEncodedInteger )
An integer that consumes 1, 3, 4, or 9 bytes, depending on its numeric value
To convert a number value into a length-encoded integer:
If the value is < 251, it is stored as a 1-byte integer.
If the value is ≥ 251 and < ( 216 ), it is stored as fc + 2-byte integer.
If the value is ≥ (216) and < (224), it is stored as fd + 3-byte integer.
If the value is ≥ (224) and < (264) it is stored as fe + 8-byte integer.
下麵就是解析獲得 column_count 的代碼邏輯:
lenLenenc := byte2Int(eventByte[pos : pos+1])
fmt.Println("lenenc-int: " + strconv.Itoa(lenLenenc))
var columnCnt int
if lenLenenc < 251 {
columnCnt = byte2Int(eventByte[pos : pos+1])
pos += 1
} else if lenLenenc >= 251 && lenLenenc < int(math.Pow(2, 16)) {
columnCnt = byte2Int(eventByte[pos : pos+3])
pos += 3
} else if lenLenenc >= int(math.Pow(2, 16)) && lenLenenc < int(math.Pow(2, 24)) {
columnCnt = byte2Int(eventByte[pos : pos+4])
pos += 4
} else if lenLenenc >= int(math.Pow(2, 24)) && lenLenenc < int(math.Pow(2, 64)) {
columnCnt = byte2Int(eventByte[pos : pos+9])
pos += 9
}
fmt.Println("column_count: " + strconv.Itoa(columnCnt))
接下來就是表中列的類型信息:column_type_def 。當然,在event里是不會存儲具體列類型文本,如int、char等,而是存儲的類型的“代號”,例如“0x03”代表int類型,這在 binary_log_types.h 中進行了定義。為了方便,把本文涉及到的類型放在一個map結構中,方便取用:
// column type, binary_log_types.h
var typeCol map[string]string
typeCol = make(map[string]string)
typeCol["03"] = "MYSQL_TYPE_LONG" // int
typeCol["0a"] = "MYSQL_TYPE_DATE" // date
typeCol["0f"] = "MYSQL_TYPE_VARCHAR" // varchar
typeCol["fe"] = "MYSQL_TYPE_STRING" // char
接下來位元組記錄了表的列的類型,每一列占1個位元組,總共column-count個位元組。
var columnDef []string
a := eventByte[pos : pos+columnCnt]
for i := 0; i < columnCnt; i++ {
columnDef = append(columnDef, typeCol[hex.EncodeToString(a[i:i+1])])
}
fmt.Println(columnDef) //column-def
pos += columnCnt
接下來是 column_meta_def 信息,MySQL文檔的描述是:
column_meta_def (lenenc_str) -- array of metainfo per column, length is the overall length of the metainfo-array in bytes, the length of each metainfo field is dependent on the columns field type
column_meta_def 記錄了表的列的類型的元數據(通常為列的長度和精度),有些列類型沒有元數據,有些類型有元數據,根據類型不同,有的用1個位元組記錄,有的用2個位元組記錄。
例如: MYSQL_TYPE_NEWDECIMAL(0xf6)有2個位元組的元數據,第一個位元組用於記錄長度(precision), 第二個位元組用於記錄精度(scale): decimal(8,2) meta_def = 0x0802。這個信息暫時用不到,不做詳細解析。
for _, v := range columnDef {
if v == "MYSQL_TYPE_STRING" || v == "MYSQL_TYPE_VAR_STRING" || v == "MYSQL_TYPE_VARCHAR" || v == "MYSQL_TYPE_DECIMAL" || v == "MYSQL_TYPE_NEWDECIMAL" || v == "MYSQL_TYPE_ENUM" {
pos += 2
} else if strings.Contains(v, "int") || strings.Contains(v, "bit") || v == "date" {
continue
} else if v == "MYSQL_TYPE_BLOB" || v == "MYSQL_TYPE_DOUBLE" || v == "MYSQL_TYPE_FLOAT" {
pos += 1
}
}
接下來是 null_bitmap 信息。也就是使用bitmap格式存儲哪些列是null。計算 null_bitmap 位元組長度的公式:
文檔中是:[len=(column_count + 8) / 7]
而源碼中則是:
/*
Calculate a bitmap for the results of maybe_null() for all columns.
The bitmap is used to determine when there is a column from the master
that is not on the slave and is null and thus not in the row data during
replication.
*/
uint num_null_bytes = (m_colcnt + 7) / 8;
m_data_size += num_null_bytes;
我們表中的列的數量是4,((4 + 8) / 7) 與 ((4 + 7) / 8)值相同,因此暫時看不出差異。但是假設我們表中有24列,((24 + 8) / 7) = 4 ,而 ((4 + 7) / 8) = 3,24列需要24 bit位來存儲 null_bitmap ,所以這裡文檔中描述應該是錯誤的。這裡暫時以源碼為準。
lenNull := (columnCnt + 7) / 8
//lenNull := (columnCnt + 8) / 7
fmt.Println("null bitmap length: " + strconv.Itoa(lenNull))
bitmap := ""
for _, v := range eventByte[pos : pos+lenNull] {
bitmap = bitmap + " " + reverse(byteToBinaryString(v))
}
pos += lenNull
fmt.Println("null bitmap: " + bitmap)
接下來的4個位元組就是binlog event的checksum了,業務解析暫時用不到,這裡沒有解析。
pos += 4
我們來看一下TABLE_MAP_EVENT解析輸出:
timestamp: 1665132862 time: 2022-10-07 16:54:22 +0800 CST
event type: 19
server id: 13
event length: 54
log pos: 99087
flags: 0
table id:455
flags:1
schema name length: 4
schema name: test
table name length: 4
table name: test
lenenc-int: 4
column_count: 4
columnDef: [MYSQL_TYPE_LONG MYSQL_TYPE_STRING MYSQL_TYPE_VARCHAR MYSQL_TYPE_DATE]
null bitmap length: 1
null bitmap: 00000000
checksum: [14 197 3 243]
到了這裡,整個 TABLE_MAP_EVENT 就解析結束了。
UPDATE_ROWS_EVENT的event body
接下來是 UPDATE_ROWS_EVENT的解析。
參看文檔地址:https://dev.mysql.com/doc/internals/en/rows-event.html
增加一個新的判斷邏輯,判斷event是不是 UPDATE_ROWS_EVENT 類型:
//UPDATE_ROWS_EVENT Payload
if eventType == 31 {
......
}
UPDATE_ROWS_EVENT的 event body 的前6個位元組同樣是table id。這裡可以參考MySQL源碼:log_event.cc:11591,Rows_log_event::write_data_header。
fmt.Println("table id: " + strconv.Itoa(byte2Int(eventByte[pos:pos+6]))) // table id
pos += 6
接下來是2個位元組的flags和2個位元組的extra-data-length。
fmt.Println("flags: " + strconv.Itoa(byte2Int(eventByte[pos:pos+2]))) // flags
pos += 2
pos += 2 // extra-data-length
接下來是列的數量 number of columns 。這是變長的,我們的測試表只有4列,所以只需要1個位元組就可以存儲。為了代碼簡單,沒有考慮更多列的情況,
fmt.Println("columns: " + strconv.Itoa(byte2Int(eventByte[pos:pos+1]))) // columns
cols := (byte2Int(eventByte[pos:pos+1]) + 7) / 8
pos += 1
接下來就是兩個同樣長度的 columns-present-bitmap ,因為這是 UPDATE_ROWS_EVENT ,會存儲修改前和修改後的列的數據,所以需要兩個 columns-present-bitmap 來顯示列是否為空。columns-present-bitmap1 是展示修改前是否用到的列,columns-present-bitmap2 是展示修改後是否用到的列。
fmt.Println("columns-present-bitmap1: " + bytesToBinaryString(eventByte[pos:pos+cols])) // columns-present-bitmap1
pos += cols
fmt.Println("columns-present-bitmap2: " + bytesToBinaryString(eventByte[pos:pos+cols])) // columns-present-bitmap2
pos += cols
如果執行代碼:這兩行會輸出:
columns-present-bitmap1: [11111111]
columns-present-bitmap2: [11111111]
各列的bit位都是1,說明修改語句涉及了所有列。因為只有4列,剩餘bit位沒有用到,用1填充。
接下來也就是真正存儲的行數據了:rows。修改前的數據和修改後的數據。
首先是修改前數據的 nul-bitmap ,也就是更新涉及的那些列的值是null。文檔描述它的長度是:nul-bitmap, length (bits set in 'columns-present-bitmap1'+7)/8
bits := ((len(eventByte[pos:pos+cols]) * 8) + 7) / 8
bitmap1 := ""
for _, v := range eventByte[pos : pos+bits] {
bitmap1 = bitmap1 + " " + reverse(byteToBinaryString(v))
}
fmt.Println("null bitmap1:" + bitmap1)
pos += bits
因為所有列都不為空,為了代碼邏輯簡單,就不在後面解析列數據判斷 nul-bitmap 了,實際上是需要判斷對應列是否為空再解析。
接下來是第一列數據,從 TABLE_MAP_EVENT 解析的結果看,第一列是int類型,所以使用固定長度4個位元組。這裡沒有判斷 TABLE_MAP_EVENT 中列類型信息,實際是需要的,同樣為了代碼簡潔,直接解析。
fmt.Println("col1: " + strconv.Itoa(byte2Int(eventByte[pos:pos+4])))
pos += 4
接下來是第二列,數據類型是char類型,需要1個位元組來存儲長度信息(超過255,則需要2個位元組,這是從 TABLE_MAP_EVENT 中的 column_meta_def 信息得來,這裡沒有考慮)。
len2 := byte2Int(eventByte[pos : pos+1])
pos += 1
fmt.Println("col2: " + bytesToString(eventByte[pos:pos+len2]))
pos += len2
接下來是第三列,數據類型是varchar,同樣是變長欄位,這裡以為數據較小,使用1個位元組存儲長度。
len3 := byte2Int(eventByte[pos : pos+1])
pos += 1
fmt.Println("col3: " + bytesToString(eventByte[pos:pos+len3]))
pos += len3
接下來是第四列,數據類型是date,使用固定3個位元組存儲。位元組十六進位輸出:0x4a280f,因為是小端位元組序存儲,所以解析順序應該是0x0f284a,轉換為二進位:000011110010100001001010。從左至右,前15位表示年份,所以,year='000011110010100'=1940,接下來4位表示月份,所以,month='0010'=2,後5位表示日誌,所以,day='01010'=10。最終解析到日期值:1940-2-10
x1 := reverse(eventByte[pos : pos+3])
dateByte1 := []byte(bytesToBinaryString(x1))
fmt.Println("col4: " + strconv.Itoa(str2DEC(string(dateByte1[:15]))) + "-" + strconv.Itoa(str2DEC(string(dateByte1[15:19]))) + "-" + strconv.Itoa(str2DEC(string(dateByte1[19:]))))
pos += 3
修改前的rows解析完成,下麵是修改後的rows。
邏輯與解析修改前數據邏輯一樣,開始是 nul-bitmap ,其後是各列數據,解析邏輯與修改前數據解析邏輯一致,不再重覆。
最後,是4個位元組的checksum。
我們來看一下解析輸出:
timestamp: 1665132862 time: 2022-10-07 16:54:22 +0800 CST
event type: 31
server id: 13
event length: 84
log pos: 99171
flags: 0
table id: 455
flags: 1
---------------------------------------
columns: 4
columns-present-bitmap1: 11111111
columns-present-bitmap2: 11111111
before VALUES: ------------------------------------
null bitmap1: 00001111
col1: 2
col2: Jerry
col3: Hollywood
col4: 1940-2-10
after VALUES: ------------------------------------
null bitmap2: 00001111
col1: 2
col2: Jerry
col3: Hollywood
col4: 1940-2-11
checksum: [239 244 199 92]
至此我們完成了 UPDATE_ROWS_EVENT 類型 event 的解析。
總結一下
之所以選擇 TABLE_MAP_EVENT 和 UPDATE_ROWS_EVENT 兩個event 來解析,是因為 TABLE_MAP_EVENT 中包含 ROWS_EVENT 需要的表結構信息,包括列類型,類元數據信息。選擇 UPDATE_ROWS_EVENT ,是因為 UPDATE_ROWS_EVENT 是 ROWS_EVENT 中相對比較複雜的event,WRITE_ROWS_EVENT 和 DELETE_ROWS_EVENT 相對要簡單得多。
解析涉及到的數據類型比較少,只有 int、char、varchar、date四種,除了date略顯複雜外,其它三個比較簡單。更多的數據類型沒有涉及。
從解析過程中來看,MySQL的邏輯日誌還是很大的,包含了很多額外數據,一條DML語句會產生五個event,這也是為什麼開啟binlog為什麼會很容易達到IO瓶頸或者網路瓶頸,MySQL為此也使用了組提交來進行優化。
這可能是寫的最差的解析代碼了,毫無重用和封裝可言,這主要是為了閱讀代碼邏輯方便,能夠清晰的看出對event的解析過程。文中很多調用的函數沒有列出,主要是很占篇幅。
為了能夠得到詳細參考,這個比較差勁的代碼被上傳到了gitee:
https://gitee.com/huajiashe_byte/binlog_parse
希望通過文章的解析,binlog不再是黑盒,更好的理解 MySQL 的主從複製原理。
Enjoy GreatSQL