mysql binlog的row模式数据解析【转】
drc-mysql是一种支持多master 多slave的快速并行复制的解决方案,基于mysql的binlog,目前支持binlog的STATEMENT模式。为了实现drc-mysql对 ROW模式的支持,本文对此展开研究,分析了binlog的事件格式,并针对不同的数据类型进行解析。
本文的目的是为了展示如何从row模式事件中解析数据,因此事件中一些记录其他信息的字节会直接略过,感兴趣的同学可以看看log_event.h以及log_event.cc两个文件。
获取Binlog事件:
Mysql对Binlog的处理是以事件为单位的,每一次DML操作可能会产生多次事件,例如对于innodb存储引擎,会额外产生一条QUERY_EVENT(事务的begin语句)以及XID_EVENT(事务提交)。
通过调用libmysql.so库中的cli_safe_read()函数可以获取一次binlog事件:
cli_safe_read(mm); // mm类型为MYSQL*
net = &mm->net;
buf = (const char*) net->read_pos + 1;
Binlog的事件类型大约有27种,这里只介绍与ROW模式相关的事件
1) QUERY_EVENT:与STATEMENT模式处理相同,存储的是SQL,主要是一些与数据无关的操作,eg: begin、drop table;
2) TABLE_MAP_EVENT:记录了下一条事件所对应的表信息,在其中存储了数据库名和表名;
3) WRITE_ROWS_EVENT:操作类型为insert;
4) UPDATE_ROWS_EVENT:操作类型为update;
5) DELETE_ROWS_EVENT:操作类型为delete;
6) XID_EVENT, 用于标识事务提交。
在buf[EVENT_TYPE_OFFSET]中记录了事件的类型 (EVENT_TYPE_OFFSET = 4),根据其中记录的整数,对比log_event.h中的Log_event_type,可以找到相应的事件类型。
以一条insert语句为例,包含4个事件:
TABLE_MAP_EVENT
QUERY_EVENT (begin)
WRITE_ROWS_EVENT
XID_EVENT
事件时间戳:
buf[0] ~ buf[3]的四个字节,存储了执行操作前的时间戳。
事件长度:
Buf[9]开始的四个字节构成的整数,可以使用如下的方式来进行整数转换:
#define UCHAR(ptr) ((*(ptr)+256)%256)
const char *ptr = buf + 9;
unsigned int data_len = UCHAR(ptr) + (UCHAR(ptr+1)<<8) + (UCHAR(ptr+2)<<16) + (UCHAR(ptr+3)<<24);
获取数据库和表名:
由于在insert/delete/update事件中不记录表的相关信息,因此每次DML操作都会产生一个TABLE_MAP_EVENT事件,其中存储了获取数据库名和表名。
例如对于数据库名:tt0001;表名:x18,从buf[27]开始表示为如下格式:
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
6 | t | t | 0 | 0 | 0 | 1 | \0 | 3 | x | 1 | 8 | \0 |
解析数据:
在buf中记录了很多信息,但我们的目的是为了解析出数据,因此可以跳过一些字节,直接到达我们的目标数据头部。
cols = buf[27]; //在insert/delete/update事件中,buf[27]表示列的个数
bits = (cols+7)/8
对于WRITE_ROWS_EVENT、DELETE_ROWS_EVENT: ptr = buf +28+bits
对于UPDATE_ROWS_EVENT:ptr = buf +28+bits * 2
从ptr开始,记录了我们需要解析的数据。
1) UPDATE_ROWS_EVENT
Old record | New record | Old record | New record | Old record | …… |
每更新了多少行,就有多少对 old/new record,当一个事件包存储不下所有记录时,将会拆分成多个 UPDATE_ROWS_EVENT事件。
2) WRITE_ROWS_EVENT
包含一条插入的数据record
3) DELETE_ROWS_EVENT
包含被删除的数据record,格式为:
record | record | record | …… |
从上面的分析可以看出,要想从binlog中解析出数据,除了辅助信息外,关键是要从record中获取得到行数据,因为DML操作对应的事件类型,都以record为记录单位。
Record结构:
在Record的前几位,会用多个字节来表示值为NULL的列,record的结构可表示为
bit_map | Col1 | Col2 | Col3 | …… |
其中bit_map的所占字节数为(cols+7)/8
例如,执行:insert into xx values(1, NULL, NULL, 15, “ssss”);
xx表有5列,需要(cols +7)/8 = 1 个字节就可以表示所有的列
bit_map = 230,230转换为二进制:1110 0110
其中,最低位表示第一列,第cols( = 5)位为最后一列,为1表示该列值为NULL,为0表示非NULL,在随后的数据记录中只会记录非NULL的值,例如这里跳过bit_map所占字节之后,只会记录1、15和‘ssss’
再比如,当执行如下语句时:insert into x14(a,b) values (NULL,”dsda”);
这里有9列,因此需要2个字节记:
ptr[0] = -4; ptr[1] = -1
转换为二进制 (ptr[1])1111 1111 (ptr[0])1111 1100
注意,这里虽然在SQL语句中a值为NULL,但由于a列是自增类型,因此存储在binlog中的就是一个整数,而非NULL值.
对于值为NULL的列,我们可以通过表的定义得到该列的默认值。
解析不同的数据类型:
在record中bit_map之后的列数据中,针对不同的数据类型,可能在record中占用不同的字节,因此需要针对每种数据类型进行处理,为 了获取到每一列的信息,我们可以调用MYSQL的接口函数mysql_fetch_field()。这里需要注意一种特殊情形,即对于set和enum类 型,在调用该API时,会被转换为MYSQL_TYPE_STRING类型,可以调用show columns from 来得到这两种类型的定义。
这里列出了大部分常用数据类型的字节数和解析方法:
1. MYSQL_TYPE_LONG
Int类型,占用4个字节,sint4korr(ptr)
2. MYSQL_TYPE_TINY
Tinyint类型 ,占用1个字节
3. MYSQL_TYPE_SHORT
smallint 类型, 2个字节, sint2korr(ptr)
4. MYSQL_TYPE_INT24
MEDIUMINT类型,3个字节, sint3korr(ptr)
5. MYSQL_TYPE_LONGLONG
Bigint 类型,8个字节, sint8korr(ptr)
6. MYSQL_TYPE_NEWDECIMAL
Decimal类型,精度限制为65, 字节数与该类型的定义相关,可以参考用户手册 ,对该类型的解析主要是计算出其占用的字节数,调用libmysql.so库中的bin2decimal函数来实现解析。
7. MYSQL_TYPE_FLOAT、MYSQL_TYPE_DOUBLE
直接进行类型的强制转换,分别占4和8个字节,然后根据定义对输出进行精度控制。
8. MYSQL_TYPE_BIT
Bit类型,占用的字节数与其定义相关,计算方式:
byt_len = length%8==0? length/8 : (length/8 + 1);
例如,当定义为bit(M)时,length = M;将byt_len个字节中存储的数据转换为一个整数。
9. MYSQL_TYPE_SET
SET类型,定义为SET(M),M值为以下范围时:
1 ~8,1个字节
9~16, 2个字节
17~24, 3个字节
25~32, 4个字节
33~64, 8个字节
然后将相应字节内的数转换为整数即可
10. MYSQL_TYPE_ENUM
Enum类型,当该类型内的元素超过255个时,使用2个字节,否则使用1个字节表示,相应字节内转换为整数M,表示在enum中的第M个元素。
11. MYSQL_TYPE_STRING、MYSQL_TYPE_VAR_STRING、MYSQL_TYPE_BLOB
包括char()、varchar()以及text类型,其处理方式相同,在record中首先根据其定义的长度,例如:
对于varchar(10),使用一个字节记录长度;而对于varchar(300),则需要使用两个字节来记录字符串的长度;
字符串“abcdef”,在record中被记录为“6abcdef”。
12. MYSQL_TYPE_TIME
Time类型,3个字节,计算方法:
d_int = UCHAR(ptr) + (UCHAR(ptr+1)<<8) + (UCHAR(ptr+2)<<16);
例如对于‘12:01:22’,计算结果为120122
13. MYSQL_TYPE_TIMESTAMP
timestamp时间戳类型,4个字节,直接进行类型的强制转换为整数
14. MYSQL_TYPE_DATE
Date类型,3个字节,计算方法:
d_int = UCHAR(ptr) + (UCHAR(ptr+1)<<8) + (UCHAR(ptr+2)<<16)
例如:
00001111 10110111 00100001
其中,1-5位表示日期,6-9位表示月份,剩余的表示年份,因此上述date类型可转换为2011-09-01
15. MYSQL_TYPE_YEAR
Year类型,1个字节,记录年份,用一个字节记录,从1900年开始
例如,当值为112时,表示112+1900 = 2012年
16. MYSQL_TYPE_DATETIME
Datetime类型,8个字节,直接类型转换为long long,
例如对于 2011-08-27 19:32:46
计算结果值为20110827193246
其实,不管是什么数据类型,我们只要知道其占有的字节数,就能推敲出他们在文件中存储的格式。
未来可能的应用
1. 通过解析binlog中的行数据,进行增量数据dump;
2. 结合handlersocket进行replication。
http://www.taobaodba.com/html/585_mysql-binlog%E7%9A%84row%E6%A8%A1%E5%BC%8F%E6%95%B0%E6%8D%AE%E8%A7%A3%E6%9E%90.html