簡明的binlog event解析

来源:https://www.cnblogs.com/greatsql/archive/2022/10/26/16828541.html
-Advertisement-
Play Games

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 中有表中列信息(類型、非空等)。

image-20221009160414204

上圖就是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]

image-20221009160550571

而源碼中則是:

/*
  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

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • LVS: lvs是一個負載調度器,由內核集成,性能強大,支持百萬計併發。 LVS集群的相關概念: VS:虛擬伺服器,指LVS伺服器自身 RS:提供服務的伺服器 CIP:客戶端ip地址 VIP:lvs伺服器對外發佈的ip地址,用戶通過vip訪問集群 DIP:LVS連內網的ip地址叫DIP,用於接收用戶 ...
  • Docker簡介和安裝 Docker是什麼 Docker 是一個應用打包、分發、部署的工具 你也可以把它理解為一個輕量的虛擬機,它只虛擬你軟體需要的運行環境,多餘的一點都不要, 而普通虛擬機則是一個完整而龐大的系統,包含各種不管你要不要的軟體。 跟普通虛擬機的對比 | 特性 | 普通虛擬機 | Do ...
  • Ansible使用playbook部署LNMP 環境介紹: | 系統|ip|主機名|服務| | : : | : : | : : | : : | |centos8|192.168.222.250|ansible| ansinle| |ceotos8|192.168.222.137|nginx|ngin ...
  • 表在資料庫中的存儲方式。 存儲引擎只存在mysql中,(Oracle中有對應機制,但是不叫存儲引擎)。 完整的建表語句: CREATE TABLE mytable( id INT(10) PRIMARY KEY, username VARCHAR(30) NOT NULL, PASSWORD VAR ...
  • 1、什麼是事務一個事務是一個完整的業務邏輯單元,不可再分。 比如:銀行轉賬,從A賬戶向B賬務轉賬10000,需要執行兩條update語句 update t_act set balance = balance - 10000 where actno = 'act-001' ; update t_act ...
  • 創建表的時候可以給欄位添加相應的約束,約束的目的:保證表中數據的合法性,唯一性,有效性。 非空約束(not null):約束欄位不能為NULL 唯一約束(unique):約束欄位不能重覆 主鍵約束(primary key):約束欄位既不能為NULL也不能重覆 外鍵約束(foreign key):阿裡 ...
  • ①索引到底是什麼; ②索引底層的實現; ③聚簇索引是什麼?二級索引呢; ④最左首碼原則; ⑤如何設計索引,遵循的原則; ⑥索引相關語法; ...
  • 摘要:T+0查詢是指實時數據查詢,數據查詢統計時將涉及到最新產生的數據。 本文分享自華為雲社區《大數據解決方案:解決T+0問題》,作者: 小虛竹 。 T+0問題 T+0查詢是指實時數據查詢,數據查詢統計時將涉及到最新產生的數據。在數據量不大時,T+0很容易完成,直接基於生產資料庫查詢就可以了。但是, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...