本篇博文介绍 binlog 行事件(Row Event)数据体的字节级细节 ——
这部分是实现“SQL还原器”的关键,因为真正的插入/更新/删除数据都编码在这里。


🧩 一、WRITE_ROWS_EVENT 的完整结构

ROW 格式 下,MySQL 使用三个核心事件:

  • WRITE_ROWS_EVENT(插入)
  • UPDATE_ROWS_EVENT(更新)
  • DELETE_ROWS_EVENT(删除)

其中 WRITE_ROWS_EVENT 的结构如下(以 MySQL 5.7+/8.0 为例):

┌────────────────────────────────────────────┐
│ Event Header (19 bytes)                   │
├────────────────────────────────────────────┤
│ Table ID (6 bytes)                        │
│ Flags (2 bytes)                           │
│ [Extra Data Length (2 bytes)] (>=5.6.2)   │
│ [Extra Data (variable)]                   │
│ Columns Count (LenEncInt)                 │
│ Columns-used Bitmap1 (n bytes)            │
│ [Columns-used Bitmap2 (for UPDATE only)]  │
│ Row Data (variable)                       │
└────────────────────────────────────────────┘

🧱 二、Row Data(行数据)结构

行数据部分才是真正存放每一行的列值。

每一行的结构如下:

┌────────────────────────────────────────────┐
│ NULL-Bitmap (⌈column_count / 8⌉ bytes)     │
│ Column Values (for each column)            │
└────────────────────────────────────────────┘

1️⃣ NULL Bitmap

  • 每一列对应1个bit(从低位到高位)。
  • 1 表示该列值为 NULL0 表示有值。

2️⃣ Column Values

每列的实际数据根据 TABLE_MAP_EVENT 提供的字段类型(column type)决定。


🧩 三、常见列类型编码方式(简化版)

MySQL类型

存储字节

说明

TINYINT

1

有符号/无符号

SMALLINT

2

little-endian

INT

4

little-endian

BIGINT

8

little-endian

VARCHAR(n)

1或2+数据

前缀为长度

CHAR(n)

n

定长

DATETIME2

5+

复合字段:年月日时分秒(内部压缩格式)

DECIMAL

变长

编码复杂(需解码整数/小数部分)


🧮 四、示例:单行 INSERT 的字节图示

假设有表:

CREATE TABLE user (
  id INT PRIMARY KEY,
  name VARCHAR(10),
  age TINYINT,
  note VARCHAR(20)
);

执行:

INSERT INTO user VALUES (1, 'Alice', 23, NULL);

在 binlog 中(ROW 格式)被序列化为:

┌────────────────────────────────────────────┐
│ Table ID = 0x000000000001 (6B)             │
│ Flags = 0x0000 (2B)                        │
│ Columns Count = 4 (LenEncInt=0x04)         │
│ Columns-used Bitmap = 0x0F (00001111b)     │
│ NULL Bitmap = 0x08 (00001000b)             │
│ Column Values:                             │
│   id   = 01 00 00 00                       │  INT=1
│   name = 05 41 6C 69 63 65                 │  len=5, "Alice"
│   age  = 17                                │  TINYINT=23
│   note = (无, 因为NULL位为1)               │
└────────────────────────────────────────────┘

解析逻辑:

字节序列

含义

0x04

列数4

0x0F

四列都被使用

0x08

第4列为NULL

01 00 00 00

id = 1

05 41 6C 69 63 65

name = “Alice”

17

age = 23

(跳过note)

note = NULL


🧠 五、Python伪代码示例:解析行数据

import struct

def parse_row(data, column_types):
    """解析一行row data"""
    pos = 0
    num_cols = len(column_types)
    null_bitmap_size = (num_cols + 7) // 8

    null_bitmap = data[pos:pos+null_bitmap_size]
    pos += null_bitmap_size

    values = []
    for i, col_type in enumerate(column_types):
        is_null = (null_bitmap[i // 8] >> (i % 8)) & 1
        if is_null:
            values.append(None)
            continue

        if col_type == 'INT':
            val = struct.unpack_from('<I', data, pos)[0]
            pos += 4
        elif col_type == 'TINY':
            val = struct.unpack_from('<b', data, pos)[0]
            pos += 1
        elif col_type == 'VARCHAR':
            length = data[pos]
            pos += 1
            val = data[pos:pos+length].decode()
            pos += length
        else:
            raise NotImplementedError(f"类型 {col_type} 未实现")
        values.append(val)

    return values

给定:

column_types = ['INT', 'VARCHAR', 'TINY', 'VARCHAR']
row_data = bytes.fromhex('08 01 00 00 00 05 41 6C 69 63 65 17')
print(parse_row(row_data, column_types))

输出:

[1, 'Alice', 23, None]

🧰 六、从Row数据还原SQL

最后拼接成 SQL:

INSERT INTO user (id, name, age, note)
VALUES (1, 'Alice', 23, NULL);

✅ 总结

模块

内容

表映射

TABLE_MAP_EVENT

行事件结构

Header + TableID + ColumnsUsed + RowsData

行数据

NULL位图 + 列值序列

列值编码

按MySQL类型定义编码(变长、整型、小数)

还原SQL

需要表结构 + 解码列值