- 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了,业务解析暂时用不到,这里没有解析。我们来看一下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
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |