本篇博文介绍 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表示该列值为NULL,0表示有值。
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 |
| id = 1 |
| name = “Alice” |
| 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 | 需要表结构 + 解码列值 |
















