首先,我们知道MySQL本身就带有replication的机制,我们需要伪造一个slave,向master注册,这样的话master才会发送binlog event。注册很简单,通过调用limysql.so中的cli_advanced_command(),指定binlog filename+position,向master发送COM_BINLOG_DUMP命令。在发送dump命令的时候,我们可以指定flag为BINLOG_DUMP_NON_BLOCK,这样master在没有可以发送的binlog event之后,就会返回一个EOF的包。数据包的具体格式如下:
例如COM_BINLOG_DUMP类型的数据包payload(数据包体)是这样的:
3.1.2 接收事件
通过调用libmysql.so库中的cli_safe_read()函数,获得master发过来的数据,每次获得一个事件记录的数据,cli_safe_read()的返回值标示了从master发送过来的数据的数据字节数。而发送过来的数据保存在mysql->net->read_pos数组中。
3.2 Binlog事件
通过调用libmysql.so库中的cli_safe_read()函数可以获取一次binlog事件。
MySQL的Binlog事件类型有27种,在MySQL5.6之后增加到38种,但是我们只介绍与ROW模式相关的事件,所有的event都含有如下通用的事件结构:
+===============================+
| event header |
+===============================+
| event data |
+===============================+
分别为事件头和事件体组成,而事件的内部结构随MySQL版本的不同而变化着,我们需要用到的版本为v4,用于mysql5.1及以上,其他版本就不做介绍了,想要了解的朋友可以参考官方文档。下图为v4版本的event header格式。
由于各个事件的事件头一致,这里我们就不重复介绍了,后面各个事件我们也将忽略对事件头字段的描述。
3.2.1 ROTATE_EVENT
ROTATE_EVENT, 记录了接下来要请求的binlog的信息。格式如下:
它里面其实就是标明下一个event所在的binlog filename和position。这里需要注意的是,当slave发送binlog dump之后,master会首先发送一个ROTATE_EVENT,用来告知slave下一个event所在的位置,然后才跟着其他事件。
3.2.2 QUERY_EVENT
QUERY_EVENT, 存储的是SQL,主要是一些与数据无关的操作,如begin,alter table,drop table,create table等。格式如下
3.2.3 TABLE_MAP_EVENT
TABLE_MAP_EVENT,记录了某个table所对应的表信息,在其中存储了数据库名和表名等。格式如下:
在这个事件中我们需要注意的是,虽然我们可以用表的id标识符table_id来代表一个特定的表,但是因为alter table或rotate binlog event等原因,master会改变某个table的table_id,所以我们在外部不能使用这个table_id来索引某个table。
还需要关注的就是里面的column meta信息,后续我们解析ROW_EVENT的时候会根据这个来处理不同类型的数据。
3.2.4 ROWS_EVENT
ROWS_EVENT,包含了insert,update,以及delete三种event,并且有v0,v1,v2三个版本。ROWS_EVENT的基本格式如下:
在这里MySQL5.6开始event的type发生变化了,所以MySQL5.6需要改一下,
所以为了我们软件的版本兼容性,我们需要在软件中支持不同的版本。
MySQL5.1~MySQL5.6
· TABLE_MAP_EVENT:19
· WRITE_ROWS_EVENT: 23
· UPDATE_ROWS_EVENT: 24
· DELETE_ROWS_EVENT: 25
MySQL5.6以上
· TABLE_MAP_EVENT:19
· WRITE_ROWS_EVENT: 30
· UPDATE_ROWS_EVENT: 31
· DELETE_ROWS_EVENT: 32
ROWS_EVENT的table_id跟TABLE_MAP_EVENT一样,虽然table_id可能变化,但是ROWS_EVENT和TABLE_MAP_EVENT的table_id是能保证一致的,所以我们也是通过这个来找到TABLE_MAP_EVENT的。
为了节省空间,ROWS_EVENT里面对于各列状态都是采用bitmap的方式来处理的。首先我们需要得到column present bitmap的数据,这个值用来表示当前列的一些状态,如果没有设置,也就是某列对应的bit为0,表名该ROWS_EVENT里该列的数据,外部用null代替就可以了。
然后就是每个record里的bitmap,这个是用来表明一行实际的数据里面有哪些列是NULL的,如果该列为NULL,则为1。
在得到column_bitmap和null_bitmap后,我们就可以实际解析这行对应的数据了,对于每一列,首先判断是否column_bitmap标记了,如果为0,这跳过用null表示,然后再看是否null_bitmap里面标记了,如果为1,则表明为null。
但是,因为我们得到的是一行数据的二进制流,又如何将一列数据解析出来呢,这里就需要靠TABLE_MAP_EVENT里的column_meta了。Column_def中定义了该列的数据类型,对于一些特定的类型,譬如MYSQL_TYPE_LONG,MYSQL_TYPE_TINY等,长度都是固定的,所以我们可以直接读取对应长度的数据得到实际的值,但对于一些类型,则没有这么简单。这时候就需要通过meta来辅助计算了。举个例子:
MYSQL_TYPE_BLOB类型,meta为1表示的为tinyblob,第一个字节解释blob的长度,2表名的是shortblob,前两个字节为blob的长度等,而对于MYSQL_TYPE_VARCHAR,meta存储的则是string的长度,下一小节我们介绍每一种类型的计算方法。
3.2.5 XID_EVENT
XID_EVENT一般出现在一个事务操作之后或者其他语句提交之后。它的主要作用提交事务操作和把事件刷新至binlog文件中。里面存的是8个字节的事务ID号。
3.3 不同的数据类型的长度
上一节我们介绍了在不同事件的事件格式以及需要注意的事项,在其中ROWS_EVENT事件中,在record中bit_map之后的列数据中,针对不同的数据类型,可能在record中占用不同的字节,因从需要针对每种数据类型进行处理,
下面列出大部分常用数据类型的字节数和解析方法:
3.3.1 MYSQL_TYPE_INT家族
3.3.2 MYSQL_TYPE_DOUBLE家族
MYSQL_TYPE_NEWDECIMAL类型
声明语法为decimal(M,D),decimal参量的取值范围如下:
M为数字的最大数(精度),其范围为1~65,默认为10
D是小数点后面的数目(标度)。范围为0~30,但不得超过M
table_map_event中的metadata为2个字节,它会记录该类型的精度和标度,其中第一个字节表示精度,第二个字节表示精度。那么数据的长度是如何计算的呢?
比如说decimal(15,5)a=1234567890.12345,那么a的精度为15,标度为5,所以a的整数位最多为10,mysql中每9个数字用一个32位4个字节的整形来存储,剩下不够9个字节的按照如下方式:
intdig2byte[10] = {0,1,1,2,3,3,3,4,4,4};
该数组表示剩下几个字节的数字用几个字节存储。为什么这样做呢,我们知道一个int型变量能存储的最大整数位2^32,是一个十位数的整数,所以最多可以表示9位数的数字。binlog中的decimal按照小数点前后分别存储,所以a的整数位需要2个int型来存储,小数后的计算方式也一样,需要3个字节存储,所以decimal(15,5)在binlog中需要11个字节来存储数据
3.3.3 MYSQL_TYPE_STRING
处理该类型数据时,需要利用metadata辅助计算,metadata的前一个字节表示类型,后一个字节表示长度,如果类型为MYSQL_TYPE_SET和MYSQL_TYPE_EUM时,数据的长度则为后一个字节的大小。如果类型为
MYSQL_TYPE_STRING,第一个字节的数字即为该类型数据的长度。
3.3.4 MYSQL_TYPE_BIT
查看mysqlcapture实现代码
3.3.5 MYSQL_TYPE_DATA家族
3.3.6 MYSQL_TYPE_BLOB家族
Metadata为1,第一个字节表示长度,metadata为2,前两个字节表示长度,metadata为3,前三个字节表示长度,metadata为4,前四个字节表示长度。
3.3.7MYSQL_TYPE_VARCHAR
如果metadata小于256,则第一个字节表示数据的长度,如果metadata大于256
则前两个字节表示数据的长度。