作者:宋利兵
来源:MySQL代码研究(mysqlcode)
00 - Table_map_log_event
Table_map_log_event是Row Format Binlog中的一种Event。它记录了一个表的元数据信息。例如数据库名,表名和字段的类型等信息。当slave执行row events时,Table_map_log_event的作用有:
- 根据其中的数据库名和表名打开slave上对应的表
- 检测slave上的表的字段类型是否和master上的表的字段类型兼容。如果不兼容,slave会报告错误。
- 将数据转换成slave上表的字段类型。但转换紧限于slave和master的字段类型是兼容的情况。
01 - Table_map_log_event的产生
Table_map_log_event以语句为单位。在语句记录第一个rows event之前,会为每一个要更新的表产生一个Table_map_log_event. 因此binlog中的Table_map_log_event有以下特点:
- Table_map_log_event在所有rows events之前
一个语句的row format events,看起来如下所示:
Query_log_event("BEGIN")
Table_map_log_event // for db1.t1
Table_map_log_event // for db1.t2
Update_rows_log_event // for db1.t1
Update_rows_log_event // for db1.t2
Xid_log_event
- 语句不产生rows event时,也不产生Table_map_log_event
这是因为在产生第一个rows event时,才会产生Table_map_log_event。这样可以保证在语句不产生任何更新时,不会记录任何event到binlog中。
- 不产生rows event的表,也可能会记录Table_map_log_event
在记录Table_map_log_event时,还不能确定一个表是否会更新数据。因此会为所有加了写锁的表产生Table_map_log_event。当看到下面的binlog内容时,不要感到奇怪:
Query_log_event("BEGIN")
Table_map_log_event // for db1.t1
Table_map_log_event // for db1.t2
Update_rows_log_event // for db1.t1
Xid_log_event
- 参考代码
sql/handler.cc中write_locked_table_maps()和binlog_log_row()。
02 - table_map_log_event的格式
table_map_log_event的格式如下图所示:
- pack_length
首先介绍一个常用的术语pack_length。它是一种数值的存储方式,在Binary Log和MySQL的通讯包中经常用到。为了减少存储和传输时占用的空间,长度字段和整形的数值采用变长的方式存储。具体算法是:
- 小于251的数值
用1个字节直接存储该数值
- 大于等于251,小于等于65535(0xFF)的数值
先用1个字节存储数值252,再用2个字节存储该数值。
- 大于65535,小于等于16777215(0xFFF)的数值
先用1个字节存储数值253,再用3个字节存储该数值。
- 大于16777215的数值
先用1个字节存储数值254,再用8个字节存储该数值。
如果大多数情况下,数值小于251,这种存储方法比较节约空间。同时,又能够支持数值很大的情况。
- 参考代码
sql-common/pack.c中的net_store_length().
- Event Header
和其他 event一样。
- table id
table id就像一个表的主键,唯一识别一个table_map_log_event。为了节省空间,rows event中不会记录数据库名和表名,只会记录一个table id。通过table id来关联到一个table_map_log_event,就可以知道是哪个表产生的。
- flags
目前未使用,内容始终为0,占2字节。
- db name length
数据库名的长度,不包含'\0'。采用pack_lenght存储。
- database name
数据库名,以'\0'结尾,因此占用的空间是db name length+1字节。
- table name length
表名长度,不包含'\0'。采用pack_lenght存储。
- table name
表名名,以'\0'结尾,因此占用的空间是table name length+1字节。
- column count
字段的个数。采用pack_lenght存储。
- column types
这里的字段类型不是SQL语句中的类型而是MySQL内部的数据类型。后边会详细介绍。每个字段的类型占一个字节,总长度等于字段的个数。类型存储的顺序是按照master上该表中字段的顺序产生的。所以如果slave上表的字段顺序变了,就会导致错误。
- metadata length
字段的元数据长度。采用pack_lenght存储。
- metadata
除了类型,字段还有长度等属性。这些属性信息存储在metadata中,后边会详细介绍。
- null flags
字段是否可以为空的属性,每个字段占一个比特。由于记录日志时是以字节为最小单位的,尾部不足1字节时,按1个字节存储。例如:有9个字段,前8个字段占一个字节,后一个字段也需要占1个字节,总共2个字节。存储顺序是:第一个字节存储master上第1-8个字段的null属性,第二个字节存储第9-16个字段的null属性,后面依次轮推。在一个字节中8个字段的null属性按字段的顺序从低位到高位排列。例如:01010101,左边第一位是第一个字段的null属性,依次轮推。代码在log_event.cc中的Table_map_log_event()中。如下图所示:
03 - table_map_log_event中的字段类型
table_map_log_event中存储的字段类型是MySQL内部类型,了解内部类型和SQL数据类型的关系,可以帮助我们更好的理解MySQL的实现。SQL数据类型和MySQL内部类型的对应关系如下图所示:
- TINYINT/BOOL
BOOL只是TINYINT的别名。
- CHAR/BINARY
CHAR和BINARY除了字符集不同,其他特征都是相同的。因此它们使用同一个内部类型,通过字符集属性来区分。BINARY(N)等价于 CHAR(N) CHARSET binary.
- VARCHAR/VARBINARY
类似于CHAR/BINARY
- ENUM/SET
这个要特别注意,ENUM/SET在Table_map_log_event中的类型和CHAR类型相同都是MYSQL_TYPE_STRING。实际的类型信息存储在metadata中。通过metadata中的类型信息可以区别这三种类型。
- TINYBLOB/BLOB/
- MEDIUMBLOB/LONGBLOB/
- TINYTEXT/TEXT/
- MEDIUMTEXT/LONGTEXT
TEXT和BLOB的关系类似于CHAR和BINARY。不同的BLOB/TEXT类型的区别只是最大长度上的区别。因此内部对所有的BLOB/TEXT字段使用同一类型表示。通过该类型的字符集属性可以区别是BLOB还是TEXT。该类型还有一个最大长度的属性,通过此属性可以区分出是TINYBLOB还是其他的BLOB。最大长度的信息存储在metadata中。
04 - 不同字段的元数据
metadata中按master上字段的顺序记录了字段的元数据(属性)信息。元数据信息有以下特点:
- 不同类型的字段可能会有不同的属性信息,长度也可能不相同。
- 有一些类型则没有属性信息。
- 任何类型的元数据信息长度总是固定的。
根据column type中的类型信息,我们就可以知道每一个类型的metadata信息。metadata中的元数据格式如下图所示:
C1的元数据占2字节,C2的类型没有元数据,因此不占空间。C3的元数据只有一字节,C4占2字节。
除了以下的类型,其他类型均没有metadata。
- MYSQL_TYPE_FLOAT
1字节,内容为:sizeof(float).
- MYSQL_TYPE_DOUBLE
1字节,内容为:sizeof(double).
- MYSQL_TYPE_NEWDECIMAL
2字节,第一个字节是精度,第二个字节存储小数位数。
- MYSQL_TYPE_BIT
2字节,第一个字节是(bit length)/8,第二个字节是:(bit length)%8。
- MYSQL_TYPE_VARCHAR
2字节,存储长度。- MYSQL_TYPE_BLOB
1字节,存储blob的最大长度,单位字节。内容为:1,2,3或4。
- MYSQL_TYPE_JSON
- MYSQL_TYPE_GOMETRY
这两个类型都是从BLOB继承来的,因此metadata内容和MYSQL_TYPE_BLOB相同。
- MYSQL_TYPE_STRING
2个字节。
- 当实际的类型是ENUM或SET时,第一个字节存储真实类型MYSQL_TYPE_ENUM或MYSQL_TYPE_SET,第二个字节存储长度。
- 当实际类型是CHAR时,用2个字节来存储长度。为了避免和ENUM/SET的第一个字节冲突,采用了特定的编码方式。
- 参考代码
sql/field.cc中的do_save_field_metadata()函数。
注意:本文的代码都是指MySQL-5.7中的代码,其他版本可能会有不一致。
推荐订阅原文作者宋利兵的公众号 MySQL代码研究