MySQL网络协议
MySQL目前有五种连接方式,分别是TCP/IP,TLS/SSL,Unix Sockets,Shared Memory,Named pipes.
方式 | 默认开启 | 支持系统 | 只支持本机 | 如何开启 | 参数配置 |
---|---|---|---|---|---|
TCP/IP | 是 | 所有系统 | 否 | –skip-networking=yes/no. | –port –bind-address |
TLS/SSL | 是 | 所有系统(基于TCP/IP)之上 | 否 | –ssl=yes/no. | –ssl-* options |
Unix Sockets | 是 | 类Unix系统 | 是 | 设置–socket= 来关闭. | –socket=socket path |
Shared Memory | 否 | Windows系统 | 是 | –shared-memory=on/off. | –shared-memory-base-name= |
Named pipes | 否 | Windows系统 | 否 | –enable-named-pipe=on/off. | –socket= |
Unix Sockets和Shared Memory都是只支持本机的,所以如果你的程序和数据库是在一台机子的,就可以考虑这两种协议。
MySQL Handshake Packet
MySQL通信过程中数据包。
用户发送CRUD操作给数据库的数据包过程:
1.首先Client先与Server通过TCP三次握手建立连接
2.建立连接之后,Server发送Handshake数据包给Client,Handshake含有数据库的版本、协议版本、用于后期加密的Salt等。
3.Client接收到Server的Handshake包之后,解析其中数据,最主要是获得Salt,然后把用户名、密码(利用salt+sha1进行加密)、schema(要操作的数据库名)等信息打包成一个AuthPacket,用来认证请求。
4.Server接受到AuthPacket之后,对其的身份进行验证,验证成功发送一个OK Packet。否则发送Error Packet。
5.Client接受到OK Packet之后就发送Query Packet给Server。
6.Server返回结果查询的,将Result Packet返回给Client。
7.Client解析Result Packet完毕,发送Request Quit。
8.四次握手断开连接。
客户端发送Query请求,可以看到WireShark捕获的包。
请求SQL: select name form paper
1.首先看第一框中的三次握手,服务端与客户端先是建立连接。
2.服务端发送 Server Greeting proto=10 version=8.0.12 这个包就是HandShake。
3.Login Requset user=root db=data是客户端发送的Auth Packet。
4.服务端认证客户端的Auth Packet之后,返回一个OK Packet
5.可以看到我们只发送了一次请求:select name form paper
,但是客户端居然有三次Request Query,实际上第一次的Query是客户端发送set names uft-8
的请求到服务端,指定了客户端和服务器之间传递字符的编码规则为UTF8。
第二次的Request Query是set autocommit=0
,表示取消自动提交事务。每个SQL语句所在的事务都需要显示的“commit”才能提交事务。如果你想明确地执行事务,需要禁用自动提交模式并告诉MySQL你想让它在何时提交或回滚有关的修改。
第三次的Request Query才是我们的SQL请求。
6.服务端接受并解析了我们的请求,把结果放在Response Packet传回来。
功能界面中我们还可以看到物理层、数据链路层、网络层、传输层,每一层都套自己对应协议的数据头。这块不是我们要研究的重点,重点是应用层上的数据。因为图中我选择的是一个TCP包,所以没能看到应用层。
当客户端发送SQL请求到Server端时,产生了一系列Packet。第一个Packet就是下面要介绍的HandShake。
Handshake Packet格式
Handshake packet是由Server向Client发送的初始化包,因为所有从Server向Client端发送的包都是一样的格式,前面的四个字节是包头,其中前三位代表Handshake packet数据长度,第四位是包序列号。下面是Handshake packet具体内容的格式:
相对包内容的位置 | 长度(字节) | 名称 | 描述 |
---|---|---|---|
0 | 1 | 协议版本 | 协议版本的版本号,通常为10(0x0A) |
1 | len = strlen (server_version) + 1 | 数据库版本 | 长度为数据库版本字符串的长度+上标示结束的的一个字节 |
len + 1 | 4 | 线程ID | 此次连接MySQL Server启动的线程ID |
len + 5 | 8 + 1(0x00表示结束) | 挑战随机数(第一部分) | 用于后续账户密码验证 |
len + 14 | 2 | 数据库提供的功能 | 用于与客户端协商通讯方式 |
len + 16 | 1 | 编码格式 | 标识数据库目前的编码方式 |
len + 17 | 2 | 服务器状态 | 用于表示服务器状态,比如是否是事务模式或者自动提交模式 |
len + 19 | 2 | 扩展的协议,数据库提供的功能 | 用于与客户端协商通讯方式 |
len + 21 | 1 | Authentication Plugin length | 身份验证插件长度 |
len + 22 | 10 | 保留字节 | 未来可能会用到,预留字节 |
len + 32 | 12 + 1(0x00表示结束) | 挑战随机数(第二部分) | 用于后续账户密码验证 |
Mysql 5.x保留字节是13位。MySQL是8.x的,包做了点修改,即len+19和len+21位的使用保留字节,所以现在只剩下10个预留字节了。
WireShark分析HandShake Packet
上面的数据格式的描述可能不够具体,所以这里用WireShark来捕获Handshake看看数据是否真如上面所描述的。
点击Server Greeting proto=10 version=8.0.12
可以看到从Frame物理层到MySQL Protocol
应用层各层的具体内容。点击MySQL Protocol
可以看到具体的十六进制的内容,以及它的ASCII码。
其中Packet Length
和Packet Number
就是包的数据头,从上面我们知道,Packet Length
是占三个字节的。那么就是说0x00004a表示的就是数据长度,转为十进制发现就是74。
这里有人可能会有疑惑,为什么不是0x4a0000?这是因为MySQL采用的是小端存储形式。因为小端存储方式,计算机计算会更加方便。大端存储在判定正负更方便,符号位固定为第一个字节。
这里再解释一下Version这个字段,根据上面的描述,我们知道它的长度为数据库版本字符串的长度+上标示结束的一个字节。就是说实际上存储的时候还有一个0x00的字节跟着version字段表示结束符。你会发现有好多这种字段跟上结束符的数据,所以在填充数据的时候,需要把0x00也一起填上。
Auth Packet 客户端认证包
Auth Packet主要是发送用户的信息、密码、Schema等,其中密码需要HandShake中Salt。所以在发送Auth Packet,需要先解析出Salt,然后对密码进行加密,之后再一起打包发送。
Auth Packet数据格式
与HandShake相同,前面的四个字节是包头,其中前三位代表Handshake packet数据长度,第四位是包序列号。
以下是Auth四字节后的内容。
相对包内容的位置 | 长度 | (字节) | 名称 描述 |
---|---|---|---|
0 | 2 | 协议协商 | 用于与服务端协商通讯方式 |
2 | 2 | 扩展的协议 | 用于与服务端协商通讯方式 |
4 | 4 | 消息最长长度 | 客户端可以发送或接收的最长长度,0表示不做任何限制 |
8 | 1 | 字符编码 | 客服端字符编码方式 |
9 | 23 | 保留字节 | 未来可能会用到,预留字节,用0代替 |
32 | 不定长 | 认证字符串 | 用户名:NullTerminatedString格式编码 |
不定 | 不定长 | 认证密码 | 加密后的密码:LengthEncodedString格式编码 |
不定 | 不定长 | 数据库名称 | NullTerminatedString格式编码 |
NullTerminatedString(Null结尾方式): 字符串以遇到Null作为结束标志,相应的字节为0x00
LengthEncodedString(指定字符串长度方式)
Mysql 5.x协议协商是4个字节的,MySQL8.x的协议协商被拆成了两部分。用两个字节表示多出来了扩展的协议。
Wireshark分析Auth Packet
用WireShark来分析一下Auth Packet的数据格式。点击Login Request user=root db=data
可以看到Auth包的具体内容。
这里着重解释一下Password,首先这个数据长度是指定的,即图中显示为20个字节。而这个加密的密码是根据HandShake中的两个Salt来的,HandShake中第一个Salt是8位的,第二个Salt是12位的。根据两个Salt最后得到加密后的密码。
这部分内容是由客户端自己生成,所以说如果我们如果要写一个程序连接数据库,那么这个包就得按照这个格式,不然服务端将会无法识别。
加密方式
生成Auth Packet的过程中,需要将密码加密再发送。MySQL4.1x之前的版本采用的是323加密方式,4.1.x之后采用了411的加密方式。这块加密还不够熟悉,所以后面需要补一下。不过在网上可以找到对应的代码。
1 | package mysql |
2 | |
3 | import ( |
4 | "crypto/sha1" |
5 | "math" |
6 | ) |
7 | |
8 | const SCRAMBLE_LENGTH_323 = 8 |
9 | |
10 | // Random struct, see libmysql/password.c |
11 | type randStruct struct { |
12 | maxValue uint32 |
13 | maxValueDbl float64 |
14 | seed1 uint32 |
15 | seed2 uint32 |
16 | } |
17 | |
18 | // Initialise rand struct, see libmysql/password.c |
19 | func randominit(seed1, seed2 uint32) *randStruct { |
20 | return &randStruct{ |
21 | maxValue: 0x3FFFFFFF, |
22 | maxValueDbl: 0x3FFFFFFF, |
23 | seed1: seed1 % 0x3FFFFFFF, |
24 | seed2: seed2 % 0x3FFFFFFF, |
25 | } |
26 | } |
27 | |
28 | // Generate a random number, see libmysql/password.c |
29 | func (r *randStruct) myRnd() float64 { |
30 | r.seed1 = (r.seed1*3 + r.seed2) % r.maxValue |
31 | r.seed2 = (r.seed1 + r.seed2 + 33) % r.maxValue |
32 | return float64(r.seed1) / r.maxValueDbl |
33 | } |
34 | |
35 | // Password hash used in pre-4.1, see libmysql/password.c |
36 | func hashPassword(password []byte) []uint32 { |
37 | nr := uint32(1345345333) |
38 | add := uint32(7) |
39 | nr2 := uint32(0x12345671) |
40 | for i := 0; i < len(password); i++ { |
41 | if password[i] == ' ' || password[i] == '\t' { |
42 | continue |
43 | } |
44 | tmp := uint32(password[i]) |
45 | nr ^= (((nr & 63) + add) * tmp) + (nr << 8) |
46 | nr2 += (nr2 << 8) ^ nr |
47 | add += tmp |
48 | } |
49 | result := make([]uint32, 2) |
50 | result[0] = nr & ((1 << 31) - 1) |
51 | result[1] = nr2 & ((1 << 31) - 1) |
52 | return result |
53 | } |
54 | |
55 | // Encrypt password the pre-4.1 way, see libmysql/password.c |
56 | func scramble323(message, password []byte) (result []byte) { |
57 | if len(password) == 0 { |
58 | return |
59 | } |
60 | // Check message is no longer than max length |
61 | if len(message) > SCRAMBLE_LENGTH_323 { |
62 | message = message[:SCRAMBLE_LENGTH_323] |
63 | } |
64 | // Generate hashes |
65 | hashPass := hashPassword(password) |
66 | hashMessage := hashPassword(message) |
67 | // Initialise random struct |
68 | rand := randominit(hashPass[0]^hashMessage[0], hashPass[1]^hashMessage[1]) |
69 | // Generate result |
70 | result = make([]byte, SCRAMBLE_LENGTH_323) |
71 | for i := 0; i < SCRAMBLE_LENGTH_323; i++ { |
72 | result[i] = byte(math.Floor(rand.myRnd()*31) + 64) |
73 | } |
74 | extra := byte(math.Floor(rand.myRnd() * 31)) |
75 | for i := 0; i < SCRAMBLE_LENGTH_323; i++ { |
76 | result[i] ^= extra |
77 | } |
78 | return |
79 | } |
80 | |
81 | // Encrypt password using 4.1+ method |
82 | func scramble41(message, password []byte) (result []byte) { |
83 | if len(password) == 0 { |
84 | return |
85 | } |
86 | // stage1_hash = SHA1(password) |
87 | // SHA1 encode |
88 | crypt := sha1.New() |
89 | crypt.Write(password) |
90 | stg1Hash := crypt.Sum(nil) |
91 | // token = SHA1(SHA1(stage1_hash), scramble) XOR stage1_hash |
92 | // SHA1 encode again |
93 | crypt.Reset() |
94 | crypt.Write(stg1Hash) |
95 | stg2Hash := crypt.Sum(nil) |
96 | // SHA1 2nd hash and scramble |
97 | crypt.Reset() |
98 | crypt.Write(message) |
99 | crypt.Write(stg2Hash) |
100 | stg3Hash := crypt.Sum(nil) |
101 | // XOR with first hash |
102 | result = make([]byte, 20) |
103 | for i := range result { |
104 | result[i] = stg3Hash[i] ^ stg1Hash[i] |
105 | } |
106 | return |
107 | } |
MySQL协议OK/Error包解析
OK/Error Packet是由Server端向Client发送的请求回应。下面主要来解析一下这两个Packet,以及解释一下为什么Wireshark抓到的包与网上的格式有所差别。
OK Packet 数据格式
普通的OK包会在以下几种情况下产生,由Server发送给相应的接收方:
- COM_PING: 连接或者测试数据库
- COM_QUERY: 不需要查询结果集的操作,比如INSERT, UPDATE, or ALTER TABLE
- COM_REFRESH: 数据刷新
- COM_REGISTER_SLAVE: 注册从服务器
首先前面的四个字节是包头,其中前三位代表packet数据长度,第四位是包序列号。以下是OK Packet的四字节后的内容。
相对包内容的位置 | 长度(字节) | 名称 | 描述 |
---|---|---|---|
0 | 1 | 包头标识 | 0x00 代表这是一个OK 包 |
1 | rows_len | 影响行数 | 相应操作影响的行数,比如一个Update操作的记录是5条,那么这个值就为5 |
1 + rows_len | id_len | 自增id | 插入一条记录时,如果是自增id的话,返回的id值 |
1 + rows_len + id_len | 2 | 服务器状态 | 用于表示服务器状态,比如是否是事务模式或者自动提交模式 |
3 + rows_len + id_len | 2 | 警告数 | 上次命令引起的警告数 |
5 + rows_len + id_len | msg_len | 额外信息 | 此次操作的一些额外信息 |
初看这个格式,你们会发现你们的OK包和上面的数据格式是对不上的。用WireShark来解析一下。
第一个点就是包头标志。
用Wireshark分析的时候,可以看到MySQL Protocol
中没有包头标志这个数据的。但是实际上你看具体的16进制数据会发现,Packet Number
和 Affected Rows
之间还有0x00
就是我框出来的00
。所以数据还是有的,只是MySQL Protocol
没有显示而已。
第二个点是是自增id。
在上面的包中也没有显示,但是在下面具体数据中可以看到Affected Rows和Server status还有一个0x00来存放自增长ID。当我们执行插入语句时就会发现这个字段就可以看到了。
第三个点是额外信息。
在执行更新操作时,就可以看到额外信息了。
宗上面的分析,OK Packet
的格式是能对应上的,只是根据不同SQL请求,字段的数据才会显示出来。
Error Packet数据格式
同样下面是第四字节后具体内容。
相对包内容的位置 | 长度(字节) | 名称 | 描述 |
---|---|---|---|
0 | 1 | 包头标识 | 0xFF 代表这是一个Error 包 |
1 | 2 | 错误代码 | 该错误的相应错误代码 |
3 | 1 | 标识位 | SQL执行状态标识位,用’#’进行标识 |
4 | 5 | 执行状态 | SQL的具体执行状态 |
9 | msg_len | 错误信息 | 具体的错误信息 |
Wireshark分析的界面如下,最重要的信息是错误msg。
MySQL协议Result Set包解析
当执行了INSERT、UPDATE、ALTER TABLE
操作时,会生成对应的OK/Error
包。接下来要说明的是SELECT
结果Result Set
的解析。Result Set
会涉及到了多个不同结构的包,有Column_Count、Column_Def、EOF、Row
。
Result Set包简析
用一张图片来简单解析一下每个包的具体作用。
从上图来看,客户端发送一个select的com_query包之后,DB会按照下列步骤返回:
1.返回一个
Protocol::LengthEncodedInteger
,其中数据为column_count
.(该表的显示返回中字段的个数)
2.接下来会跟column_count
个Protocol::ColumnDefinition
包(对每一个字段的说明).
3.再读取一个eof
包表示ColumnDefinition
包流结束.
4.读取n个Row包,Row含有请求的数据值。
5.如果读到任何一个error包之后,从此读取结束,抛出错误.
6.或者你读取到了第二个eof包,按照正常顺序这里就会结束了.但是如果
eof
包中的status & SERVER_MORE_RESULT_EXISTS
不为0,表明还有ResultSet
,则返回到步骤1,开始读取下一个ResultSet
.
Column_count Packet
该包前四个字节表示的包的字节长度以及包的序列号。后面存放的是一个类型为LengthEncodeInteger
表示结果中含有几个列。Number of fields显示该返回字段有2列。
Column_def Packet
该包前四个字节表示的包的字节长度以及包的序列号。后四字节为以下内容。
相对包内容的位置 | 长度 | (字节) 名称 | 描述 |
---|---|---|---|
0 | 不定长 | catalog | 目录,通常为def |
不定长 | 不定长 | schema | 操作的数据库 |
不定长 | 不定长 | table | 操作的虚拟表名 |
不定长 | 不定长 | org_table | 操作的物理表名 |
不定长 | 不定长 | name | 虚拟列字段名 |
不定长 | 不定长 | org_name | 物理列字段名 |
不定长 | 0x0c | length of fixed-length fields | 以下字段长度 |
不定长 | 2 | character | 列字符集 |
不定长 | 4 | length | 字段最大长度 |
不定长 | 1 | type | 字段类型 |
不定长 | 2 | flags | 标志 |
不定长 | 1 | decimals | ?? |
不定长 | 2 | 预留字节数 | 预留字节数 |
以下是Wireshark对column_def的解析截图。
EOF Packet
相对包内容的位置 | 长度(字节) | 名称 | 描述 |
---|---|---|---|
0 | 1 | 包头标识 0xFE | 代表这是一个EOF 包 |
1 | 2 | 警告数 | 上次命令引起的警告数 |
3 | 2 | 服务器状态 | 服务器状态 |
以下是Wireshark的数据截图。
Row Packet
Row Data含着的是我们需要获取的数据,一个Result Set
包里面包含着多个Row Data
结构。每一个值前面有长度字节值,可以帮助我们区分开不同的列。
相对包内容的位置 | 长度(字节) | 名称 | 描述 |
---|---|---|---|
0 | 不定 | length | 这个字段的数据长度 |
不定 | 不定 | value | 这个字段的值 |
Wireshark显示Row的数据,虽然只显示了text,但是点击十六进制的数据,还是能看到length的数据。
MySQL协议Quit包
用户端想要断开连接时,需要往服务端发送一个Quit Packet。这个包比较简单,下面直接看其数据结构。
Quit Packet
首先前面的四个字节是包头,其中前三位代表packet数据长度,第四位是包序列号。以下是Quit Packet的四字节后的内容。只有一个标志位数据。
相对包内容的位置 | 长度(字节) | 名称 | 描述 |
---|---|---|---|
0 | 1 | 包头标识 0x01 | 代表这是一个Quit 包 |
下面是WireShark捕获的Quit Packet。可以看到其标识位的数据
字节位运算细节
前面主要是讲解各个MySQL协议通信过程中涉及到的Packet。下面主要是讲解如何根据Packet的结构,将需要的字节按照一定的顺序写入Packet中。
低字节转高字节存在的问题
1 | public static void main(String args[]){ |
2 | byte x = -126; |
3 | System.out.println(Integer.toBinaryString(x & 0xff)); |
4 | System.out.println(Integer.toBinaryString(x)); |
5 | } |
既然byte是八位的二进制,那和0xff
做&
运算的意义是什么呢?x&0xff
难道不还是x
吗!
的确是的,但是在做&
运算的时候0xff
表示的是一个int
类型,而x
在与int
做运算的时候,就需要做byte
到int
的一个转化。转化的过程为:
- 当byte为一个负值,会将8位的byte填充至32位,用1来填充。也就是说
byte x = -126
原本是1000 0010
,变成1111 1111 .... 1000 0010
。 - 当byte为一个正值,用0来填充,这时候是没有影响的。原本是
1000 0010
变成0000 0000... 1000 0010
x = -126
.考虑第一种情况,x
填充成1111 1111.... 1000 0010
再与0xff
做&
运算,最后变成了1000 0010
。
i & 0xff
的意义
上面的输出虽然得到了解释,但是对i & 0xff
的意义还未认识到位。首先byte x =-126
,在填充成int类型后变成了1111 1111 .... 1000 0010
,虽然二进制表示的十进制还是-126
的,但是x
在内存中存储的进制已经发生了变化。我希望就算它是被int类型填充,它的二进制还是保持为原来的。即使它的十进制不一样。
很多时候,我们要求的就是二进制唯一性。举个列子,从服务端发送的来了一个Handshake,第四位表示的一个包的序列号,用一个字节来表示。假设服务端的
packed number
为-5,我们解析这个包的同时,也需要用一个字节来保存,如果这个时候我不用字节来存,而是用一个int来存,是不是就存在上面低字节转高字节的问题。
会导致原本是-5的byte二进制1111 1011
,变成了1111 1111.... 1111 1011
。所以为了解决二进制不唯一的问题就要利用& 0xff
来避免。
所以当我们在做低位向高位转化,并且涉及到的是进制上的运算,就需要用到&0xff。
Packet Read & Write
主要用代码去实现,如何将Server端反馈回来的包,进行解析。如下图,完成三次握手之后,服务端发送了一个handshake packet。我们要做的是定义好handshake的类,以及实现将字节到类的转化。
Handshake类的实现
1 | public class HandshakePacket extends MysqlPacket { |
2 | |
3 | public byte protocolVersion;//版本协议 |
4 | public byte[] serverVersion;//版本号 |
5 | public long threadId;//执行的线程号 |
6 | public byte[] seed;//用于后期加密的salt1 |
7 | public int serverCapabilities;//通信的协议 |
8 | public byte serverCharsetIndex;//编码格式 |
9 | public int serverStatus;//服务端的状态 |
10 | public byte[] restOfScrambleBuff;//这个其实就是seed2 |
11 | } |
MysqlPacket是所有包的基础类,含有包的长度以及包的序列号。read和write是其两个抽象方法。这里主要是讲解Packet如何根据数据类型来实现read。
1 | public abstract class MysqlPacket { |
2 | public int packetLength; |
3 | public byte packetId; |
4 | |
5 | public abstract void read(byte[] data); |
6 | public abstract void write(ByteBuffer buffer); |
7 | } |
Packet Read实现的思路
首先来看packetLength是如何获取的。该数据是占用三个字节的。所以有下面的方法:
1 | /** |
2 | * 使用0xff,当低位的数据byte向高位的数据进行转化的时候要考虑到二进制的唯一性。 |
3 | **/ |
4 | public int readUB3() { |
5 | final byte[] b = this.data;//data是服务端发送给我们的handshark字节数组 |
6 | int i = b[position++] & 0xff;//position表示当前字节的读取位置,初始值位0 |
7 | i |= (b[position++] & 0xff) << 8; |
8 | i |= (b[position++] & 0xff) << 16; |
9 | return i; |
10 | } |
同理像packetId、protocolVersion只用一个字节就可以存放的,就可以用下面的方法:
1 | public byte read() { |
2 | return data[position++] ; |
3 | } |
有一个要注意的是如果某个字段占用较大的字节数,如threadID该字段需要占用了四个字节。如果用int来存储就会有问题。因为已经表明了该字段需要四个字节,虽然int是四个字节,但是其最高位是表示符号的。所以int来放最多只能表示3个字节+7位。
所以为了溢出需要用long来存储。
1 | public long readUB4() { |
2 | final byte[] b = this.data; |
3 | long l = (long) (b[position++] & 0xff); |
4 | l |= (long) (b[position++] & 0xff) << 8; |
5 | l |= (long) (b[position++] & 0xff) << 16; |
6 | l |= (long) (b[position++] & 0xff) << 24; |
7 | return l; |
8 | } |
特殊的字段还有salt,根据它存储格式描述来看,它是以null为结尾的数据,所以实现如下:
1 | public byte[] readBytesWithNull() { |
2 | final byte[] b = this.data; |
3 | int offset = -1; |
4 | for (int i = position; i < length; i++) {//用来存放找到的第一个null位置 |
5 | if (b[i] == 0) { |
6 | offset = i; |
7 | break; |
8 | } |
9 | } |
10 | switch (offset) {//根据null的位置进行操作 |
11 | case -1: |
12 | byte[] ab1 = new byte[length - position]; |
13 | System.arraycopy(b, position, ab1, 0, ab1.length); |
14 | position = length; |
15 | return ab1; |
16 | case 0: |
17 | position++; |
18 | return EMPTY_BYTES; |
19 | default://通常是进入到这个方法体中 |
20 | byte[] ab2 = new byte[offset - position]; |
21 | System.arraycopy(b, position, ab2, 0, ab2.length); |
22 | position = offset + 1; |
23 | return ab2; |
24 | } |
25 | } |
特殊的字段还有Auth Packet中的password,其数据格式为LengthEncodedString,即在password之前有一个表示该字段的长度。
使用LengthEncodedInteger编码的整数可能会使用1, 3, 4, 或者9 个字节,具体使用字节取决于数值的大小,下表是不同的数据长度的整数所使用的字节数:
最小值(包含) | 最大值(不包含) | 存储方式 |
---|---|---|
0 | 251 | 1个字节 |
251 | 2^16 | 3个字节(0xFC + 2个字节具体数据) |
2^16 | 2^24 | 4个字节(0xFD + 3个字节具体数据) |
2^24 | 2^64 | 9个字节(0xFE + 8个字节具体数据) |
其read方法如下:
1 | public byte[] readBytesWithLength() { |
2 | int length = (int) readLength();//获取长度的方法 |
3 | if (length == NULL_LENGTH) { |
4 | return null; |
5 | } |
6 | if (length <= 0) { |
7 | return EMPTY_BYTES; |
8 | } |
9 | byte[] ab = new byte[length]; |
10 | System.arraycopy(data, position, ab, 0, ab.length); |
11 | position += length; |
12 | return ab; |
13 | } |
14 | |
15 | public long readLength() {//根据读到的一个字节值,来判断实际的长度, |
16 | int length = data[position++] & 0xff; |
17 | switch (length) { |
18 | case 251: |
19 | return NULL_LENGTH; |
20 | case 252: |
21 | return readUB2(); |
22 | case 253: |
23 | return readUB3(); |
24 | case 254: |
25 | return readLong(); |
26 | default: |
27 | return length; |
28 | } |
29 | } |
还有如Auth packet中的Username是一个NullTerminatedString格式的数据,其read方法如下:
1 | public String readStringWithNull() {//实现和readBytesWithNUll其实是类似的 |
2 | final byte[] b = this.data; |
3 | if (position >= length) { |
4 | return null; |
5 | } |
6 | int offset = -1; |
7 | for (int i = position; i < length; i++) { |
8 | if (b[i] == 0) { |
9 | offset = i; |
10 | break; |
11 | } |
12 | } |
13 | if (offset == -1) { |
14 | String s = new String(b, position, length - position); |
15 | position = length; |
16 | return s; |
17 | } |
18 | if (offset > position) { |
19 | String s = new String(b, position, offset - position); |
20 | position = offset + 1; |
21 | return s; |
22 | } else { |
23 | position++; |
24 | return null; |
25 | } |
26 | } |
上面讲解如何将服务端中代码解析到具体的类中,接下来主要来讲解如何将一个包的数据,按照协议的要求发出。
如服务端需要接受Client的一个Auth,格式要求如下:
Produce Auth Packet
具体步骤是先生成AuthPacket
,然后将对应的数据附上。然后调用auth.write(buffer)
,将数据写入到buffer
中。
再调用buffer.get(bytes, 0, bytes.length)
把buffer
转为二进制数据返回bytes,然后利用socket编程将bytes传给Server端。这里主要针对auth.write(buffer)
进行说明。
1 | /** |
2 | * 根据handshake包中的salt1和salt2生成加密的密码,将用户名、密码、数据库名一起打包成authPacket发送给服务端 |
3 | * @param rand1 salt1长度为8 |
4 | * @param rand2 salt2长度为12 |
5 | * @param user 用户名 |
6 | * @param password 未加密的密码 |
7 | * @param database 要操作的数据库名 |
8 | * @return 返回auth packet的二进制 |
9 | */ |
10 | public static byte[] produceAuthPacket(byte[] rand1,byte[] rand2, String user , String password,String database) { |
11 | |
12 | byte[] seed = new byte[rand1.length + rand2.length]; |
13 | System.arraycopy(rand1, 0, seed, 0, rand1.length); |
14 | System.arraycopy(rand2, 0, seed, rand1.length, rand2.length); |
15 | |
16 | AuthPacket auth = new AuthPacket(); |
17 | auth.packetId = 1;//包的序列号 |
18 | auth.clientFlags = getClientCapabilities();//客户端协议 |
19 | auth.maxPacketSize = 1024 * 1024 * 1024;//包的最大值 |
20 | auth.user = user;//用户名 |
21 | try {//利用salt1,salt2对password进行加密 |
22 | auth.password = SecurityUtil |
23 | .scramble411(password.getBytes(), seed); |
24 | } catch (NoSuchAlgorithmException e) { |
25 | e.printStackTrace(); |
26 | } |
27 | auth.database = database;//操作的数据库名 |
28 | |
29 | ByteBuffer buffer = ByteBuffer.allocate(256); |
30 | auth.write(buffer);//将数据的二进制写入buffer中 |
31 | buffer.flip(); |
32 | byte[] bytes = new byte[buffer.remaining()]; |
33 | buffer.get(bytes, 0, bytes.length); |
34 | return bytes; |
35 | } |
这里讲一些特殊字段的write,如长度为4字节的clientFlags。
1 | //如果是存入四个字节,其实是不能使用int的,因为24-32位是有符号位的,所以这里需要使用Long,这样可以保证前32表示的都是值 |
2 | public static final void writeUB4(ByteBuffer buffer, long l) { |
3 | buffer.put((byte) (l & 0xff)); |
4 | buffer.put((byte) (l >>> 8)); |
5 | buffer.put((byte) (l >>> 16)); |
6 | buffer.put((byte) (l >>> 24)); |
7 | } |
还有数据后跟着null的字段,如username。
1 | public static final void writeWithNull(ByteBuffer buffer, byte[] src) { |
2 | buffer.put(src); |
3 | buffer.put((byte) 0); |
4 | } |
还有在插入数据前,写入长度的数据,如password。因为表示长度的数据是一个LengthEncodedInteger,所以根据长度的大小,先要插入一个标志符,具体如下:
最小值(包含) | 最大值(不包含) | 存储方式 |
---|---|---|
0 | 251 | 1个字节 |
251 | 2^16 | 3个字节(0xFC + 2个字节具体数据) |
2^16 | 2^24 | 4个字节(0xFD + 3个字节具体数据) |
2^24 | 2^64 | 9个字节(0xFE + 8个字节具体数据) |
1 | public static final void writeWithLength(ByteBuffer buffer, byte[] src) { |
2 | int length = src.length; |
3 | if (length < 251) { |
4 | buffer.put((byte) length); |
5 | } else if (length < 0x10000L) { |
6 | buffer.put((byte) 252); |
7 | writeUB2(buffer, length); |
8 | } else if (length < 0x1000000L) { |
9 | buffer.put((byte) 253); |
10 | writeUB3(buffer, length); |
11 | } else { |
12 | buffer.put((byte) 254); |
13 | writeLong(buffer, length); |
14 | } |
15 | buffer.put(src); |
16 | } |
Mysql Binlog
MySQL复制原理基础
复制原理
MySQL主备复制基于二进制日志binlog。任何数据更改都会写入二进制日志。
数据库管理员搭建主备复制时,只需要在备库change master to指定主库的IP、端口、同步开始的二进制文件和文件偏移量(MySQL 5.6以后支持GTID模式,二进制文件和文件偏移量可以用GTID号集合替换)就可以了。
备库通过IO线程连接主库,接收主库推送过来的二进制日志,并记录到本地的中继日志relaylog;同时也会启动SQL线程将中继日志的数据变更应用到备库本地数据库中.
主库接受到备库IO线程的请求,会专门对该slave启用独立的binlog dump线程,从IO线程指定的二进制文件和文件偏移量开始发送二进制日志;并且在主库有任何新的变更后,在记录到自己的二进制日志的同时也会通过网络推送给备库的IO线程。
复制线程
- Master线程
binlog dump线程
dump线程的作用是读取主库上二进制日志中的事件。在复制线程处于正常运行状态时,当事务提交的时候,binlog日志sync到磁盘上之后,MySQL会调用signal_update()函数,这个函数的作用是通知binlog dump线程,binlog日志有更新了,dump线程将产生的增量binlog推送到从库的IO线程;在主从之间建议复制连接的时候,从库IO线程将binlog文件名以及位置点(GTID模式下是发送GTID集合)发送给主句dump线程拉取从库所需binlog。
对于一主多从的情况,master上会有多个binlog dump线程。
- Slave线程
I/O线程:
I/O线程的作用就是拉取主库上的
binlog
日志,在从库上存贮为relay log
日志。方便SQL线程中relay log
中重放事务。
I/O线程在与主库建立连接的时候,超过slave_net_timeout
时间没有建立连接成功,从库就认为这次连接失败,需要重试连接,重试连接的次数由MASTER_RETRY_COUNT
决定。
I/O线程在与主库成功建立连接之后,针对可能主库很长时间都没有更新数据的情况,I/O线程采用了心跳机制,I/O线程在空闲的时候,每隔MASTER_HEARTBEAT_PERIOD
时间间隔,I/O线程就向主库发送一个心跳包,测试与主库的连接是否正常。
使用mysqlbinlog
工具也可以解析relay log
日志。SQL线程:
读取
relay log
,并且重放relay log
中的事务,以达到复制的目的。
复制格式
binlog
的格式可以分为三种,从某种意义上来讲也可以说对应着三种复制模式,使用binlog_format
控制binlog
的格式,该参数的不同值代表了不同的复制模式,下面以该参数的三个值对复制格式进行简单的阐述:
- statement:5.1.5之前只支持statement格式(俗称:statement binary replication,缩写:SBR),简单实现了数据同步,但在执行跨库更新等SQL语句时容易出现主从的数据不一致
- row:5.1.5及其之后,新增支持row格式(俗称:row binary replication,缩写:RBR),不再是简单记录SQL执行顺序,而是逐行记录了存储引擎的数据如何变更的,主从之间的数据一致性保障得到大幅度提升
- mixed:5.1.8及其之后新增支持mixed格式(俗称:mixed binary replication,缩写:MBR),本质上是让MySQL Server自行根据SQL语句的不同来判断是否需要使用row格式,当出现可能造成主从数据不一致的SQL时(例如:用户自定义函数,跨库SQL等),binlog自动转为row格式记录,否则默认使用statement格式记录
MySQL binlog
什么是binlog
二进制日志(binary log简称binlog)是MySQL中的日志文件,会记录MySQL数据库执行的更改操作。但是只记录更改操作,如SELECT、SHOW等操作,不会被记录到binlog中。binlog对于MySQL而言是很重要的日志文件,它可以有以下功能:
- 搭建复制
MySQL的复制完全依赖于binlog搭建,复制的原理本质上就是从库从主库上拉取主库的二进制日志,然后将拉取到的日志在从库上进行重放,实现数据的同步。
- 数据恢复
一般的备份方案,都是特定的时刻对数据库进行备份,没有对数据库进行实时的备份。如果数据库在两个备份时间点之间出现故障,那么恢复数据的时候,最多只能恢复到最近一个备份时间点。但是binlog日志可以基于时间点或者位置点进行数据恢复。在上述故障情况的时候,可以拿最近一个时间点的备份+binlog将数据恢复到故障时刻。
- 审计
用户可以通过二进制日志中的信息来进行审计,判断是否有对数据局进行注入的攻击。
binlog文件
binlog文件主要包括:
1 | mysql-bin.00000x |
2 | mysql-bin.index |
mysql-bin.index
记录了数据库中还未被purge
的binlog
文件名。
1 | [root@10-10-90-177 binlog]# cat mysql-bin.index |
2 | /home/mysql/data/mysqldata1/binlog/mysql-bin.000001 |
3 | /home/mysql/data/mysqldata1/binlog/mysql-bin.000002 |
4 | /home/mysql/data/mysqldata1/binlog/mysql-bin.000003 |
mysql-bin.00000x
是真正的binlog文件 ,x
表示binlog文件的序号,这些二进制文件里面的数据是以二进制的形式保存,可以通过特殊的工具(mysqlbinlog)进行解析,获取人类可读的信息。
binlog文件组成
- binlog是由event组成,event是binlog的最小组成单元
- binlog文件头部固定以4个字节开头,这四个字节称为BINLOG_MAGIC(fe 62 69 6e)
- 每个binlog文件以一个Format_desc类型的event开始
- 每个binlog文件以一个Rotate类型的event结束
特殊情况:
当数据库出现宕机的情况,重新启动数据库会生成一个新的binlog文件,但是宕机之前的最新binlog文件中,不是以ROTATE_EVENT结束的)
- 在FORMAT_DESCRIPTION_EVENT和ROTATE_EVENT之间是各种不同event,每个event代表Master上不同的操作。
BINLOG_MAGIC
每个binlog文件以固定的4个字节(fe 62 69 6e)开始,以表示是一个binlog文件,在Linux环境下,我们可以通过hexdump命令查看binlog文件的字节组成。
event
我们有两种方式可以查看,binlog文件中的event组成方式。
- 在MySQL数据库中通过SHOW BINLOG EVENTS IN 'binlog-file-name’的方式查看
- 是通过mysqlbinlog工具查看binlog文件的组成。
1 | root@localhost : (none) 10:50:48> show binlog events in 'mysql-bin.000004'; |
2 | +------------------+-----+-------------+-----------+-------------+-----------------------------------------------------------+ |
3 | | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | |
4 | +------------------+-----+-------------+-----------+-------------+-----------------------------------------------------------+ |
5 | | mysql-bin.000004 | 4 | Format_desc | 3306114 | 120 | Server ver: 5.6.34-log, Binlog ver: 4 | |
6 | | mysql-bin.000004 | 120 | Query | 3306114 | 201 | BEGIN | |
7 | | mysql-bin.000004 | 201 | Rows_query | 3306114 | 265 | # insert into test1(`name`) values('woqu') | |
8 | | mysql-bin.000004 | 265 | Table_map | 3306114 | 320 | table_id: 70 (gangshen.test1) | |
9 | | mysql-bin.000004 | 320 | Write_rows | 3306114 | 365 | table_id: 70 flags: STMT_END_F | |
10 | | mysql-bin.000004 | 365 | Xid | 3306114 | 396 | COMMIT /* xid=24 */ | |
11 | | mysql-bin.000004 | 396 | Query | 3306114 | 477 | BEGIN | |
12 | | mysql-bin.000004 | 477 | Rows_query | 3306114 | 556 | # update test1 set name='woqu-change' where name = 'woqu' | |
13 | | mysql-bin.000004 | 556 | Table_map | 3306114 | 611 | table_id: 70 (gangshen.test1) | |
14 | | mysql-bin.000004 | 611 | Update_rows | 3306114 | 674 | table_id: 70 flags: STMT_END_F | |
15 | | mysql-bin.000004 | 674 | Xid | 3306114 | 705 | COMMIT /* xid=27 */ | |
16 | | mysql-bin.000004 | 705 | Query | 3306114 | 786 | BEGIN | |
17 | | mysql-bin.000004 | 786 | Rows_query | 3306114 | 854 | # delete from test1 where name = 'woqu-change' | |
18 | | mysql-bin.000004 | 854 | Table_map | 3306114 | 909 | table_id: 70 (gangshen.test1) | |
19 | | mysql-bin.000004 | 909 | Delete_rows | 3306114 | 961 | table_id: 70 flags: STMT_END_F | |
20 | | mysql-bin.000004 | 961 | Xid | 3306114 | 992 | COMMIT /* xid=28 */ | |
21 | | mysql-bin.000004 | 992 | Rotate | 3306114 | 1039 | mysql-bin.000005;pos=4 | |
22 | +------------------+-----+-------------+-----------+-------------+-----------------------------------------------------------+ |
23 | 17 rows in set (0.00 sec) |
可以看到mysql-bin.000004
文件中,是以一个Format_desc
类型的event开头,以一个Rotate
类型的event结尾,中间是各种类型event,这是看binlog
文件中event的组成,如果想要查看更详细的event信息,可以通过mysqlbinlog
工具来查看
1 | /*!40019 SET @@session.max_insert_delayed_threads=0*/; |
2 | /*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/; |
3 | DELIMITER /*!*/; |
4 | at 4 |
5 | 171205 10:35:43 server id 3306114 end_log_pos 120 CRC32 0x7d7e496c Start: binlog v 4, server v 5.6.34-log created 171205 10:35:43 |
6 | at 120 |
7 | 171205 10:49:25 server id 3306114 end_log_pos 201 CRC32 0x24d309ef Query thread_id=2 exec_time=0 error_code=0 |
8 | SET TIMESTAMP=1512442165/*!*/; |
9 | SET @@session.pseudo_thread_id=2/*!*/; |
10 | SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/; |
11 | SET @@session.sql_mode=1075838976/*!*/; |
12 | SET @@session.auto_increment_increment=2, @@session.auto_increment_offset=2/*!*/; |
13 | /*!\C utf8 *//*!*/; |
14 | SET @@session.character_set_client=33,@@session.collation_connection=33,@@session.collation_server=83/*!*/; |
15 | SET @@session.lc_time_names=0/*!*/; |
16 | SET @@session.collation_database=DEFAULT/*!*/; |
17 | BEGIN |
18 | /*!*/; |
19 | at 201 |
20 | 171205 10:49:25 server id 3306114 end_log_pos 265 CRC32 0xef0e9a3a Rows_query |
21 | insert into test1(`name`) values('woqu') |
22 | at 265 |
23 | 171205 10:49:25 server id 3306114 end_log_pos 320 CRC32 0xf86a076e Table_map: `gangshen`.`test1` mapped to number 70 |
24 | at 320 |
25 | 171205 10:49:25 server id 3306114 end_log_pos 365 CRC32 0x653b8de4 Write_rows: table id 70 flags: STMT_END_F |
26 | ## INSERT INTO `gangshen`.`test1` |
27 | ## SET |
28 | ## @1=4 /* INT meta=0 nullable=0 is_null=0 */ |
29 | ## @2='woqu' /* VARSTRING(60) meta=60 nullable=1 is_null=0 */ |
30 | at 365 |
31 | 171205 10:49:25 server id 3306114 end_log_pos 396 CRC32 0x53ca8e9d Xid = 24 |
32 | COMMIT/*!*/; |
33 | ............ |
34 | at 705 |
35 | 171205 10:50:20 server id 3306114 end_log_pos 786 CRC32 0x815b7a14 Query thread_id=2 exec_time=0 error_code=0 |
36 | SET TIMESTAMP=1512442220/*!*/; |
37 | BEGIN |
38 | /*!*/; |
39 | at 786 |
40 | 171205 10:50:20 server id 3306114 end_log_pos 854 CRC32 0xc265e50d Rows_query |
41 | delete from test1 where name = 'woqu-change' |
42 | at 854 |
43 | 171205 10:50:20 server id 3306114 end_log_pos 909 CRC32 0x3a62ecaf Table_map: `gangshen`.`test1` mapped to number 70 |
44 | at 909 |
45 | 171205 10:50:20 server id 3306114 end_log_pos 961 CRC32 0x2bf4c689 Delete_rows: table id 70 flags: STMT_END_F |
46 | ## DELETE FROM `gangshen`.`test1` |
47 | ## WHERE |
48 | ## @1=4 /* INT meta=0 nullable=0 is_null=0 */ |
49 | ## @2='woqu-change' /* VARSTRING(60) meta=60 nullable=1 is_null=0 */ |
50 | at 961 |
51 | 171205 10:50:20 server id 3306114 end_log_pos 992 CRC32 0x412de6a3 Xid = 28 |
52 | COMMIT/*!*/; |
53 | at 992 |
54 | 171205 10:50:23 server id 3306114 end_log_pos 1039 CRC32 0x8372f001 Rotate to mysql-bin.000005 pos: 4 |
55 | DELIMITER ; |
56 | End of log file |
57 | ROLLBACK /* added by mysqlbinlog */; |
58 | /*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/; |
59 | /*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/; |
可以看到第一个event是FORMAT_DESCRIPTION_EVENT
,记录了binlog的版本为v4。最后一个event,是ROTATE_EVENT
,它记录了下一个binlog文件的文件名以及对应的位置点。
MySQL 协议基础
MySQL协议是用于MySQL中Server端与Client端进行通讯的协议,MySQL主备复制也遵守Server和Client端通讯协议。
基础数据类型
MySQL协议传输中的数据包含两种类型,一种是数值型数据,另一种是字符型数据。
数值型数据
- Protocol::FixedLengthInteger
- Protocol::LengthEncodedInteger
MySQL使用Protocol::FixedLenthInteger 表示固定长度的整数
类型 | 长度 | 对应函数 | 实际对应类型 |
---|---|---|---|
int<1> | 1 byte | ||
int<2> | 2 byte | int2store() | uint16 |
int<3> | 3 byte | int3store() | uint |
int<4> | 4 byte | int4store() | uint32 |
int<6> | 6 byte | int6store() | ulonglong |
int<8> | 8 byte | int8store() | ulonglong |
使用Protocol::LengthEncodedInteger类型表示的整数,会根据表示的整数大小占用不同的字节数(1,3,4或9个字节)
整数范围 | 表示方式 | 示例 |
---|---|---|
0≤N<251 | 1 byte | 0x08 表示整数8 |
251≤N<2^16 | 0xFC + 2 byte | 0xFC0008 = 0xFC + 0x0008 表示整数260 |
216≤N<224 | 0xFD + 3 byte | 0xFD123456 = 0xFD + 0x123456 表示整数1193299 |
224≤N<264 | 0xFE + 8 byte | 0xFE0000000000000009 = 0xFE + 0x0000000000000009 表示整数263 |
字符型数据
-
Protocol::FixedLengthString 固定长度的字符串
字符串用固定长度表示,例如ERR_Packet中的 sql_state 部分就是固定5个字节长度
-
Protocol::NullTerminatedString
以0x00结尾的字符串
-
Protocol::VariableLengthString
在程序运行过程中动态确定长度的字符串
-
Protocol::LengthEncodedString
在字符串开始用Protocol::LengthEncodedInteger类型整数表示字符串的长度
-
Protocol::RestOfPacketString
字符串是整个数据包的最后一个组成部分,字符串的长度等于整个数据包的长度减去当前位置
基本协议数据包格式
MySQL中server端与client端进行数据交换的时候,会将要交换的数据,按照上述的协议数据类型封装成数据包,然后通过MySQL协议进行数据包的传输。
基础数据包的格式定义
相关代码函数:
1 | sql/net_serv.cc -> net_write_command() |
名字 | 基本数据类型 | 类型 | 描述 |
---|---|---|---|
payload_length | Protocol::FixedLengthInteger | int<3> | 数据包中有效信息的长度,不包括数据包头部4个字节的长度 |
sequence_id | Protocol::FixedLengthInteger | int<1> | 数据包序列号 |
payload | Protocol::VariableLengthString | string | 数据包的有效信息 |
基础数据包的格式解析
MySQL协议中数据包固定由 payload_length
+ sequence_id
+ payload
组成。每个数据包前3个字节固定存储payload_length信息,即数据包中有效信息payload的长度,第4个字节表示该数据包在此次通信中的序号。因为payload_length为int<3>类型,所以能表示的最大payload部分长度为224-1个字节,也就是说一个数据包能传输的数据量最大为224-1字节(合16MB)。
那么对于一次请求中,数据量超过16MB的数据,MySQL是如何处理的呢?
对于payload部分大于等于16MB的,MySQL会将数据进行切分,每16MB为一部分,然后将切分的数据按照数据包的格式封装,进行发送,直到切分的数据部分长度小于16MB。
常见数据包示例
MySQL协议中常见的数据包类型,不同类型的数据包,其格式都是符合上述的基础数据包格式定义的。但是对于不同数据包类型,其payload部分的数据格式是不同的。
OK_Packet
对于Client发送的请求,如果执行成功,则Server端会向Client端发送OK_Packet告诉Client端请求执行成功。但是在5.7.5版本之后,OK Packet也可以用于表示EOF信息,原先的 EOF_Packet被废弃了,EOF Packet后面章节会详细描述。
OK_Packet的格式定义
相关代码位置:
1 | sql/protocol_classic.cc -> net_send_ok()函数 |
名字 | 基本数据类型 | 类型 | 描述 |
---|---|---|---|
header | Protocol::FixedLengthInteger | int<1> | 0x00表示该数据包是OK_Packet,0xFE表示该数据包是ERR_Packet |
affect_rows | Protocol::LengthEncodedInteger | int | 请求在Server端执行影响的记录行数 |
last_insert_id | Protocol::LengthEncodedInteger | int | 对于INSERT请求,表示最近插入的那行记录的自增字段值 |
status_flags | Protocol::FixedLengthInteger | int<2> | Server端的状态信息,如果Client发送的请求中带有CLIENT_PROTOCOL_41标志,会有这一部分,否则为空 |
warnings | Protocol::FixedLengthInteger | int<2> | 警告的数量,如果Client发送的请求中带有CLIENT_PROTOCOL_41标志,会有这一部分,否则为空 |
status_flag | Protocol::FixedLengthInteger | int<2> | 如果Client发送的请求中带有CLIENT_TRANSACTIONS标志,会有这一部分,否则为空 |
messages | Protocol::LengthEncodedString | string | 当前会话状态改变的信息,如果Client发送的请求中带有CLIENT_SESSION_TRACK标志,会有这一部分,否则为空 |
session_state_change_info | Protocol::LengthEncodedString | string | 当前会话状态改变的信息,如果Client发送的请求中带有SERVER_SESSION_STATE_CHANGED标志,会有这一部分,否则为空 |
messages | Protocol::RestOfPacketString | string | 请求执行结果信息,如果不包含信息,该部分为空 |
其中int<lenenc>
和string<lenenc>
中的lenenc
表示的是LengthEcode
。
status_flag
字段中记录的是Server端的状态信息,MySQL支持的server_status
类型:
1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
当表示两个及两个以上状态时,将代表多个状态的值做^
运算即可。
OK Packet的格式解析
上面讲到5.7.5版本之后,OK Packet
既可以表示OK Packet
也可以表示EOF Packet
,那么在5.7.5之后如何区分是OK Packet 还是 EOF Packet呢?
OK_Packet
:头部部分是0x00
,并且数据包的长度是大于7EOF_Packet
:头部部分是0xFE
,并且数据包的长度是小于9
为了保证5.7.5之前版本与之后版本的兼容性,5.7.5之后的MySQL 版本中的client端都会发送CLIENT_DEPRECATE_EOF
标志。旧版本client不会发送该标志,所以Server端还是采用专门的EOF_Packet
(下面会具体介绍)表示EOF信息,新版本的client在向Server端发送请求时会带上该标志,但是对于旧版本的Server端,无法识别该标志,所以依然是采用专门的EOF_Packet表示EOF信息。
client端发送带CLIENT_PROTOCOL_41
标志的数据包示例(内容为16进制格式):
1 | 07 00 00 #数据包长度 |
2 | 02 #数据包序号 |
3 | 00 #头部 |
4 | 00 #影响的记录行数 |
5 | 00 #上一次插入的记录自增值 |
6 | 02 00 #状态标志 |
7 | 00 00 #警告数量 |
各分段数据含义:
- 数据包长度:MySQL协议中固定格式,示例中
07 00 00
表示0x000007
,代表数据包长度为7个字节 - 数据包序号:MySQL协议中固定格式,示例中
02
表示数据包序号为2 - 头部:示例中
00
表示该数据包为OK_Packet
- 影响的记录行数:示例中
00
表示0x00
,代表本次client请求共影响0行记录 - 上一次插入的记录自增值:实例中
00
表示0x00
- 状态标志:因为是小端存储,所以示例中
02 00
表示0x0002
,代表Server端开启了auto_commit
- 警告的数量:因为是小端存储,所以示例中
00 00
表示0x0000
,表示告警数量为0
Session State Information
OK_Packet中也可以记录会话级别的状态改变信息,包括系统变量信息、当前会话使用的schema信息、会话级别状态改变信息、事务特性、事务状态信息。当Client端发送的请求中带有SERVER_SESSION_STATE_CHANGED
标志,则在Server端返回的OK_Packet
中会带有这些信息。OK_Packet一共支持如下几种会话级别状态信息:
1 | enum enum_session_state_type |
2 | { |
3 | SESSION_TRACK_SYSTEM_VARIABLES, /* Session system variables */ |
4 | SESSION_TRACK_SCHEMA, /* Current schema */ |
5 | SESSION_TRACK_STATE_CHANGE, /* track session state changes */ |
6 | SESSION_TRACK_GTIDS, |
7 | SESSION_TRACK_TRANSACTION_CHARACTERISTICS, /* Transaction chistics */ |
8 | SESSION_TRACK_TRANSACTION_STATE /* Transaction state */ |
9 | }; |
具体可参考官方文档:page_protocol_basic_ok_packet
ERR_Packet
该数据包用于返回出现错误信息。 相关代码位置:
1 | sql/protocol_classic.cc -> net_send_error_packet()函数 |
ERR_Packet的格式定义
在5.7.5之前版本中,ERR_Packet采用单独表示格式,5.7.5之后,ERR_Packet与OK_Packet复用相同格式。以下是针对5.7.5之前版本介绍ERR_Packet格式定义。
名字 | 基本数据类型 | 类型 | 描述 |
---|---|---|---|
header | Protocol::FixedLengthInteger | int<1> | 用0xFF表示该数据包是一个ERR Packet |
error_no | Protocol::FixedLengthInteger | int<2> | 表示错误的类型,小端存储 |
sql_state_marker | Protocol::VariableLengthString | string[1] | 如果Client发送的请求中带有CLIENT_PROTOCOL_41标志,会有这一部分,否则为空 |
sql_state | Protocol::VariableLengthString | string[5] | 如果Client发送的请求中带有CLIENT_PROTOCOL_41标志,会有这一部分,否则为空 |
err_message | Protocol::RestOfPacketString | string | 可读的错误信息详情 |
ERR_Packet的格式解析
数据包示例(内容为16进制格式):
1 | 4b 00 00 #数据包长度 |
2 | 02 #数据包序号 |
3 | ff #ERR_Packet标识 |
4 | 15 04 #错误号 |
5 | 23 32 38 30 30 30 41 63 63 65 73 73 20 64 65 6e 69 65 64 20 66 6f 72 20 75 73 65 72 20 27 72 65 70 6c 27 40 27 31 32 31 2e 31 32 31 2e 30 2e 36 34 27 20 28 75 73 69 6e 67 20 70 61 73 73 77 6f 72 64 3a 20 59 45 53 29 #错误信息 |
各分段数据含义:
- 数据包长度:MySQL协议中固定格式,示例中
4b 00 00
表示0x00004b
,代表数据包长度为75个字节 - 数据包序号:MySQL协议中固定格式,示例中
00
表示数据包序号为0 - 头部:示例中ff表示该数据包为
ERR_Packet
- 错误号:因为是小端存储,所以示例中
15 04
对应16进制0x0415
,转换为10进制为1045 - 错误信息:示例中的内容可以转换为
Access denied for user 'repl'@'121.121.0.64'(usingpassword:YES)
EOF_Packet
表示当前会话可以结束了,结束连接的信号。 相关代码位置:
1 | sql/protocol_classic.cc -> write_eof_packet()函数 |
EOF_Packet的格式定义
名字 | 基本数据类型 | 类型 | 描述 |
---|---|---|---|
header | Protocol::FixedLengthInteger | int<1> | 用0xFE表示该数据包是一个ERR_Packet |
warnings | Protocol::FixedLengthInteger | int<2> | 警告的数量,如果Client发送的请求中带有CLIENT_PROTOCOL_41标志,会有这一部分,否则为空,小端存储 |
server_status | Protocol::FixedLengthInteger | int<2> | Server端的状态信息,如果Client发送的请求中带有CLIENT_PROTOCOL_41标志,会有这一部分,否则为空,小端存储 |
其中server_status
内容参照OK_Packet中status_flags
字段。
EOF_Packet的格式解析
数据包示例(内容为16进制格式):
1 | 05 00 00 #数据包长度 |
2 | 05 #数据包序号 |
3 | fe #头部 |
4 | 00 00 #警告的数量 |
5 | 02 00 #Server端的状态信息 |
各分段数据含义:
- 数据包长度:MySQL协议中固定格式,示例中
05 00 00
表示0x000005
,代表数据包长度为5个字节 - 数据包序号:MySQL协议中固定格式,示例中05表示数据包序号为5
- 头部:标识是否是
EOF
数据包,0xFE标识该数据包是EOF_Packet
- 警告的数量:因为是小端存储,所以示例中
00 00
表示0x0000
,表示告警数量为0 - Server端的状态信息:因为是小端存储,所以示例中
02 00
表示0x0002
,代表Server端开启了auto_commit
Query_Packet
Query_Packet的格式定义
名字 | 基本数据类型 | 类型 | 描述 |
---|---|---|---|
payload_length | Protocol::FixedLengthInteger | int<3> | 数据包中有效信息的长度,不包括数据包头部4个字节的长度,小端存储 |
sequence_id | Protocol::FixedLengthInteger | int<1> | 数据包序列号 |
command_type | Protocol::FixedLengthInteger | int<1> | 要发送的请求的类型 |
command | Protocol::RestOfPacketString | string | 请求内容 |
Query_Packet的格式解析
以SET @master_binlog_checksum
语句请求为例
数据包示例(内容为16进制格式):
1 | 36 00 00 #数据包长度 |
2 | 00 #数据包序号 |
3 | 03 #发送的command类型 0x03表示的是COM_QUERY类型的命令 |
4 | 53 45 54 20 40 6d 61 73 74 65 72 5f 62 69 6e 6c 6f 67 5f 63 68 65 63 6b 73 75 6d 3d 20 40 40 67 6c 6f 62 61 6c 2e 62 69 6e 6c 6f 67 5f 63 68 65 63 6b 73 75 6d #要发送的请求语句 //SET @master_binlog_checksum=@@global.binlog_checksum |
各分段数据含义:
- 数据包长度:MySQL协议中固定格式,示例中
36 00 00
表示0x000036
,代表数据包长度为36个字节 - 数据包序号:MySQL协议中固定格式,示例中00表示数据包序号为0
- COMMAND类型:表示发送的请求的类型,MySQL支持的命令类型如下:
1 | enum enum_server_command |
2 | { |
3 | COM_SLEEP,COM_QUIT, |
4 | COM_INIT_DB, |
5 | COM_QUERY, |
6 | COM_FIELD_LIST, |
7 | COM_CREATE_DB, |
8 | COM_DROP_DB, |
9 | COM_REFRESH, |
10 | COM_SHUTDOWN, |
11 | COM_STATISTICS, |
12 | COM_PROCESS_INFO, |
13 | COM_CONNECT, |
14 | COM_PROCESS_KILL, |
15 | COM_DEBUG, |
16 | COM_PING, |
17 | COM_TIME, |
18 | COM_DELAYED_INSERT, |
19 | COM_CHANGE_USER, |
20 | COM_BINLOG_DUMP, |
21 | COM_TABLE_DUMP, |
22 | COM_CONNECT_OUT, |
23 | COM_REGISTER_SLAVE, |
24 | COM_STMT_PREPARE, |
25 | COM_STMT_EXECUTE, |
26 | COM_STMT_SEND_LONG_DATA, |
27 | COM_STMT_CLOSE, |
28 | COM_STMT_RESET, |
29 | COM_SET_OPTION, |
30 | COM_STMT_FETCH, |
31 | COM_DAEMON, |
32 | COM_BINLOG_DUMP_GTID, |
33 | COM_RESET_CONNECTION, |
34 | COM_END |
35 | }; |
- 要发送的请求语句:将要发送的语句转换为ASCII对应的16进制,示例中的内容可以转换为:
SET @master_binlog_checksum=@@global.binlog_checksum
MySQL 复制流程详解
MySQL复制流程概述
按照复制原理中描述的,在备库上提交change master to请求主备数据连接以后
- TCP连接阶段:备库和主库之间会建立TCP链接(TCP/IP的三次握手建立连接过程不在本文讨论范畴,这里不详细描述)
- 用户认证阶段:主库会对备库的用户名密码进行认证,进行MySQL应用层的三次握手
- register_slave注册slave阶段:备库通过register_slave将自己注册到主库上(该步骤可忽略),以便master在show slave host时可以查看到哪些slave连接上来了
- request_dump请求binlog阶段:备库通过request_dump,请求从主库指定的二进制日志和文件偏移量开始获取所有的二进制日志。
- master发送Binlog Event阶段:Master启动binlog dump线程,从指定的二进制日志的对应文件偏移量开始向备库的IO线程发送二进制日志Event。(该步骤在主库上就是读取文件和网络发送的过程,本文档中不详细描述)
- 备库接收存储Relaylog阶段:备库的IO线程持续接收Event并将Event存储relay log中。具体Event的存储格式以及各种类型的字段在Binlog Event的存储格式请参考后续。
接下来的几节将详细对以上2,3,4步的网络协议字节格式、及可能取值等方面进行详细描述
用户认证过程详解
用户认证时序图
1 | Master->Slave:Server Greeting 开始MySQL握手协议 |
2 | Slave->Master:Login Request 进行账号密码验证登陆 |
3 | Master->Slave:Response OK 账号密码验证通过 |
即
- master先发送了greeting网络包;
- slave接收到该网络包以后提交帐号密码进行验证;
- master接收到用户名密码,验证通过后会通过Response OK 返回表示认证通过。
普通的客户端连接MySQL同样需要经历这三次握手协议。
异常情况
-
第1步如果slave没有接收到greeting网络包,在超时等待后会报错退出
-
第2步,如果master没有接收到slave提交的帐号密码,此时,可以在master数据库中看到
类似于如下信息:可以看到当前数据库中会有一个连接连上来,但是在User字段显示是的unauthenticated user,这就是表示连接已经建立,但是用户认证还没有完成。该线程的状态是Receiving from client,表示正在等待client发送用户认证的数据信息。超时等待后,master也会报错并中止对应的线程.
-
第3步,如果master密码认证错误,会直接发送一个错误包,slave和Master都需要中止这个连接。
接下来几节将详细描述用户认证时序图中1,2,3三个网络包的具体字节分段含义。
Server Greeting步骤
Greeting数据包格式
类型 | 名字 | 描述 |
---|---|---|
int<3> | payload_length | 数据包中有效信息的长度,不包括数据包头部4个字节的长度 |
int<1> | sequence_id | 数据包序列号 |
int<1> | 协议版本号 | 表示当前连接使用的协议版本 |
string< null-terminated > | 服务器版本信息 | 表示当前server端服务器版本 |
int<4> | thread_id | 表示当前连接,在server上的线程ID |
string< null-terminated > | scramble data part1 | 用于密码校验的随机数部分一 |
int<2> | 无意义 | |
int<1> | server_charset | 服务器的字符集 |
int<2> | server_status | 服务器状态 |
int<2> | server_capability | 服务器权能标志 |
int<1> | pkt_scramble_length | |
string[10] | 无意义 | |
string< null-terminated > | scramble data part2 | 用于密码校验的随机数部分二 |
string< null-terminated > | scramble plugin name | 用于密码校验的插件名称 |
数据包解析示例
数据包示例:
1 | 4e 00 00 数据包长度 //88 |
2 | 00 数据包序号 //0 |
3 | 0a 协议版本号//10 |
4 | 35 2e 37 2e 31 39 2d 6c 6f 67 00 服务器版本信息 //5.7.19-log |
5 | 72 00 00 00 线程ID //114 |
6 | 7e 1a 15 11 07 0a 7e 1c 00 scramble data part1 |
7 | ff f7 |
8 | 53 服务器字符集 // |
9 | 02 00 服务器状态 |
10 | ff 81 服务器权能标志 |
11 | 15 pkt_scramble_length |
12 | 00 00 00 00 00 00 00 00 00 00 无用 |
13 | 0b 2c 62 79 75 7a 3d 01 61 25 6f 22 00 scramble data part2 |
14 | 6d 79 73 71 6c 5f 6e 61 74 69 76 65 5f 70 61 73 73 77 6f 72 64 00 scramble插件名字 //mysql_native_password |
各分段含义解释:
- 数据包长度:MySQL协议中固定格式,前三个字节表示数据包长度,这个长度不包括固定格式的4个字节
- 数据包序号:MySQL协议中固定格式,用一个字节表示数据包的序号
- 协议版本号:MySQL协议的版本序号
- 服务器版本信息:服务器的版本,这边为5.7.19-log
- 线程ID:表示master分配给该连接的线程的ID号,与show processlist中的thread id对应
- scramble data part1:master 生成的随机数,用于加密密码
- 服务器字符集:表示master使用的字符集编码
- 服务器状态:表示服务器状态
在/include/mysql_com.h
定义了服务器的状态
1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
- 服务器权能标志:用于与客户端协商协议方式
在/include/mysql_com.h
定义
1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 | /* Enable authentication response packet to be larger than 255 bytes. */ |
23 |
|
24 | /* Don't close the connection for a connection with expired password. */ |
25 |
|
26 | /** |
27 | Capable of handling server state change information. Its a hint to the |
28 | server to include the state change information in Ok packet. |
29 | */ |
30 |
|
31 | /* Client no longer needs EOF packet */ |
32 |
|
33 |
|
34 |
|
- scramble data part2:与scramble data part1组成为随机数,用于加密密码
- scramble插件名字:指定用于scramble data加密方式的插件,这边为native_mysql_password
Login Request步骤
本节主要描述slave发送的用户认证的数据包(slave->master)过程
相关代码位置
1 | sql/rpl_slave.cc文件中的handle_slave_io() |
2 | => sql/rpl_slave.cc文件中的safe_connect()函数 |
3 | => sql/rpl_slave.cc文件中的connect_to_master()函数 |
4 | => sql-common/client.c文件中的CLI_MYSQL_REAL_CONNECT()函数 |
5 | => sql-common/client.c文件中的run_plugin_auth()函数 |
6 | => sql-common/client.c文件中的native_password_auth_client()函数 |
数据包格式
类型 | 名字 | 描述 |
---|---|---|
int<3> | payload_length | 数据包中有效信息的长度,不包括数据包头部4个字节的长度 |
int<1> | sequence_id | 数据包序列号 |
int<4> | client_flag | client的标记 |
int<4> | max_packet_size | 允许发送的最大数据包的大小 |
int<1> | client_charset | client使用的字符集 |
string[23] | 无用 | |
string< null-terminated > | user_name | 登陆的用户名 |
int<1> | client_capability | |
string[20] | 加密后的scramble_data | |
string< null-terminated > | database_name | 要连接的数据库的名称 |
string< null-terminated > | scramble_plugin_name | 用于密码校验的插件名称 |
数据包解析
数据包示例
1 | 58 00 00 数据包长度 //98 |
2 | 01 数据包序号 //1 |
3 | 0d a2 2b 00 client flag |
4 | 01 00 00 00 max_packet_size |
5 | 08 client使用的字符集编码 //latin1 |
6 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 无用 |
7 | 71 62 65 6e 63 68 00 用户名 //qbench |
8 | 14 client权能标识 |
9 | f2 4c b6 e9 4c 0d d7 9d 07 30 c9 21 07 4f 23 51 09 a1 af 47 加密之后的scramble data |
10 | 6d 79 73 71 6c 00 连接的数据库 //mysql |
11 | 6d 79 73 71 6c 5f 6e 61 74 69 76 65 5f 70 61 73 73 77 6f 72 64 00 加密的插件名称 //mysql_native_password |
各分段数据含义:
- 数据包长度:MySQL协议中固定格式,前三个字节表示数据包长度,这个长度不包括固定格式的4个字节
- 数据包序号:MySQL协议中固定格式,用一个字节表示数据包的序号
- client flag :指定客户端发送数据的一些规范,
- max_packet_size:指定客户端发送数据包的最大值,
- client使用的字符集编码:client所使用的字符集编码
- 用户名:用于用户登陆的用户名
- client权能标识:client capability
- 加密之后的scramble data:将用于登陆的密码加密之后得到的结果
- 数据库名称:指定要连接的schema的名称
- 加密插件的名称:进行加密的插件的名称,这边为native_mysql_password
参考淘宝内核月报关于native_mysql_password
插件加密方式:
1 | //client: |
2 | hash_stage1 = sha1(password) |
3 | hash_stage2 = sha1(hash_stage1) |
4 | reply = sha1(scramble, hash_stage2) ^ hash_stage1 |
5 | |
6 | //server: |
7 | // 逻辑位于sql/password.c:check_scramble_sha1 |
8 | // mysql.user表中, 对应user的passwd实际上是 |
9 | |
10 | hash_stage2 res1 = sha1(scramble, hash_stage2) |
11 | hash_stage1 = reply ^ res1 |
12 | hash_stage2_reassured = sha1(hash_stage1) |
13 | //再根据hash_stage2_reassured == hash_stage2(from mysql.user)是否一致来判定是否合法 |
异常情况
在用户认证过程中,master发送给slave一个Greeting包之后,slave发送一个auth包给master,因为我们是模拟IO线程的过程,所以这个auth包的内容是我们自己组织的,在发送将auth包发送给master进行用户认证的过程中,可能会出现一些异常情况,这样会导致master返回一个非OK包,这边列举可能出现的异常情况。
-
slave发送的auth包中,用户名和密码错误,master会返回一个ERR包,ERR包中会包含‘ACCESS DENIED’的信息 1045号错误。
-
slave发送的auth包中,数据组织格式有问题。master会返回一个EOF包,并中断连接。
response ok步骤详解
认证没有问题master将直接返回一个OK包。
register_slave注册slave过程过程详解
register slave 认证时序图
1 | Slave->Master:Request Query 设置master_binlog_checksum(必须) |
2 | Master->Slave:Response OK 请求执行成功 |
3 | Slave->Master:Request Query 设置slave_uuid(非必须) |
4 | Master->Slave:Response OK 请求执行成功 |
5 | Slave->Master:Request Register Slave 发送register slave命令(非必须) |
6 | Master->Slave:Response OK 注册slave成功 |
注册slave的过程如下
- slave客户端直接SET \at master_binlog_checksum= \at\at global.binlog_checksum
- 设置正确,返回Ok Packet
- 设置其他相关配置,比如slave_uuid等
- 设置正确,返回Ok Packet
- slave发送register_slave 注册slave请求
- 设置正确,返回Ok Packet
注册slave步骤主要是为了方便主库能获得slave的IP/Port等信息,第5步可忽略
异常情况
- 第1步,第3步如果设置的命令有误,Master将返回Error Packet。
- 第5步,如果填充的register_slave Packet包错误,Master同样将返回Error Packet
接下来几节将详细描述上述第5步网络包的具体字节分段含义。
Register Slave 步骤
相关代码位置
1 | sql/rpl_slave.cc文件中的handle_slave_io()函数 |
2 | ==>sql/rpl_slave.cc文件中的register_slave_on_master()函数 |
数据包格式|
类型 | 名字 | 描述 |
---|---|---|
int<3> | payload_length | 数据包中有效信息的长度,不包括数据包头部4个字节的长度 |
int<1> | sequence_id | 数据包序列号 |
int<1> | command_type | 发送的请求的类型 |
int<4> | server_id | slave的server_id |
string< null-terminated > | report_host | |
string< null-terminated > | report_user | |
string< null-terninated > | report_password | |
int<2> | port | 端口号 |
string[8] |
数据包解析
数据包示例:
1 | 12 00 00 数据包长度 |
2 | 00 数据包序号 |
3 | 15 发送的command类型 0x15表示的是COM_REGISTER_SLAVE类型的命令 |
4 | 0f 27 00 00 slave的server_id |
5 | 00 report host |
6 | 00 report user |
7 | 00 report password |
8 | ea 0c 端口号 |
9 | 00 00 00 00 00 00 00 00 无用 |
各分段数据含义:
- 数据包长度:MySQL协议中固定格式,前三个字节表示数据包长度,这个长度不包括固定格式的4个字节
- 数据包序号:MySQL协议中固定格式,用一个字节表示数据包的序号
- COMMAND类型:表示发送的请求的类型
在my_command.h文件中定义了command的类型:
1 | enum enum_server_command |
2 | { |
3 | COM_SLEEP,COM_QUIT, |
4 | COM_INIT_DB, |
5 | COM_QUERY, |
6 | COM_FIELD_LIST, |
7 | COM_CREATE_DB, |
8 | COM_DROP_DB, |
9 | COM_REFRESH, |
10 | COM_SHUTDOWN, |
11 | COM_STATISTICS, |
12 | COM_PROCESS_INFO, |
13 | COM_CONNECT, |
14 | COM_PROCESS_KILL, |
15 | COM_DEBUG, |
16 | COM_PING, |
17 | COM_TIME, |
18 | COM_DELAYED_INSERT, |
19 | COM_CHANGE_USER, |
20 | COM_BINLOG_DUMP, |
21 | COM_TABLE_DUMP, |
22 | COM_CONNECT_OUT, |
23 | COM_REGISTER_SLAVE, |
24 | COM_STMT_PREPARE, |
25 | COM_STMT_EXECUTE, |
26 | COM_STMT_SEND_LONG_DATA, |
27 | COM_STMT_CLOSE, |
28 | COM_STMT_RESET, |
29 | COM_SET_OPTION, |
30 | COM_STMT_FETCH, |
31 | COM_DAEMON, |
32 | COM_BINLOG_DUMP_GTID, |
33 | COM_RESET_CONNECTION, |
34 | COM_END |
35 | } |
- report host:指定report的IP地址
- report user:指定report的user
- report password:指定report的账户的密码
- 端口号:指定连接的端口号,等于change master to语句中的port选项
异常情况
如果填充的register_slave Packet
包错误,Master同样将返回Error Packet
request dump请求binlog过程详解
request dump 请求时序图
1 | Slave->Master:Request Send Binlog 请求发送binlog |
2 | Master->Slave:Rotate Event 主库发送一个Rotate Event内容 |
3 | Master->Slave:Format Description Event 主库发送一个Format Description Event内容 |
4 | Master->Slave:··················Binlog Event |
5 | Master->Slave:··················Binlog Event |
6 | Master->Slave:··················Binlog Event |
请求binlog的过程如下
- slave客户端发送request dump请求,请求中包含binlog dump开始发送二进制文件名称和偏移量
- master接收到请求,位置正确的话,会先发送Rotate Event表示接下来的二进制日志将从指定的二进制文件和偏移量开始发送
- master紧接着会从指定的二进制文件头读取Format Description Event 并发送给slave,用于slave 确认各个event的header长度等信息。
- master从二进制文件和偏移量开始读取二进制数据并通过网络发给Slave
异常情况:
- 第1步,如果slave请求的二进制文件或者偏移量不正确,master将返回ERR Packet
Request dump 步骤
相关代码位置
1 | sql/rpl_slave.cc文件中的handle_slave_io()函数 |
2 | ==>sql/rpl_slave.cc文件中的request_dump()函数 |
数据包格式
类型 | 名字 | 描述 |
---|---|---|
int<3> | payload_length | 数据包中有效信息的长度,不包括数据包头部4个字节的长度 |
int<1> | sequence_id | 数据包序列号 |
int<1> | command_type | 发送的请求的类型 |
int<4> | binlog_pos | 请求的binlog位置点 |
int<2> | binlog_flag | |
int<4> | server_id | slave的server_id |
string< reset-of_packet > | binlog_file_name | 请求的binlog文件名 |
数据包解析
1 | 1b 00 00 数据包长度 |
2 | 00 数据包序号 |
3 | 12 发送的command类型 0x12表示的是COM_BINLOG_DUMP |
4 | 9a 00 00 00 binlog位置点 |
5 | 00 00 binlog flag |
6 | 0f 27 00 00 slave_server_id |
7 | 6d 79 73 71 6c 2d 62 69 6e 2e 30 30 30 30 30 31binlog文件名 |
- 数据包长度:MySQL协议中固定格式,前三个字节表示数据包长度,这个长度不包括固定格式的4个字节
- 数据包序号:MySQL协议中固定格式,用一个字节表示数据包的序号
- COMMAND类型:表示发送的请求的类型
- binlog位置点:表示请求的binlog位置
- binlog flag:
- slave_server_id:表示slave的server_id
- binlog文件名:表示请求的binlog的文件名
异常情况
- binlog 请求的位置点大于对应binlog_file中最大的位置点 Master返回一个ERR数据包,Err Packet格式参见“3.3.2 ”
示例数据包:
1 | 4f 00 00 数据包长度 |
2 | 01 数据包序号 |
3 | ff ERR数据包标识 |
4 | d4 04 error code |
5 | 23 48 59 30 30 30 43 6c 69 65 6e 74 20 72 65 71 75 65 73 74 65 64 20 6d 61 73 74 65 72 20 74 6f 20 73 74 61 72 74 20 72 65 70 6c 69 63 61 74 69 6f 6e 20 66 72 6f 6d 20 70 6f 73 69 74 69 6f 6e 20 3e 20 66 69 6c 65 20 73 69 7a 65 错误信息 |
各分段数据包含义:
- 错误号为:1236
- 错误信息为:Client requested master to start replication from position>filesize
- binlog 请求的位置点不是event起始位置点
Master返回一个ERR数据包,Err Packet格式参见上一篇文章中ERR_Packet部分内容。
1 | e1 00 00 数据包长度 |
2 | 03 数据包序号 |
3 | ff ERR数据包标识 |
4 | d4 04 error code |
5 | 23 48 59 30 30 30 62 6f 67 75 73 20 64 61 74 61 20 69 6e 20 6c 6f 67 20 65 76 65 6e 74 3b 20 74 68 65 20 66 69 72 73 74 20 65 76 65 6e 74 20 27 6d 79 73 71 6c 2d 62 69 6e 2e 30 30 30 30 30 31 27 20 61 74 20 31 36 30 2c 20 74 68 65 20 6c 61 73 74 20 65 76 65 6e 74 20 72 65 61 64 20 66 72 6f 6d 20 27 2f 6f 70 74 2f 6d 79 73 71 6c 2f 64 61 74 61 2f 62 69 6e 6c 6f 67 2f 6d 79 73 71 6c 2d 62 69 6e 2e 30 30 30 30 30 31 27 20 61 74 20 31 32 33 2c 20 74 68 65 20 6c 61 73 74 20 62 79 74 65 20 72 65 61 64 20 66 72 6f 6d 20 27 2f 6f 70 74 2f 6d 79 73 71 6c 2f 64 61 74 61 2f 62 69 6e 6c 6f 67 2f 6d 79 73 71 6c 2d 62 69 6e 2e 30 30 30 30 30 31 27 20 61 74 20 31 37 39 2e 错误信息 |
各分段含义:
- 错误号为:1236
- 错误信息为:bogusdatain log event;the first event’mysql-bin.000001’at160,the last event read from’/opt/mysql/data/binlog/mysql-bin.000001’at123,the last byte read from’/opt/mysql/data/binlog/mysql-bin.000001’at179.
- binlog 请求的binlog filename 不存在
Master返回一个ERR数据包, 数据包示例
1 | 44 00 00 数据包长度 |
2 | 01 数据包序号 |
3 | ff ERR数据包标识 |
4 | d4 04 error code |
5 | 23 48 59 30 30 30 43 6f 75 6c 64 20 6e 6f 74 20 66 69 6e 64 20 66 69 72 73 74 20 6c 6f 67 20 66 69 6c 65 20 6e 61 6d 65 20 69 6e 20 62 69 6e 61 72 79 20 6c 6f 67 20 69 6e 64 65 78 20 66 69 6c 65 错误信息 |
各分段数据含义:
- 错误号为:1236
- 错误信息为:Could not find first log filename in binary log index file
整体流程回顾
- Master与Slave建立TCP/IP连接
- Master发送第一个MySQL协议的Greeting包给Slave
- Slave根据发送的数据包,加密自己的账户及密码等信息,发送给Master
- Master根据Slave发送的auth数据包进行认证,认证成功,则发送一个Response OK数据包给Slave,Slave完成用户登录
- Slave发送一个Query的请求,设置当前连接的master_binlog_checksum为\at\at global.binlog_checksum这一步是必须的,这是设置校验码
- Master处理完Slave的Query请求之后,发送一个Response OK数据包给Slave
- Slave发送一个Query请求,设置当前连接的slave_uuid为从库的uuid,这一步非必须
- Master处理完Slave的Query请求之后,发送一个Response OK数据包给Slave
- Slave向Master发送一个Register Slave的数据包,进行slave线程的注册
- Master将slave线程注册完成之后,返回一个Response OK数据包
- Slave在注册成功之后,向Master发送Binlog Dump命令,发送请求的binlog的文件名以及pos点
- Master在收到Slave的Binlog Dump命令之后,首先发送一个Rotate Event的内容给Slave,告诉Slave当前发送的binlog的文件名为什么
- Master之后再发送一个Format Description Event内容给Slave,包括版本信息,位置点等信息
- 接着Master按照Slave的Binlog Dump请求中携带的pos点的信息,从该binlog文件偏移位置为pos的地方将Event内容取出来,发送给Slave
MySQL binlog event基础知识
event基础知识
MySQL复制实质上就是讲binlog同步到从库上进行应用。首先会介绍一些关于binlog event的基础知识,之后会详细对binlog event进行分析。
event类型
5.6.34版本的MySQL的event类型有:
1 | enum Log_event_type |
2 | { UNKNOWN_EVENT= 0, |
3 | START_EVENT_V3= 1, |
4 | QUERY_EVENT= 2, |
5 | STOP_EVENT= 3, |
6 | ROTATE_EVENT= 4, |
7 | INTVAR_EVENT= 5, |
8 | LOAD_EVENT= 6, |
9 | SLAVE_EVENT= 7, |
10 | CREATE_FILE_EVENT= 8, |
11 | APPEND_BLOCK_EVENT= 9, |
12 | EXEC_LOAD_EVENT= 10, |
13 | DELETE_FILE_EVENT= 11, |
14 | NEW_LOAD_EVENT= 12, |
15 | RAND_EVENT= 13, |
16 | USER_VAR_EVENT= 14, |
17 | FORMAT_DESCRIPTION_EVENT= 15, |
18 | XID_EVENT= 16, |
19 | BEGIN_LOAD_QUERY_EVENT= 17, |
20 | EXECUTE_LOAD_QUERY_EVENT= 18, |
21 | TABLE_MAP_EVENT = 19, |
22 | PRE_GA_WRITE_ROWS_EVENT = 20, |
23 | PRE_GA_UPDATE_ROWS_EVENT = 21, |
24 | PRE_GA_DELETE_ROWS_EVENT = 22, |
25 | WRITE_ROWS_EVENT_V1 = 23, |
26 | UPDATE_ROWS_EVENT_V1 = 24, |
27 | DELETE_ROWS_EVENT_V1 = 25, |
28 | INCIDENT_EVENT= 26, |
29 | HEARTBEAT_LOG_EVENT= 27, |
30 | IGNORABLE_LOG_EVENT= 28, |
31 | ROWS_QUERY_LOG_EVENT= 29, |
32 | WRITE_ROWS_EVENT = 30, |
33 | UPDATE_ROWS_EVENT = 31, |
34 | DELETE_ROWS_EVENT = 32, |
35 | GTID_LOG_EVENT= 33, |
36 | ANONYMOUS_GTID_LOG_EVENT= 34, |
37 | PREVIOUS_GTIDS_LOG_EVENT= 35, |
38 | ENUM_END_EVENT /* end marker */ |
39 | }; |
event类型含义
这边话介绍几个常见的event类型所对应的含义,对于不常见或者现在基本上已经不用了的event类型,这边不做过多的描述,想了解详细的内容,可以查看链接 event-meanings
-
FORMAT_DESCRIPTION_EVENT
FORMAT_DESCRIPTION_EVENT是每个binlog文件开头的一个event。
-
ROTATE_EVENT
当MySQL切换至新的binlog文件的时候,MySQL会在旧的binlog文件中写入一个ROTATE_EVENT,表示新的binlog文件的文件名,以及第一个偏移地址。当在数据库中执行FLUSH LOGS语句或者binlog文件的大小超过max_binlog_size就会切换新的binlog文件。
-
TABLE_MAP_EVENT
TABLE_MAP_EVENT只有在binlog文件是以ROW格式记录的时候,才会使用。binlog中记录的每个更改的记录之前都会有一个对应要操作的表的TABLE_MAP_EVENT。TABLE_MAP_EVENT中记录了表的定义(包括database name,table name,字段定义),并且会将这个表的定义对应于一个数字,称为table_id。设计TABLE_MAP_EVENT类型event的目的是为了当主库和从库之间有不同的表定义的时候,复制仍能进行。如果一个事务中操作了多个表,多行记录,在binlog中会将对多行记录的操作event进行分组,每组行记录操作event前面会出现对应表的TABLE_MAP_EVENT。
-
QUERY_EVENT
记录更新操作的语句。
-
WRITE_ROWS_EVENT
在以ROW格式记录的binlog文件中,WRITE_ROWS_EVENT记录了插入的行记录。
-
UPDATE_ROWS_EVENT
在以ROW格式记录的binlog文件中,UPDATE_ROWS_EVENT记录了更新的行记录。
-
DELETE_ROWS_EVENT
在以ROW格式记录的binlog文件中,DELETE_ROWS_EVENT记录了删除的行记录。
&nsbp;
event字节解析
在MySQL的发展过程中,一共出现过三个版本的event结构:
- v1:在MySQL 3.23中使用
- v2:在MySQL4.0.2至4.1中使用
- v4:在MySQL5.0及以上版本中使用
因为目前MySQL数据库版本基本上都是5.0版本以上的,所以本文就只介绍v4版本的event架构。
event组织架构
v4版本的event主要由event header部分和post-header以及variable part部分组成。
- event header是每个event都会有的部分,并且每个event header的格式是相同的,固定为19个字节长度,在event header 中记录了event_length、type_code等信息,可以表示event的长度,event的类型,下一个event的偏移位置等信息,都可以再event header中获取到。
- extra_headers指定了除公共头部外的内容,但是目前版本的binlog格式中,这一部分不存在的
- fixed_part部分在某些文章中也被称做post-header,每个类型的event之间的post-header部分是不相同的,但是同一类型的event占用的post-header
字节数是一样的。每个类型的event占用多少字节为post-header,这个定义在了Format_description_event中,下面讲Format_description_event的章节中会介绍。
- variable part是event真实记录具体信息的部分。
event字节结构表示如下图:
x:y
中x表示该部分从多少字节偏移开始,y
表示该部分占用多少字节。
- timestamp记录的是该事务记录的时间
- type_code,记录的是该event的类型,具体的event类型,见上面的Log_event_type
- server_id,记录的是执行该event的server的server_id号
- event_length,该event的长度,共占多少字节
- next_position,下一个event在binlog文件中的偏移位置
- flag:标记
- extra_headers,额外的头部内容,现在是空的,不存在
- fixed part有些时候也被称作post-header
- variable part有些时候也被称作payload或者body
event公共头部
从上述的event结构中,可以看到,每个event的header部分的内容结构是一致的,所有的event,都会记录这些信息,这是所有的event的公共头部。所以我们把这块内容单独拿出来讲。
字段 | 字节数 | 描述 |
---|---|---|
timestamp | 4字节 | 时间戳 |
type_code | 1字节 | 记录event类型 |
server_id | 4 字节 | 记录event生成的MySQL所对应的server_id |
event_length | 4字节 | 这个event的字节长度,包括event header |
next_position | 4字节 | 下一个event在binlog文件中的偏移位置 |
flags | 2字节 |
示例:我们拿一个Write_rows_event的字节内容作为示例,解析event内容:
1 | b6 1e 1c 5a //timestamp:小端存储,将16进制转换成10进制为1511792310,将时间戳转换为时间为2017-11-27 22:18:30 |
2 | 1e //type_code:event类型,0x1e转换为10进制为30,查看Log_event_type中,看到30的为WRITE_ROWS_EVENT |
3 | 0f 27 00 00 //server_id :小端存储,将16进制转换为10进制为9999 |
4 | 31 00 00 00 //event_length :小端存储,将16进制转换为10进制为49 |
5 | b1 03 00 00 //next_position:小端存储,将16进制转换为10进制为945 |
6 | 00 00 //flags: |
7 | body部分省略 |
MySQL常见binlog event解析
前面讲了binlog event的基础知识,从这部分开始我们就具体来解析binlog中常见的event,了解其具体代表的意义。本文会介绍Format_description_event、Rotate_event、Query_log_event、Rows_query_log_event几种event。
Format_description_event
Format_description_event是每个binlog文件开头的一个描述信心的event。
post-header部分
字段 | 字节数 | 描述 |
---|---|---|
binlog_version | 2字节 | binlog结构的版本,v1,v2,v4 |
server_version | 50字节 | MySQL server的数据库版本 |
timestamp | 4字节 | 创建该binlog文件的时间,单位为秒 |
header_length | 1字节 | 指定公共头部的长度,v4版本中这个值一直为19,说明其他所有的event的extra_header部分都是空,extra_header长度为header_length-19 |
event_post_header_length | variable size,跟各个版本支持的event类型总数一致 | 每个event类型,占用一个字节,表示该event类型post-header部分的长度 |
event-body部分
无
字节解析示例
1 | 公共头部部分省略 |
2 | 04 00 //binlog_version :v4版本 |
3 | 35 2e 36 2e 33 34 2d 6c 6f 67 |
4 | 00 00 00 00 00 00 00 00 00 00 |
5 | 00 00 00 00 00 00 00 00 00 00 |
6 | 00 00 00 00 00 00 00 00 00 00 |
7 | 00 00 00 00 00 00 00 00 00 00 //mysql server version :16进制转换为ascii之后,值为5.6.34-log |
8 | 00 00 00 00 //timestamp: |
9 | 13 //header length :19,说明该版本的event类型公共头部长度都为19,extra_header长度为header_length-19 |
10 | 38 0d 00 08 00 12 00 04 04 04 |
11 | 04 12 00 00 5c 00 04 1a 08 00 |
12 | 00 00 08 08 08 02 00 00 00 0a |
13 | 0a 0a 19 19 00 //event_post_header_len:各个类型event的post-header部分长度 |
14 | 01 2e ef 23 e2 |
Rotate_event
当MySQL切换至新的binlog文件的时候,MySQL会在旧的binlog文件中写入一个ROTATE_EVENT,其内容包含新的binlog文件的文件名以及第一个偏移地址。当在数据库中执行FLUSH LOGS
语句或者binlog文件的大小超过max_binlog_size
参数设定的值就会切换新的binlog文件。
post-header部分
字段 | 字节数 | 描述 |
---|---|---|
next_binlog_po | 8字节 | 下一个binlog文件起始的偏移地址 |
event-body部分
字段 | 字节数 | 描述 |
---|---|---|
next_binlog_filename | variable string | 下一个binlog文件的文件名 |
字节解析示例
1 | 公共头部部分省略 |
2 | 04 00 00 00 00 00 00 00 //next_binlog_pos:下一个binlog的起始偏移地址,小端存储,16进制转换为10进制之后为4 |
3 | 6d 79 73 71 6c 2d 62 69 6e 2e 30 30 30 30 30 32 //next_binlog_filename:下一个binlog的文件名,16进制转换为ascii之后,值为mysql-bin.000002 |
4 | b3 db 0b 9a checksum |
Query_log_event
记录更新操作的语句。
post-header部分
字段 | 字节数 | 描述 |
---|---|---|
thread_id | 4字节 | 小端存储,执行语句的线程ID号 |
exec_time | 4字节 | 小端存储,语句执行的时间 |
db_len | 1字节 | database名的长度 |
error_code | 2字节 | 错误号 |
status_vars_len | 2字节 | 小端存储,这部分,在v1和v3版本的event中是没有的,指定状态值的长度 |
event-body部分
字段 | 字节数 | 描述 |
---|---|---|
status_vars | status_vars_len字节 | 记录状态值,具体的解析见下表 |
database_name | db_len+1字节 | null-terminaled类型的字符串,记录database的名字 |
query_statement | 不定 | 执行的语句 |
check_sum | 4字节 | 校验码 |
status_vars的解析是,一个字节表示状态的类型,在类型之后按照类型不同紧接着不同字节数的状态值,状态的类型一共有:
1 | enum Query_event_status_vars |
2 | { |
3 | Q_FLAGS2_CODE= 0, |
4 | Q_SQL_MODE_CODE, |
5 | Q_CATALOG_CODE, |
6 | Q_AUTO_INCREMENT, |
7 | Q_CHARSET_CODE, |
8 | Q_TIME_ZONE_CODE, |
9 | Q_CATALOG_NZ_CODE, |
10 | Q_LC_TIME_NAMES_CODE, |
11 | Q_CHARSET_DATABASE_CODE, |
12 | Q_TABLE_MAP_FOR_UPDATE_CODE, |
13 | Q_MASTER_DATA_WRITTEN_CODE, |
14 | Q_INVOKER, |
15 | Q_UPDATED_DB_NAMES, |
16 | Q_MICROSECONDS, |
17 | Q_COMMIT_TS, |
18 | Q_COMMIT_TS2, |
19 | Q_EXPLICIT_DEFAULTS_FOR_TIMESTAMP |
20 | }; |
不同状态对应的状态值的字节数
状态 | 对用的code | 状态值占用的字节数 |
---|---|---|
Q_FLAGS2_CODE | 0 | 4字节 |
Q_SQL_MODE_CODE | 1 | 8字节 |
Q_CATALOG_CODE | 2 | 第一个字节表示catalog_len,总共catalog_len+2个字节 |
Q_AUTO_INCREMENT | 3 | 4字节 |
Q_CHARSET_CODE | 4 | 6字节 |
Q_TIME_ZONE_CODE | 5 | 第一个字节表示time_zone_len,总共time_zone_len+1字节 |
Q_CATALOG_NZ_CODE | 6 | 第一个字节表示catalog_len,总共catalog_len+1个字节 |
Q_LC_TIME_NAMES_CODE | 7 | 2字节 |
Q_CHARSET_DATABASE_CODE | 8 | 2字节 |
Q_TABLE_MAP_FOR_UPDATE_CODE | 9 | 8字节 |
Q_MASTER_DATA_WRITTEN_CODE | 10 | 4字节 |
Q_INVOKER | 11 | 包含两部分,一部分是user,一部分是host。user部分,一个字节表示user_len,接着user_len个字节表示user。host部分,一个字节表示host_len,接着host_len个字节表示host。 |
Q_UPDATED_DB_NAMES | 12 | |
Q_MICROSECONDS | 13 | 3字节 |
Q_COMMIT_TS | 14 | |
Q_COMMIT_TS2 | 15 | |
Q_EXPLICIT_DEFAULTS_FOR_TIMESTAMP | 16 | 1字节 |
字节解析示例
执行语句:
1 | insert into test1(`name`) values('beijing'); |
2 | |
3 | root : (none) 06:07:04> show binlog events in 'mysql-bin.000012'; |
4 | +------------------+-----+-------------+-----------+-------------+-------------------------------------------------------------+ |
5 | | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | |
6 | +------------------+-----+-------------+-----------+-------------+-------------------------------------------------------------+ |
7 | | mysql-bin.000012 | 4 | Format_desc | 330619 | 120 | Server ver: 5.6.34-log, Binlog ver: 4 | |
8 | | mysql-bin.000012 | 120 | Query | 330619 | 212 | BEGIN | |
9 | | mysql-bin.000012 | 212 | Intvar | 330619 | 244 | INSERT_ID=28 | |
10 | | mysql-bin.000012 | 244 | Query | 330619 | 374 | use `gangshen`; insert into test1(`name`) values('beijing') | |
11 | | mysql-bin.000012 | 374 | Xid | 330619 | 405 | COMMIT /* xid=2961 */ | |
12 | +------------------+-----+-------------+-----------+-------------+-------------------------------------------------------------+ |
13 | 5 rows in set (0.00 sec) |
使用mysqlbinlog工具解析binlog文件
1 | # at 244 |
2 | #180105 15:20:29 server id 330619 end_log_pos 244 CRC32 0xf41b5eaa Intvar |
3 | SET INSERT_ID=28/*!*/; |
4 | #180105 15:20:29 server id 330619 end_log_pos 374 CRC32 0x98379221 Query thread_id=106404 exec_time=0 error_code=0 |
5 | use `gangshen`/*!*/; |
6 | SET TIMESTAMP=1515183629/*!*/; |
7 | insert into test1(`name`) values('beijing') |
8 | /*!*/; |
9 | # at 374 |
解析二进制文件:
1 | 公共头部部分省略。。。。 |
2 | a4 9f 01 00 //thread_id:执行语句的线程号,小端存储,转换为16进制为106404 |
3 | 00 00 00 00 //exec_time:执行语句的时间,小端存储,转化为16进制为0 |
4 | 08 //db_len:database name的长度,转换为10进制为8 |
5 | 00 00 //error_code:错误号,小端存储,转换为10进制为0 |
6 | 2a 00 //status_vars_len:状态值的长度,小端存储,转换为10进制为42,表示后续的42个字节为状态值的内容 |
7 | //status_vars_len |
8 | 00 00 00 00 00 |
9 | 01 00 00 20 40 00 00 00 00 |
10 | 06 03 73 74 64 |
11 | 03 02 00 02 00 |
12 | 04 21 00 21 00 53 00 |
13 | 0c 01 67 61 6e 67 73 68 65 6e 00 |
14 | 67 61 6e 67 73 68 65 6e 00 //database_name:数据库名称,null-terminaled string类型 ,转换为字符串为gangshen |
15 | 696e7365727420696e746f20746573743128606e616d6560292076616c75657328276265696a696e672729 //query_statement:执行的语句,转换为字符串为:insert into test1(`name`) values('beijing') |
16 | 21 92 37 98//checksum |
Rows_query_log_event
在row格式复制模式下,将query以语句形式记录。在5.6.2版本之后,可以通过设置binlog_rows_query_log_events
参数来控制在row格式复制模式下是否需要将query语句记录到binlog文件中。
post-header部分
无
event-body部分
字段 | 字节数 | 描述 |
---|---|---|
str_len | 1字节 | 记录语句长度 |
statement | str_len字节 | 对应的语句 |
checksum | 4字节 | 校验码 |
字节解析示例
执行语句:
1 | root : gangshen 09:11:56> insert into test1(`name`) values('rows_query'); |
2 | Query OK, 1 row affected (0.04 sec) |
3 | |
4 | root : gangshen 09:12:06> show binlog events in 'mysql-bin.000014'; |
5 | +------------------+-----+-------------+-----------+-------------+--------------------------------------------------+ |
6 | | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | |
7 | +------------------+-----+-------------+-----------+-------------+--------------------------------------------------+ |
8 | | mysql-bin.000014 | 4 | Format_desc | 330619 | 120 | Server ver: 5.6.34-log, Binlog ver: 4 | |
9 | | mysql-bin.000014 | 120 | Query | 330619 | 201 | BEGIN | |
10 | | mysql-bin.000014 | 201 | Rows_query | 330619 | 271 | # insert into test1(`name`) values('rows_query') | |
11 | | mysql-bin.000014 | 271 | Table_map | 330619 | 326 | table_id: 98 (gangshen.test1) | |
12 | | mysql-bin.000014 | 326 | Write_rows | 330619 | 377 | table_id: 98 flags: STMT_END_F | |
13 | | mysql-bin.000014 | 377 | Xid | 330619 | 408 | COMMIT /* xid=3032 */ | |
14 | +------------------+-----+-------------+-----------+-------------+--------------------------------------------------+ |
15 | 6 rows in set (0.00 sec) |
使用mysqlbinlog工具解析binlog文件
1 | # at 201 |
2 | #180108 9:12:06 server id 330619 end_log_pos 271 CRC32 0xdc68af8a Rows_query |
3 | # insert into test1(`name`) values('rows_query') |
4 | # at 271 |
解析二进制文件:
1 | 公共头部部分省略。。。 |
2 | 2e //str_len:执行语句的长度,转换为10进制为46 |
3 | 696e7365727420696e746f20746573743128606e616d6560292076616c7565732827726f77735f71756572792729 //statement:执行的语句,转换为ascii为insert into test1(`name`) values('rows_query') |
4 | 8a af 68 dc //check_sum |
Table_map_event
TABLE_MAP_EVENT只有在binlog文件是以ROW格式记录的时候,才会使用。binlog中记录的每个更改的记录之前都会有一个对应要操作的表的TABLE_MAP_EVENT。TABLE_MAP_EVENT中记录了表的定义(包括database name,table name,字段定义),并且会将这个表的定义对应于一个数字,称为table_id。设计TABLE_MAP_EVENT类型event的目的是为了当主库和从库之间有不同的表定义的时候,复制仍能进行。如果一个事务中操作了多个表,多行记录,在binlog中会将对多行记录的操作event进行分组,每组行记录操作event前面会出现对应表的TABLE_MAP_EVENT。
post-header部分
字段 | 字节数 | 描述 |
---|---|---|
table_id | 6字节 | 操作的表的table_id |
flags | 2字节 | 目前版本没有用,都是0,保留给之后的版本使用 |
event-body部分
字段 | 字节数 | 描述 |
---|---|---|
database_name | 1个字节表示字符串长度,之后接一个null-terminated类型的字符串 | |
table_name | 1个字节表示字符串长度,之后接一个null-terminated类型的字符串 | 操作的表的名称 |
column_count | packed integer(1或3或4或9个字节) | 对应表中的字段数量 |
column_type | 每个字段占用一个字节,字段类型定义在enum_field_types中 | 字段类型 |
metadata_length | packed integer(1或3或4或9个字节) | 对应字段的元数据信息的长度 |
metadata | 根据enum_field_type中的定义确定不同字段的元数据,如果某字段类型没有元数据,则不记录 | 每个字段的元数据信息,比如varchar字段需要记录最长长度 |
null_bits | int((column_count + 7)/8) 字节 | 一个bit表示一个字段是否可以为NULL,顺序是:第一个字节的最低位开始向最高位增长,之后第二个字节的最低位开始向最高位增长,以此类推 |
check_sum | 4字节 | 校验码 |
字节解析示例
建表语句:
1 | CREATE TABLE `test1` ( |
2 | `id` int(11) NOT NULL AUTO_INCREMENT, |
3 | `name` varchar(20) COLLATE utf8_bin DEFAULT NULL, |
4 | PRIMARY KEY (`id`) |
5 | ) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8 COLLATE=utf8_bin |
二进制内容解析:
1 | 公共头部部分省略 |
2 | 6c 00 00 00 00 00 //table_id :小端存储,16进制转换为10进制为108 |
3 | 01 00 //flag : |
4 | 08 67 61 6e 67 73 68 65 6e 00 //database_name :1个字节是字符串长度,后接一个null-terminated string 第一个字节表示字符串长度为8,后面内容将16进制转换为ascii字符为gangshen |
5 | 05 74 65 73 74 31 00 //table_name: 1个字节是字符串长度,后接一个null-terminated string第一个字节表示字符串长度为5,后面内容将16进制转换为ascii字符为test1 |
6 | 02 //columns count :packet integer类型,转换之后,数值为2 表示表中有两个字段 |
7 | 03 0f //column type : 一个字节表示一个字段的类型,字段类型定义在enum enum_field_types ,分别是一个int类型以及一个varchar类型,具体的字段类型及记录格式等信息可以查看《MySQL中Rows_event中字段表示》文档 |
8 | 02 //metadata length : packet integer类型,转换之后,数值为2,表示记录表中的metadata内容占用2个字节 |
9 | 14 00 // :varchar 的max length 因为int没有metadata所以跳过 |
10 | 02 //null_bits :int((column_count + 7) / 8)个字节 一个bit表示一个字段是否可以为null |
11 | 52 53 4a d9 checksum |
Write_rows_log_event
在以ROW格式记录的binlog文件中,Write_rows_log_event记录了插入的行记录。
post-header部分
字段 | 字节数 | 描述 |
---|---|---|
table_id | 6字节 | 操作的表的table_id |
flags | 2字节 | 目前版本没有用,都是0,保留给之后的版本使用 |
event-body部分
字段 | 字节数 | 描述 |
---|---|---|
var_header | 2字节 | |
m_width | packed integer(1或3或4或9个字节) | 表中字段数量 |
before_image | (m_width + 7)/8字节 | |
after_bitmap_bits | (m_with +7)/8字节 | 字段值是否为空标记,一个bit表示一个字段是否为NULL,顺序是:第一个字节的最低位开始向最高位增长,之后第二个字节的最低位开始向最高位增长,以此类推 |
after_column_content | 不定 | 按照顺序每个字段的内容 |
check_sum | 4字节 | 校验码 |
字节解析示例
建表语句
1 | CREATE TABLE `int_table` ( |
2 | `col1` tinyint(4) DEFAULT NULL, |
3 | `col2` smallint(6) DEFAULT NULL, |
4 | `col3` mediumint(9) DEFAULT NULL, |
5 | `col4` int(11) DEFAULT NULL, |
6 | `col5` bigint(20) DEFAULT NULL, |
7 | `col6` tinyint(1) DEFAULT NULL |
8 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin |
插入数据:
1 | insert into int_table values(1,11,111,1111,11111,true); |
对应Write_rows_log_event使用mysqlbinlog解析
1 | # at 340 |
2 | #180103 10:21:20 server id 330619 end_log_pos 395 CRC32 0x50177e10 Write_rows: table id 100 flags: STMT_END_F |
3 | ### INSERT INTO `gangshen`.`int_table` |
4 | ### SET |
5 | ### @1=1 /* TINYINT meta=0 nullable=1 is_null=0 */ |
6 | ### @2=11 /* SHORTINT meta=0 nullable=1 is_null=0 */ |
7 | ### @3=111 /* MEDIUMINT meta=0 nullable=1 is_null=0 */ |
8 | ### @4=1111 /* INT meta=0 nullable=1 is_null=0 */ |
9 | ### @5=11111 /* LONGINT meta=0 nullable=1 is_null=0 */ |
10 | ### @6=1 /* TINYINT meta=0 nullable=1 is_null=0 */ |
11 | # at 395 |
解析二进制文件:
1 | 公共头部部分省略 |
2 | 64 00 00 00 00 00 //table_id: 小端存储,16进制转换为10进制为100 |
3 | 01 00 //flag: |
4 | 02 00 //var header length :小端存储,16进制转换为10进制为2 |
5 | 06 //m_width :packet integer,表示表中字段数量 |
6 | ff //before image: (m_width + 7) / 8字节 |
7 | c0 //after_bitmap_bits :insert插入之后的记录的NULL标记,表中六个字段,插入的值都不为NULL |
8 | //after_columns_content:insert插入记录之后的记录值 |
9 | 01 |
10 | 0b 00 |
11 | 6f 00 00 |
12 | 57 04 00 00 |
13 | 67 2b 00 00 00 00 00 00 |
14 | 01 |
15 | 10 7e 17 50 //checksum |
Update_rows_log_event
在以ROW格式记录的binlog文件中,Update_rows_log_event记录了更新的行记录。
post-header部分
字段 | 字节数 | 描述 |
---|---|---|
table_id | 6字节 | 操作的表的table_id |
flags | 2字节 | 目前版本没有用,都是0,保留给之后的版本使用 |
event-body部分
字段 | 字节数 | 描述 |
---|---|---|
var_header | 2字节 | |
m_width | packed integer(1或3或4或9个字节) | 表中字段数量 |
before_image | (m_with+7)/8字节 | |
after_image | (m_with+7)/8字节 | |
before_bitmap_bits | (m_with+7)/8字节 | before_bitmap_bits记录的是update更改之前的记录中,字段值是否为NULL标记,一个bit表示一个字段是否为NULL,顺序是:第一个字节的最低位开始向最高位开始增长,之后第二个字节的最低位开始向最高位增长,以此类推 |
before_column_content | 不定 | update更新之前的记录值,按照顺序每个字段的内容 |
after_bitmap_bits | (m_with+7)/8字节 | after_bitmap_bits记录的是update更改之前的记录中,字段值是否为NULL标记,一个bit表示一个字段是否为NULL,顺序是:第一个字节的最低位开始向最高位开始增长,之后第二个字节的最低位开始向最高位增长,以此类推 |
after_column_content | 不定 | update更新之后的记录值,按照顺序每个字段的内容 |
check_sum | 4字节 | 校验码 |
字节解析示例
建表语句
1 | CREATE TABLE `int_table` ( |
2 | `col1` tinyint(4) DEFAULT NULL, |
3 | `col2` smallint(6) DEFAULT NULL, |
4 | `col3` mediumint(9) DEFAULT NULL, |
5 | `col4` int(11) DEFAULT NULL, |
6 | `col5` bigint(20) DEFAULT NULL, |
7 | `col6` tinyint(1) DEFAULT NULL |
8 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin |
插入数据并更新:
1 | root@localhost : gangshen 10:20:49> insert into int_table values(1,11,111,1111,11111,true); |
2 | Query OK, 1 row affected (0.03 sec) |
3 | |
4 | root@localhost : gangshen 10:21:20> update int_table set col2=22,col3=222 where col1=1; |
5 | Query OK, 1 row affected (0.02 sec) |
6 | Rows matched: 1 Changed: 1 Warnings: 0 |
7 | |
8 | root@localhost : gangshen 10:21:44> select * from int_table; |
9 | +------+------+------+------+-------+------+ |
10 | | col1 | col2 | col3 | col4 | col5 | col6 | |
11 | +------+------+------+------+-------+------+ |
12 | | 1 | 22 | 222 | 1111 | 11111 | 1 | |
13 | +------+------+------+------+-------+------+ |
14 | 1 row in set (0.00 sec) |
对应Update_rows_log_event使用mysqlbinlog解析
1 | # at 644 |
2 | #180103 10:41:37 server id 9999 end_log_pos 720 CRC32 0x5576b52f Update_rows: table id 231 flags: STMT_END_F |
3 | ### UPDATE `gangshen`.`int_table` |
4 | ### WHERE |
5 | ### @1=1 /* TINYINT meta=0 nullable=1 is_null=0 */ |
6 | ### @2=11 /* SHORTINT meta=0 nullable=1 is_null=0 */ |
7 | ### @3=111 /* MEDIUMINT meta=0 nullable=1 is_null=0 */ |
8 | ### @4=1111 /* INT meta=0 nullable=1 is_null=0 */ |
9 | ### @5=11111 /* LONGINT meta=0 nullable=1 is_null=0 */ |
10 | ### @6=1 /* TINYINT meta=0 nullable=1 is_null=0 */ |
11 | ### SET |
12 | ### @1=1 /* TINYINT meta=0 nullable=1 is_null=0 */ |
13 | ### @2=22 /* SHORTINT meta=0 nullable=1 is_null=0 */ |
14 | ### @3=222 /* MEDIUMINT meta=0 nullable=1 is_null=0 */ |
15 | ### @4=1111 /* INT meta=0 nullable=1 is_null=0 */ |
16 | ### @5=11111 /* LONGINT meta=0 nullable=1 is_null=0 */ |
17 | ### @6=1 /* TINYINT meta=0 nullable=1 is_null=0 */ |
18 | # at 720 |
解析二进制文件:
1 | 公共头部部分省略。。。 |
2 | 64 00 00 00 00 00 //table_id: 小端存储,16进制转换为10进制为100 |
3 | 01 00 //flag: |
4 | 02 00 //var header length :小端存储,16进制转换为10进制为2 |
5 | 06 //m_width :packet integer,表示表中字段数量 |
6 | ff //before image: (m_width + 7) / 8字节 |
7 | ff //after image: (m_width + 7) / 8字节 |
8 | c0 //before_bitmap_bits :update更新之前记录中NULL标记,表中六个字段,值都不为NULL |
9 | //before_column_content接下来是update更新之前记录的值 |
10 | 01 |
11 | 0b 00 |
12 | 6f 00 00 |
13 | 57 04 00 00 |
14 | 67 2b 00 00 00 00 00 00 |
15 | 01 |
16 | c0 //after_bitmap_bits :update更新之后记录中NULL标记,表中六个字段,值都不为NULL |
17 | //after_column_content接下来是update更新之前记录的值 |
18 | 01 |
19 | 16 00 |
20 | de 00 00 |
21 | 57 04 00 00 |
22 | 67 2b 00 00 00 00 00 00 |
23 | 01 |
24 | 16 a3 de bb //checksum |
Delete_rows_log_event
在以ROW格式记录的binlog文件中,Delete_rows_log_event记录了删除的行记录。
post-header部分
字段 | 字节数 | 描述 |
---|---|---|
table_id | 6字节 | 操作的表的table_id |
flags | 2字节 | 目前版本没有用,都是0,保留给之后的版本使用 |
event-body部分
字段 | 字节数 | 描述 |
---|---|---|
var_header | 2字节 | |
m_width | packed integer(1或3或4或9个字节) | 表中字段数量 |
after_image | (m_width + 7)/8字节 | |
before_bitmap_bits | (m_with+7)/8字节 | before_bitmap_bits记录的是update更改之前的记录中,字段值是否为NULL标记,一个bit表示一个字段是否为NULL,顺序是:第一个字节的最低位开始向最高位开始增长,之后第二个字节的最低位开始向最高位增长,以此类推 |
before_column_content | 不定 | delete删除之前的记录值,按照顺序每个字段的内容 |
check_sum | 4字节 | 校验码 |
字节解析示例
建表语句
1 | CREATE TABLE `int_table` ( |
2 | `col1` tinyint(4) DEFAULT NULL, |
3 | `col2` smallint(6) DEFAULT NULL, |
4 | `col3` mediumint(9) DEFAULT NULL, |
5 | `col4` int(11) DEFAULT NULL, |
6 | `col5` bigint(20) DEFAULT NULL, |
7 | `col6` tinyint(1) DEFAULT NULL |
8 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin |
删除数据:
1 | delete from int_table where col1=1; |
对应Delete_rows_log_event使用mysqlbinlog解析
1 | # at 320 |
2 | #180103 14:24:54 server id 330619 end_log_pos 375 CRC32 0xce4818b2 Delete_rows: table id 100 flags: STMT_END_F |
3 | ### DELETE FROM `gangshen`.`int_table` |
4 | ### WHERE |
5 | ### @1=1 /* TINYINT meta=0 nullable=1 is_null=0 */ |
6 | ### @2=22 /* SHORTINT meta=0 nullable=1 is_null=0 */ |
7 | ### @3=222 /* MEDIUMINT meta=0 nullable=1 is_null=0 */ |
8 | ### @4=1111 /* INT meta=0 nullable=1 is_null=0 */ |
9 | ### @5=11111 /* LONGINT meta=0 nullable=1 is_null=0 */ |
10 | ### @6=1 /* TINYINT meta=0 nullable=1 is_null=0 */ |
11 | # at 375 |
解析二进制文件:
1 | 公共头部部分省略 |
2 | 64 00 00 00 00 00 //table_id: 小端存储,16进制转换为10进制为100 |
3 | 01 00 //flag: |
4 | 02 00 //var header length :小端存储,16进制转换为10进制为2 |
5 | 06 //m_width :packet integer,表示表中字段数量 |
6 | ff //after image: (m_width + 7) / 8字节 |
7 | c0 //before_bitmap_bits :delete删除之前的记录的NULL标记,表中六个字段,值都不为NULL |
8 | //before_columns_content:delete删除记录之前的记录值 |
9 | 01 |
10 | 16 00 |
11 | de 00 00 |
12 | 57 04 00 00 |
13 | 67 2b 00 00 00 00 00 00 |
14 | 01 |
15 | 10 7e 17 50 //checksum |
Xid_event
表示支持内部XA的存储引擎上的事务提交。正常的事务是通过QUERY_EVENT来发送一个BEGIN语句并且通过QUERY_EVENT来发送一个COMMIT语句(如果事务回滚则发送的是ROLLBACK)实现。
post-header部分
无
event-body部分
字段 | 字节数 | 描述 |
---|---|---|
xid | 8字节 | xid号,小端存储,无符号数 |
check_sum | 4字节 | 校验码 |
字节解析示例
使用mysqlbinlog工具解析对应的Xid_event
1 | # at 1691 |
2 | #180103 15:30:45 server id 330619 end_log_pos 1722 CRC32 0x8816181c Xid = 2698 |
3 | COMMIT/*!*/; |
4 | # at 1722 |
解析二进制文件:
1 | 公共头部部分省略... |
2 | 8a 0a 00 00 00 00 00 00 //xid,因为是小端存储,所以实际为0x 00 00 00 00 00 00 0a 8a,转换为10进制为2698 |
3 | 1c 18 16 88 //check_sum |
Gtid_log_event
开启GTID模式的场景下,每次事务commit提交时,MySQL会在binlog中事务开始写入一个Gtid_log_event,记录该事务的GTID事务号。
post-header部分
字段 | 字节数 | 描述 |
---|---|---|
commit_flag | 1字节 | 标记事务是否提交,非0为已提交,为0表示,事务未提交 |
sid | 16字节 | 记录GTID中uuid的部分(不记录‘-’),每1个字节表示uuid中2个字符 |
gno | 8字节 | 小端存储,GTID中的事务号部分 |
event-body部分
字段 | 字节数 | 描述 |
---|---|---|
xid | 8字节 | xid号,小端存储,无符号数 |
check_sum | 4字节 | 校验码 |
字节解析示例
使用mysqlbinlog工具解析binlog文件
1 | # at 191 |
2 | #180109 18:31:08 server id 330619 end_log_pos 239 CRC32 0xd20330e8 GTID [commit=yes] |
3 | SET @@SESSION.GTID_NEXT= '89fbcea2-da65-11e7-a851-fa163e618bac:5'/*!*/; |
4 | # at 239 |
解析二进制文件:
1 | 公共头部部分省略。。。。 |
2 | 01 //commit_flag:标记事务是否已提交,表示已提交 |
3 | 89 fb ce a2 da 65 11 e7 a8 51 fa 16 3e 61 8b ac //sid:GTID中uuid部分,转换为uuid为89fbcea2-da65-11e7-a851-fa163e618bac |
4 | 05 00 00 00 00 00 00 00 //gno:GTID中的事务号,小端存储,转换为10进制为5 |
5 | e8 30 03 d2 //checksum |
Previous_gtid_log_event
在开启GTID的模式下,每个binlog文件开始会记录一个Previous_gtid_log_event,Previous_gtid_log_event中会包含当前binlog之前所有binlog中的GTID集合。
post-header部分
字段 | 字节数 | 描述 |
---|---|---|
n_sids | 8字节 | 小端存储,记录之后有几个GTID中的uuid号 |
sid | 16字节 | 记录GTID中uuid的部分(不记录‘-’),每1个字节表示uuid中2个字符 |
n_intervals | 8字节 | 记录每个sid对应有几个间隔,指的是事务号间隔 |
start | 8字节 | 每个事务号间隔中的起始事务号 |
end | 8字节 | 每个事务号间隔中的结束事务号+1 |
event-body部分
字段 | 字节数 | 描述 |
---|---|---|
xid | 8字节 | xid号,小端存储,无符号数 |
check_sum | 4字节 | 校验码 |
字节解析示例
使用mysqlbinlog工具解析binlog文件
1 | # at 120 |
2 | #180111 14:10:27 server id 330619 end_log_pos 279 CRC32 0x94a92478 Previous-GTIDs |
3 | # 89fbcea2-da65-11e7-a851-fa163e618bac:1-5:999:1050-1052, |
4 | # aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1-2:5-7 |
5 | DELIMITER ; |
解析二进制文件:
1 | 公共头部部分省略。。。 |
2 | 02 00 00 00 00 00 00 00 //n_sids:记录之后有几个GTID中的uuid号,小端存储,转换为10进制为2,表后后续有2个uuid号 |
3 | 89 fb ce a2 da 65 11 e7 a8 51 fa 16 3e 61 8b ac //sid:GTID中的uuid号转换为uuid为89fbcea2-da65-11e7-a851-fa163e618bac |
4 | 03 00 00 00 00 00 00 00 //n_intervals:对应的sid中中对应几个事务号间隔,小端存储,转换为10进制为3,表示有3个事务号间隔 |
5 | 01 00 00 00 00 00 00 00 //start:事务号间隔中的起始事务号,小端存储,转换为10进制为1 |
6 | 06 00 00 00 00 00 00 00 //end:事务号间隔中的结束事务号+1,小端存储,转换为10进制为6,实际的间隔结束事务号为5 |
7 | e7 03 00 00 00 00 00 00 //start:事务号间隔中的起始事务号,小端存储,转换为10进制为999 |
8 | e8 03 00 00 00 00 00 00 //end:事务号间隔中的结束事务号+1,小端存储,转换为10进制为1000,实际的间隔结束事务号为999 |
9 | 1a 04 00 00 00 00 00 00 //start:事务号间隔中的起始事务号,小端存储,转换为10进制为1050 |
10 | 1d 04 00 00 00 00 00 00 //end:事务号间隔中的结束事务号+1,小端存储,转换为10进制为1053,实际的间隔结束事务号为1052 |
11 | aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa aa //sid:GTID中的uuid号转换为uuid为aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa |
12 | 02 00 00 00 00 00 00 00 //n_intervals:对应的sid中中对应几个事务号间隔,小端存储,转换为10进制为2,表示有2个事务号间隔 |
13 | 01 00 00 00 00 00 00 00 //start:事务号间隔中的起始事务号,小端存储,转换为10进制为1 |
14 | 03 00 00 00 00 00 00 00 //end:事务号间隔中的结束事务号+1,小端存储,转换为10进制为3,实际的间隔结束事务号为2 |
15 | 05 00 00 00 00 00 00 00 //start:事务号间隔中的起始事务号,小端存储,转换为10进制为5 |
16 | 08 00 00 00 00 00 00 00 //end:事务号间隔中的结束事务号+1,小端存储,转换为10进制为8,实际的间隔结束事务号为7 |
17 | 78 24 a9 94 //checksum |
Anonymous_gtid_log_event
MySQL在binlog中记录每一个匿名事务之前会记录一个Anonymous_gtid_log_event表示接下来的事务是一个匿名事务。
注意:因为在5.6.34中并不会产生Anonymous_gtid_log_event,所以以下内容是基于5.7.19版本解析
post-header部分
字段 | 字节数 | 描述 |
---|---|---|
gtid_flags | 1字节 | 记录binlog格式,如果gtid_flags值为1,表示binlog中可能有以statement方式记录的binlog,如果为0表示,binlog中只有以row格式记录的binlog |
sid | 16字节 | 记录GTID中uuid的部分(不记录‘-’),每1个字节表示uuid中2个字符 |
gno | 8字节 | 小端存储,GTID中的事务号部分 |
logical_timestamp_typecode | 1字节 | 判断是否有last_commit和sequence_no,在logical_tmiestamp_typecode=2的情况下,有last_commit和sequence_no |
last_commit | 8字节 | 小端存储,上次提交的事务号 |
sequence_no | 8字节 | 小端存储,本次提交的序列号 |
event-body部分
字段 | 字节数 | 描述 |
---|---|---|
xid | 8字节 | xid号,小端存储,无符号数 |
check_sum | 4字节 | 校验码 |
字节解析示例
使用mysqlbinlog工具解析binlog文件
1 | #180109 10:53:54 server id 9999 end_log_pos 5681 CRC32 0x46be0639 Anonymous_GTID last_committed=20 sequence_number=21 rbr_only=yes |
2 | /*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/; |
3 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS'/*!*/; |
4 | # at 5681 |
解析二进制文件:
1 | 公共头部部分省略。。。 |
2 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 //sid: |
3 | 00 00 00 00 00 00 00 00 //gno: |
4 | 02 //logical_timestamp_typecode:判断是否有last_commit和sequence_no,在logical_tmiestamp_typecode=2的情况下,有last_commit和sequence_no,所以这边的话,后续有last_commit和sequence_no部分的内容 |
5 | 14 00 00 00 00 00 00 00 //last_commit:上次提交的事务号,小端存储,转换为10机制为20 |
6 | 15 00 00 00 00 00 00 00 //sequence_no:本次提交的序列号,小端存储,转换为10进制为21 |
7 | 39 06 be 46 //checksum:校验码 |
MySQL字段存储格式
介绍MySQL中常见类型字段具体的存储格式,用于解析前面讲到的Write_rows_log_event、Update_rows_event、Delete_rows_event中具体关于数据的部分,可以解析出某个字段具体记录了哪些值,具体的值是什么。
首先从数值型字段类型开始,数值型字段类型又可以分为整数类型、定点数数据类型、浮点数数据类型、比特数据类型。
数值型字段类型介绍
整数类型
-
TINYINT[(M)] [UNSIGNED] [ZEROFILL]
整数,用一个字节表示。分为有符号类型和无符号类型,有符号类型的TINYINT范围是-128到127,无符号类型的TINYINT范围是0到255。
-
SMALLINT[(M)] [UNSIGNED] [ZEROFILL]
整数,用两个字节表示。分为有符号类型和无符号类型,有符号类型的SMALLINT范围是-32768到32767,无符号类型的SMALLINT范围是0到65535
-
MEDIUMINT[(M)] [UNSIGNED] [ZEROFILL]
整数,用三个字节表示。分为有符号类型和无符号类型,有符号类型的MEDIUMINT范围是-8388608到8388607,无符号类型的MEDIUMINT范围是0到16777215
-
INT[M] [UNSIGNED] [ZEROFILL]
整数,用四个字节表示。分为有符号类型和无符号类型,有符号类型的INT范围是-2147483648到2147483647,无符号类型的INT范围是0到4294967295
-
BIGINT[M] [UNSIGNED] [ZEROFILL]
整数,用八个字节表示。分为有符号类型和无符号类型,有符号类型的BIGINT范围是-9223372036854775808到9223372036854775807,无符号类型的BIGINT范围是0到18446744073709551615SERIAL是BIGINT UNSIGNED NOT NULL AUTO_INCREMENT UNIQUE的别名
-
BOOL,BOOLEAN
TINYINT(1)的别名。
-
INTEGER[(M)] [UNSIGNED] [ZEROFILL]
INT的别名
定点数数据类型 DECIMAL,NUMERIC
- DECIMAL[(M|,D)] [UNSIGNED] [ZEROFILL]
小数,M表示数据的精度,表示数据的总长度,小数点和符号不占长度;D表示数据的标度,指小数点后面数字的位数。
M的范围是0到65,默认为10;D的范围是0到30,默认为0。
- DEC[(M[,D])] [UNSIGNED] [ZEROFILL], NUMERIC[(M[,D])] [UNSIGNED] [ZEROFILL], FIXED[(M[,D])] [UNSIGNED] [ZEROFILL]
DECIMAL的别名
浮点数数据类型 FLOAT,DOUBLE
-
FLOAT[M,D] [UNSIGNED] [ZEROFILL]
单精度浮点数,用四个字节表示。允许的值是-3.402823466E+38 到-1.175494351E-38, 0, 1.175494351E-38 到 3.402823466E+38。但是这只是理论的数值范围,实际的范围区间取决于硬件平台。M表示数据的精度,表示数据的总长度,小数点和符号不占长度;D表示数据的标度,指小数点后面数字的位数。M和D的默认值取决于硬件平台。单精度的浮点数大约是7个小数点。
-
DOUBLE[(M,D)] [UNSIGNED] [ZEROFILL]
双精度浮点数,用八个字节表示。允许的值是-1.7976931348623157E+308 到 -2.2250738585072014E-308, 0, 和 2.2250738585072014E-308 到 1.7976931348623157E+308。但是这只是理论的数值范围,实际的范围区间取决于硬件平台。M表示数据的精度,表示数据的总长度,小数点和符号不占长度;D表示数据的标度,指小数点后面数字的位数。M和D的默认值取决于硬件平台。双精度的浮点数大约是15个小数点。
-
DOUBLE PRECISION[(M,D)] [UNSIGNED] [ZEROFILL], REAL[(M,D)] [UNSIGNED] [ZEROFILL
DOUBLE的别名
比特数据类型 BIT
- BIT[(M)]
比特数据类型。M表示每个值占用的bit位数,M的范围是1到64。如果M没有指定,则默认值为1。
数值型数据解析
解析环境描述
表结构定义
1 | CREATE TABLE `number_table` ( |
2 | `col1` tinyint(4) DEFAULT NULL, |
3 | `col2` smallint(6) DEFAULT NULL, |
4 | `col3` mediumint(9) DEFAULT NULL, |
5 | `col4` int(11) DEFAULT NULL, |
6 | `col5` bigint(20) DEFAULT NULL, |
7 | `col6` decimal(25,10) DEFAULT NULL, |
8 | `col7` float DEFAULT NULL, |
9 | `col8` double DEFAULT NULL, |
10 | `col9` bit(5) DEFAULT NULL |
11 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin |
表数据展示
1 | root@localhost : gangshen 02:59:24> insert into number_table values(2,-22,222,-2222,22222,123123123123.112233,123.1,123.2,b'00110'); |
2 | Query OK, 1 row affected (0.04 sec) |
3 | |
4 | root@localhost : gangshen 03:03:39> select * from number_table; |
5 | +------+------+------+-------+-------+-------------------------+-------+-------+------+ |
6 | | col1 | col2 | col3 | col4 | col5 | col6 | col7 | col8 | col9 | |
7 | +------+------+------+-------+-------+-------------------------+-------+-------+------+ |
8 | | 2 | -22 | 222 | -2222 | 22222 | 123123123123.1122330000 | 123.1 | 123.2 | | |
9 | +------+------+------+-------+-------+-------------------------+-------+-------+------+ |
10 | 1 row in set (0.00 sec) |
然后从binlog文件中拿到对应的Table_map_event
和Writes_rows_event
对应的字节内容,开始解析
元数据存储格式
Table_map_event字节数据解析
1 | 公共头部部分省略 |
2 | 5d 00 00 00 00 00 //table_id |
3 | 01 00 //flag |
4 | 08 67 61 6e 67 73 68 65 6e 00 //database_name :1个字节是字符串长度,后接一个null-terminated string 第一个字节表示字符串长度为8,后面内容将16进制转换为ascii字符为gangshen |
5 | 0c 6e 75 6d 62 65 72 5f 74 61 62 6c 65 00 //table_name: 1个字节是字符串长度,后接一个null-terminated string第一个字节表示字符串长度为12,后面内容将16进制转换为ascii字符为number_table |
6 | 09 //columns count :packet integer类型,转换之后,数值为9 表示表中有9个字段 |
7 | 01 02 09 03 08 f6 04 05 10 //column types |
8 | 06 //metadata_length |
9 | 19 0a //col6 metadata |
10 | 04 //col7 metadata |
11 | 08 //col8 metadata |
12 | 05 00 //col9 metadata |
13 | ff 01 //null_bits :int((column_count + 7) / 8)个字节 一个bit表示一个字段是否可以为null |
14 | e2 06 a7 b1 //checksum |
从Table_map_event中可以参照上节描述,解析出表中所有的字段类型以及对应的元数据,按照顺序分别是:
第一个字段:0x01=MYSQL_TYPE_TINY 无元数据
第二个字段:0x02=MYSQL_TYPE_SHORT 无元数据
第三个字段:0x09=MYSQL_TYPE_INT24 无元数据
第四个字段:0x03=MYSQL_TYPE_LONG 无元数据
第五个字段:0x08=MYSQL_TYPE_LONGLONG 无元数据
第六个字段:0xf6=MYSQL_TYPE_NEWDECIMAL 元数据表示精度为25(0x19),标度为10(0x0a)
第七个字段:0x04=MYSQL_TYPE_FLOAT 元数据表示该字段需要使用4(0x04)个字节表示
第八个字段:0x05=MYSQL_TYPE_DOUBLE 元数据表示该字段需要使用8(0x08)个字节表示
第九个字段:0x10=MYSQL_TYPE_BIT 元数据表示该字段中一共有5(0x0500)个比特位
数值型数据“值”解析
Write_rows_log_event 字节数据解析
1 | 公共头部部分省略。。。 |
2 | 5d 00 00 00 00 00 //table_id: |
3 | 01 00 //flag |
4 | 02 00 //var header length |
5 | 09 //m_width :packet integer,表示表中字段数量 |
6 | ff ff //before image: (m_width + 7) / 8字节 |
7 | 00 fe //bitmap_bits :表中两个字段,插入的值都不为NULL |
8 | 02 //col1 |
9 | ea ff //col2 |
10 | de 00 00 //col3 |
11 | 52 f7 ff ff //col4 |
12 | ce 56 00 00 00 00 00 00 //col5 |
13 | 80 00 7b 07 56 b5 b3 06 b0 8a 28 00 //col6 |
14 | 33 33 f6 42 //col7 |
15 | cd cc cc cc cc cc 5e 40 //col8 |
16 | 06 //col9 |
17 | 5e 6d 11 fa //checksum |
接下来,对应write_row_event中各个字段的二进制,我们按照顺序解析各个字段的值
MYSQL_TYPE_TINY字段解析
MYSQL_TYPE_TINY需要使用1个字节表示,小端存储。
上面的例子中,col1的类型是MYSQL_TYPE_TINY,在所以在Write_rows_log_event中字段值的内容占用1个字节,为0x02,转换成10进制就是2
MYSQL_TYPE_SHORT字段解析
MYSQL_TYPE_SHORT需要使用2个字节表示,小端存储。
上面的例子中,col2的类型是MYSQL_TYPE_SHORT,所以在Write_rows_log_event中字段值的内容占用2个字节,为0xEAFF,因为是小端存储,所以实际的顺序为0xFFEA,数据采用补码的方式表示有符号数,所以转换成10进制数值为-22
MYSQL_TYPE_INT24字段解析
MYSQL_TYPE_INT24需要使用3个字节表示,小端存储。
上面的例子中,col3的类型是MYSQL_TYPE_INT24,所以在Write_rows_log_event中字段值的内容占用3个字节,为0xDE0000,因为是小端存储,所以实际的顺序为0x0000DE,数据采用补码的方式表示有符号数,所以转换成10进制数值为222
MYSQL_TYPE_LONG字段解析
MYSQL_TYPE_LONG需要使用4个字节表示,小端存储。
上面的例子中,col4的类型是MYSQL_TYPE_LONG,所以在Write_rows_log_event中字段值的内容占用4个字节,为0x52F7FFFF,因为是小端存储,所以实际的顺序为0xFFFFF752,数据采用补码的方式表示有符号数,所以转换成10进制数值为-2222
MYSQL_TYPE_LONGLONG 字段解析
MYSQL_TYPE_LONGLONG需要使用8个字节表示,小端存储。
上面的例子中,col5的类型是MYSQL_TYPE_LONGLONG,所以在Write_rows_log_event中字段值的内容占用8个字节,为0xCE56000000000000,因为是小端存储,所以实际的顺序为0x00000000000056CE,数据采用补码的方式表示有符号数,所以转换成10进制数值为22222
MYSQL_TYPE_DECIMAL字段解析
MYSQL_TYPE_DECIMAL采用压缩存储的方式,将9个数字(十进制)使用4个字节来表示;每9位数字使用4个字节表示,剩余未满9位数字的,所需字节数参照下表。并且DECIMAL中整数部分与小数部分是单独分开表示的。
剩余位数 | 所需字节数 |
---|---|
0 | 0 |
1 | 1 |
2 | 1 |
3 | 2 |
4 | 2 |
5 | 3 |
6 | 3 |
7 | 4 |
8 | 4 |
MYSQL_TYPE_DECIMAL类型的字段,在存储数值的时候,首先将数值按照整数部分以及小数部分划分。
整数部分从低位到高位,每9位数字使用4个字节存储,将每9位数字与正负号(正数:0,负数:-1)做异或运算之后,以大端、有符号整数存储,对于剩余不足9位数字的部分,参照表格存储,将剩余数字与正负号(正数:0,负数:-1)做异或运算之后并异或上一个0x80,以大端、有符号整数存储。
小数部分从高位到低位,每9位数字使用4个字节存储,将每9位数字与正负号(正数:0,负数:-1)做异或运算之后,以大端、有符号整数存储,对于剩余不足9位数字的部分,参照表格存储,将剩余数字与正负号(正数:0,负数:-1)做异或运算之后并异或上一个0x80,以大端、有符号整数存储。
例如数字12.34,整数部分12,2位数字需要一个字节,0x0C与0做异或操作结果是0x0C,小数部分34,2位数字需要一个字节,0x22与0做异或操作结果是0x22,所以数字12.34的MYSQL_TYPE_DECIMAL在binlog中以0xOC22方式存储。
上面的例子中,col6的类型是MYSQL_TYPE_NEWDECIMAL,按照MYSQL_TYPE_NEWDECIMAL存储要求,需要((15/9)*4+3 + (10/9)*4+1)=12个字节,所以在Write_rows_log_event中字段值的内容占用12个字节,为0x80007b0756b5b306b08a2800,其中整数部分使用7个字节存储,内容为0x80007b0756b5b3,小数部分使用5个字节存储,内容为0x06b08a2800。
-
首先解析整数部分
MYSQL_TYPE_NEWDECIMAL类型值的符号可以从第一个字节中获取到,将第一个字节和0x80做与操作,如果结果不为0表示该值为正数,记mask=0;如果结果为0表示该值为负数,记mask=-1。
因为MYSQL_TYPE_NEWDECIMAL类型使用第一个字节的最高位来表示值的正负,所以在解析具体的值的时候,需要将该符号位忽略,所以真实的数值内容需要将第一个字节与0x80做异或运算。即上述内容中,整数部分数值内容为0x00007b0756b5b3,小数部分数值内容为0x06b08a2800。
整数部分因为最多存储15位数据,所以使用4+6=7个字节存储。因为整数部分是从低位到高位(从右往左)每9位数字使用4个字节存储,剩余位数按照要求存储,所以这7个字节中,前3个字节表示前(15-9)=6位,剩余4个字节表示后9位。将前三个字节0x00007b转换成10进制为123,因为存储的时候将数值与正负号(正数:0,负数:-1)做过异或运算,所以解析的时候需要将解析出来的123与mask做异或运算,即实际前6位数值为123。后4个字节0x0756b5b3转换成10进制为123123123,因为存储的时候将数值与正负号(正数:0,负数:-1)做过异或运算,所以解析的时候需要将解析出来的123与mask做异或运算,即后9位数字为123123123。将整数部分连接起来就是123123123123。
-
接着解析小数部分
小数部分因为最多存储10位数据,所以使用4+1=5个字节存储。因为小数部分是从高位到低位(从左往右)每9位数字使用4个字节存储,剩余位数按照要求存储,所以这5个字节中,前4个字节表示9位数字,剩余1个字节表示(10-9)=1位。前4个字节0x0ab08a28转换成10进制是112233000,因为存储的时候将数值与正负号(正数:0,负数:-1)做异或运算,所以解析的时候需要将解析出来的112233000与mask做异或运算,即实际前9位数字为112233000。后一个字节0x00转换成10进制为0,因为存储的时候将数值与正负号(正数:0,负数:-1)做异或运算,所以解析的时候需要将解析出来的0与mask做异或运算,即实际后1位数字为0。将小数部分连接起来就是1122330000。
将解析出来的整数部分与小数部分连接在一起就是123123123123.1122330000
MYSQL_TYPE_FLOAT字段解析
MYSQL_TYPE_FLOAT类型需要四个字节表示。
上面的例子中,col7的类型是MYSQL_TYPE_FLOAT,所以在Write_rows_log_event中字段值的内容占用4个字节,为0x3333f642,将字节内容强转成float类型的数据(MySQL在解析binlog的时候,也是将字节内容强转成float类型),得到的结果取决于不同的编程语言。比如在Python中,0x3333f642内容强转成float的结果就是123.099998。
MYSQL_TYPE_DOUBLE字段解析
MYSQL_TYPE_DOUBEL类型需要八个字节表示
上面的例子中,col8的类型是MYSQL_TYPE_DOUBLE,所以在Write_rows_log_event中字段值的内容占用8个字节,为0xcdcccccccccc5e40,将字节内容强转成double类型的数据(MySQL在解析binlog的时候,也是将字节内容强转成double类型),得到的结果取决于不同的编程语言。比如在Python中,0xcdcccccccccc5e40内容强转成double类型就是123.200000。
MYSQL_TYPE_BIT字段解析
MYSQL_TYPE_BIT类型需要(M+7)/8,M是字段定义的比特位数。
上面的例子中,col9类型是MYSQL_TYPE_BIT,对应的元数据中定义该字段为5个比特位,所以需要(5+7)/8=1个字节存储数值。所以在Write_row_event中字段值的内容占用1个字节,为0x06,对应的二进制表示方式为b’00110’,所以该字段的值为b’00110’。
日期型字段类型
DATE,DATETIME,TMIESTAMP类型
-
DATE
日期。支持的范围是‘1000-01-01’到‘9999-12-31’。MySQL使用’YYYY-MM-DD’格式显示DATE值。但允许使用字符串或数字给DATE列赋值。
-
DATETIME[(fsp)]
日期和时间的组合。支持的范围是‘1000-01-01 00:00:00.000000’到’9999-12-31 23:59:59.999999’。MySQL使用’YYYY-MM-DD HH:MM:SS[.fraction]'格式显示DATETIME值,但允许使用字符串或数字给DATETIME列赋值。从MySQL5.6.4开始,一个可选的范围从0到6的fsp值可以指定秒数的精度。值为0表示没有小数部分。如果省略的话,默认精度为0.
从MySQL5.6.5开始,自动化初始和更新到当前日期时间,可以使用DATETIME列的DEFAULT和ON UPDATE定义项。
-
TIMESTAMP[(fsp)]
时间戳。范围从’1970-01-01 00:00:01.000000’UTC到’2038-01-19 03:14:07.999999’UTC。TIMESTAMP存储从纪元('1970-01-01 00:00:00’UTC)至今的总秒数。
TIME类型
-
TIME[(fsp)]
时间。范围从’-838:59:59.000000’到’838:59:59.000000’。MySQL使用’HH:MM:SS[.fraction]'格式显示TIME值。但允许使用字符串或数字给TIME列赋值。从MySQL5.6.4开始,一个可选的范围从0到6的fsp值可以指定秒数的精度。值为0表示没有小数部分。如果省略的话,默认精度为0。
YEAR类型
- YEAR[(2|4)]
两位或四位格式的年。默认是四位格式,虽然显示上YEAR(2)或YEAR(4)的格式不同,但他们具有相同的范围的值。四位格式显示为1901至2155,和0000。两位格式显示为70至69,表示1970至2069。MySQL以为YYYY或YY格式显示YEAR值,但允许使用字符串或数字赋值。
日期型数据存储格式
解析环境描述
表结构定义
1 | CREATE TABLE `time_table` ( |
2 | `col1` date DEFAULT NULL, |
3 | `col2` datetime DEFAULT NULL, |
4 | `col3` datetime(3) DEFAULT NULL, |
5 | `col4` timestamp NULL DEFAULT NULL, |
6 | `col5` timestamp(4) NULL DEFAULT NULL, |
7 | `col6` time DEFAULT NULL, |
8 | `col7` time(5) DEFAULT NULL, |
9 | `col8` year(4) DEFAULT NULL, |
10 | `col9` year(4) DEFAULT NULL |
11 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin |
表数据展示
1 | root@localhost : gangshen 07:04:33> insert into time_table values('2017-12-14','2017-12-14 09:54:00','2017-12-14 09:54:00.112','2017-12-14 09:54:00','2017-12-14 09:54:00.1113','09:54:00','09:54:00.00000','2017','2017'); |
2 | Query OK, 1 row affected (0.03 sec) |
3 | |
4 | root@localhost : gangshen 07:04:47> select * from time_table; +------------+---------------------+-------------------------+---------------------+--------------------------+----------+----------------+------+------+ |
5 | | col1 | col2 | col3 | col4 | col5 | col6 | col7 | col8 | col9 | |
6 | +------------+---------------------+-------------------------+---------------------+--------------------------+----------+----------------+------+------+ |
7 | | 2017-12-14 | 2017-12-14 09:54:00 | 2017-12-14 09:54:00.112 | 2017-12-14 09:54:00 | 2017-12-14 09:54:00.1113 | 09:54:00 | 09:54:00.00000 | 2017 | 2017 | |
8 | +------------+---------------------+-------------------------+---------------------+--------------------------+----------+----------------+------+------+ |
9 | 1 row in set (0.00 sec) |
然后从binlog文件中拿到,对应的Table_map_event
和Writes_rows_event
对应的字节内容,开始解析
元数据解析
Table_map_event字节数据解析
1 | 公共头部部分省略: |
2 | 4e 00 00 00 00 00 //table_id :小端存储,16进制转换为10进制为78 |
3 | 01 00 //flag : |
4 | 08 67 61 6e 67 73 68 65 6e 00 //database_name :1个字节是字符串长度,后接一个null-terminated string 第一个字节表示字符串长度为8,后面内容将16进制转换为ascii字符为gangshen |
5 | 0a 74 69 6d 65 5f 74 61 62 6c 65 00 //table_name: 1个字节是字符串长度,后接一个null-terminated string第一个字节表示字符串长度为5,后面内容将16进制转换为ascii字符为time_table |
6 | 09 //columns count :packet integer类型,转换之后,数值为9 表示表中有9个字段 |
7 | 0a 12 12 11 11 13 13 0d 0d //column type |
8 | 06 //metadata_length: packet integer类型,转换之后,数值为9,表示记录表中的metadata内容占用9个字节 |
9 | 00 //col2 |
10 | 03 //col3 |
11 | 00 //col4 |
12 | 04 //col5 |
13 | 00 //col6 |
14 | 05 //col7 |
15 | ff 01 //null_bits :int((column_count + 7) / 8)个字节 一个bit表示一个字段是否可以为null |
16 | 38 9b 0a 26 //checksum |
从Table_map_event中可以按照上面的讲述,解析出表中所有的字段类型以及对应的元数据,按照顺序分别是:
第一个字段:0x0a=MYSQL_TYPE_DATE 无元数据
第二个字段:0x12=MYSQL_TYPE_DATETIME2 元数据0x00表示该字段的时间精度为0
第三个字段:0x12=MYSQL_TYPE_DATETIME2 元数据0x03表示该字段的时间精度为3
第四个字段:0x11=MYSQL_TYPE_TIMESTAMP2 元数据0x00表示该字段的时间精度为0
第五个字段:0x11=MYSQL_TYPE_TIMESTAMP2 元数据0x04表示该字段的时间精度为4
第六个字段:0x13=MYSQL_TYPE_TIME2 元数据0x00表示该字段的时间精度为0
第七个字段:0x13=MYSQL_TYPE_TIME2 元数据0x05表示该字段的时间精度为5
第八个字段:0x0d=MYSQL_TYPE_YEAR 无元数据
第九个字段:0x0d=MYSQL_TYPE_YEAR 无元数据
日期型数据“值”解析
Write_rows_log_event字节数据解析
1 | 公共头部部分省略 |
2 | 4e 00 00 00 00 00 //table_id: 小端存储,16进制转换为10进制为78 |
3 | 01 00 //flag: |
4 | 02 00 //var header length :小端存储,16进制转换为10进制为2 |
5 | 09 //m_width :packet integer,表示表中字段数量 |
6 | ff ff //before image: (m_width + 7) / 8字节 |
7 | 00 fe //bitmap_bits :表中两个字段,插入的值都不为NULL |
8 | 8e c3 0f //col1 |
9 | 99 9e 5c 9d 80 //col2 |
10 | 99 9e 5c 9d 80 04 60 //col3 |
11 | 5a 31 d9 b8 //col4 |
12 | 5a 31 d9 b8 04 59 //col5 |
13 | 80 9d 80 //col6 |
14 | 80 9d 80 00 00 00 //col7 |
15 | 75 //col8 |
16 | 75 //col9 |
17 | 7e da 55 ce //checksum |
MYSQL_TYPE_DATE字段解析
MYSQL_TYPE_DATE类型使用3个字节存储,小端存储。3个字节一共24个比特位,其中从低到高(从右到左)前5位,表示日期,接着的从低到高4位表示月份,剩余的位数表示年份。
在上面的例子中,col1的类型就是MYSQL_TYPE_DATE,所以在Write_rows_log_event中占用3个字节的内容,为0x8ec30f,因为是小端存储,所以实际顺序为0x0fc38e,3个字节一共24个比特位,从右往左,5位表示日期,4位表示月份,15位表示年份。将0x0fc38e转换成二进制是b’0000 1111 1100 0011 1000 1110’,所以day=b’01110’=14,month=b‘1100’=12,year=‘0000 1111 1100 001’=2017,所以对应的字段的值是‘2017-12-14’
MYSQL_TYPE_DATETIME2字段解析
MYSQL_TYPE_DATETIME2类型使用5个字节存储,并且根据时间精度的不同,额外需要不同的字节存储数据,时间精度部分为大端存储。
年月日时分秒的内容存储在基础的5个字节中,5个字节一共40位,从左往右前18位表示年月(年份 * 13 + 月份),其中最高位表示符号,最高位为1表示是正的,为0表示是负的,接着的5位表示日期,剩余的17位中,按照顺序,5位表示时,6位表示分,6位表示秒。因为时间精度最多为6位,一个字节表示两位,所以时间精度为1或者2的时候,使用一个额外的字节表示时间精度,时间精度为3或者4的时候,使用两个额外的字节表示时间精度,时间精度为5或者6的时候,使用三个额外的字节表示时间精度。
时间精度 | 所需字节数 |
---|---|
0 | 0 |
1 | 1 |
2 | 1 |
3 | 2 |
4 | 2 |
5 | 3 |
6 | 3 |
在上面的例子中,col2和col3的类型就是MYSQL_TYPE_DATETIME2,因为col2的时间精度为0,所以col2在Write_rows_log_event中占用5个字节的内容,为0x999e5c9d80,因为col3的时间精度为3,所以col3在Write_rows_log_event中占用(5+2)个字节的内容,为0x999e5c9d800460。
我们先解析col2字段的值,从上面的描述中,5个字节(40位)内容,前18位表示年月(年份 * 13+月份),5位表示日期,5位表示时,6位表示分,6位表示秒。0x999e5c9d80转换为二进制是b‘1001 1001 1001 1110 0101 1100 1001 1101 1000 0000’,年月=b’1001 1001 1001 1110 01’ ,日期=b’01110’,时=b’01001’,
分=b’110110’,秒=b’000000’。年月中最高位符号位(正数最高位为1,负数最高位为0),所以在计算数值的时候,要排除最高位的影响(将基础的5个字节减去0x8000000000),即年月值的实际二进制为b’0001 1001 1001 1110 01’,转换为10进制为26233。年=26233/13,月=26233%13,日=b’01110’=14,时=b’01001’=9,分=b’110110’=54,秒=b’000000’=0。所以col2字段的值为2017-12-14 09:54:00
接着解析col3字段值,因为col2和col3字段的基础5个字节的内容一致,所以直接跳过,解析额外的时间精度部分,时间精度部分为0x0460,因为是大端存储,所以转换为10进制结果为1120,即时间精度部分为1120。所以col3字段的值为2017-12-14 09:54:00.1120
MYSQL_TYPE_TIMESTAMP2字段解析
MYSQL_TYPE_TIMESTAMP2类型使用4个字节存储,大端存储,并且根据时间精度的不同,额外需要不同的字存储数据,时间精度部分也使用大端存储;如果时间精度为1或者2,则4个字节之外还需要1个字节存储数据;如果时间精度为3或者4,则4个字节之外还需要2个字节存储数据;如果时间精度为5或者6,则4个字节之外还需要3个字节存储数据。
时间精度 | 所需字节数 |
---|---|
0 | 0 |
1 | 1 |
2 | 1 |
3 | 2 |
4 | 2 |
5 | 3 |
6 | 3 |
在上面的例子中,col4和col5的类型就是MYSQL_TYPE_TIMESTAMP2,因为col4的时间精度为0,所以col4在Write_rows_log_event中占用4个字节的内容,为0x5a31d9b8,因为col5的时间精度为4,所以col5在Write_rows_log_event中占用(4+2)=6个字节,为0x5a31d9b80459。
我们先解析col4字段的值,4个字节,大端存储,所以0x5a31d9b8转换为10进制数为1513216440,即col4的时间戳为1513216440(时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数),转换为时间为2017-12-14 09:54:00
接着解析col5字段的值,因为col4和col5字段前4个字节内容相同,故不重新解析,直接解析时间精度部分,时间精度为0x0459,因为是大端存储,所以转换为10进制为1113,即时间精度部分为1113。所以col5字段的值为2017-12-14 09:54:00.1113
MYSQL_TYPE_TIME2字段解析
MYSQL_TYPE_TIME2类型使用3个字节存储,大端存储,并且根据时间精度的不同,额外需要不同的字节存储数据,时间精度部分使用大端存储。
时分秒的内容存储在基础的3个字节中,3个字节一共24位,从左往右,前12位表示时,中间6位表示分,最后6位表示秒,额外的字节存储秒的时间精度部分。因为时间精度最多为6位,一个字节表示两位,所以时间精度为1或者2的时候,使用一个额外的字节表示时间精度,时间精度为3或者4的时候,使用两个额外的字节表示时间精度,时间精度为5或者6的时候,使用三个额外的字节表示时间精度。
时间精度 | 所需字节数 |
---|---|
0 | 0 |
1 | 1 |
2 | 1 |
3 | 2 |
4 | 2 |
5 | 3 |
6 | 3 |
MYSQL_TYPE_TIME2类型字段值可以为负数。那么MYSQL_TYPE_TIME2类型是如何存储正负值的呢。
-
没有时间精度
在没有时间精度的情况下,MYSQL_TYPE_TIME2类型使用3个字节存储时分秒内容(大端存储)。MySQL使用0x800000作为时间原点(00:00:00),比0x800000大的为正的时间值,比0x800000小的为负的时间值。即时分秒的内容是与0x800000做减法之后的绝对值。
-
时间精度为1或者2
在时间精度为1或者2的情况下,MYSQL_TYPE_TIME2类型使用3个基础字节存储时分秒内容(大端存储),使用一个额外的字节存储时间精度。MySQL使用0x80000000作为时间原点(00:00:00.00),比0x80000000大的为正的时间值,比0x80000000小的为负的时间值,即时分秒的即时间精度的内筒是与0x80000000做减法之后的绝对值。
-
时间精度为3或者4
在时间精度为1或者2的情况下,MYSQL_TYPE_TIME2类型使用3个基础字节存储时分秒内容(大端存储),使用两个额外的字节存储时间精度。MySQL使用0x8000000000作为时间原点(00:00:00.0000),比0x8000000000大的为正的时间值,比0x8000000000小的为负的时间值,即时分秒的即时间精度的内筒是与0x8000000000做减法之后的绝对值。
-
时间精度为5或者6
在时间精度为1或者2的情况下,MYSQL_TYPE_TIME2类型使用3个基础字节存储时分秒内容(大端存储),使用三个额外的字节存储时间精度。MySQL使用0x800000000000作为时间原点(00:00:00.000000),比0x800000000000大的为正的时间值,比0x800000000000小的为负的时间值,即时分秒的时间精度是与0x800000000000做减法之后的绝对值。
在上面的例子中,col6和col7的类型就是MYSQL_TYPE_TIME2,因为col6的时间精度为0,所以col6在Write_rows_log_event中占用3个字节的内容,为0x809d80,因为col7的时间精度为5,所以col7在Write_rows_log_event中占用(3+3)=6个字节的内容,为0x809d80000000。
我们先解析col6的值,按照上面的描述,col6字段类型为MYSQL_TYPE_TIME2,时间精度为0,所以使用3个字节存储内容,在Write_rows_log_event中占用3个字节的内容,为0x809d80。因为时间精度为0的情况下,MySQL是以0x800000为时间原点,具体的时分秒为0x809d80与0x800000差值的绝对值=0x009d80。且0x809d80比0x800000大,所以字段值符号为正。时分秒=0x009d80=b‘0000 0000 1001 1101 1000 0000’,时=前12位=b’0000 0000 1001’=9,分=中间6位=b’1101 10’=54,秒=最后6位=b’00 0000’=0,即col6字段的值为09:54:00
接着解析col7的值,按照上面的描述,col7字段类型为MYSQL_TYPE_TIME2,时间精度为5,所以使用3+3=6个字节存储内容,在Write_rows_log_event中占用6个字节的内容,为0x809d80000000,因为时间精度为5的情况下,MySQL是以0x800000000000为时间原点,具体的时分秒以及时间精度存储为0x809d80000000与0x800000000000差值的绝对值=0x009d80000000。且0x809d80000000比0x800000000000大,所以字段值符号为正。时分秒=0x009d80=b‘0000 0000 1001 1101 1000 0000’,时=前12位=b’0000 0000 1001’=9,分=中间6位=b’1101 10’=54,秒=最后6位=b’00 0000’=0,时间精度=0x000000=0,即col7字段的值为09:54:00.00000
MYSQL_TYPE_YEAR字段解析
MYSQL_TYPE_DATA类型使用1个字节存储。表示的值是(要表示的年份-1900)
在上面的例子中,col8和col9的类型就是MYSQL_TYPE_YEAR,col8和col9都在Write_rows_log_event中占用1个字节的内容,分别为0x75和0x75,转换为10进制为117,将结果加上1900,则最终col8和col9字段的值为2017。
日期型字段类型
中文等字符存储以该字段的字符集规定存储二进制数据。
CHAR和VARCHAR类型
-
[NATIONAL] CHAR[( M )] [CHARACTER SET charset_name ] [COLLATE collation_name ]
定长字符串存储时总是使用空格填充右侧达到指定长度。M表示字符串列的长度。M的范围是0到255。省略的花,长度为1。
CHAR是CHARACTER的简写。NATIONAL CHAR(或简写NCHAR)是标准的定义CHAR列应该使用默认字符集的SQL方法。MySQL4.1及更高版本使用utf8作为默认字符集。
CHAR BYTE是BINARY的别名。这是为了保证兼容性。
MySQL允许定义创建一个CHAR(0)的列。这主要用于必须有一个列,而实际上并不使用值,用以与旧版本的应用程序相兼容。当需要只有两个值的列时CHAR(0)也很不错:定义了CHAR(0)NULL的列只占用一位,可只取值NULL和’’(空字符串)
-
[NATIONAL] VARCHAR(M) [CHARACTER SET charset_name] [COLLATE collation_name]
变长字符串。M表示最大列长度。M的范围是0到65535。VARCAHR的最大长度由最长行的大小(所有列的共享65535字节)和字符集决定。例如,utf8字符需要多达每个字符三个字节,所以VARCHAR列最多指定21844个字符。
MySQL存储VARCHAR时使用一个或两个字节的前缀+数据。长度前缀表示内容占用的字节数。当列长度不超过255个字节长度前缀占一个字节,当列长度超过255个字节长度前缀占两个字节。VARCHAR是CHARACTER VARYING的简写。
BINARY和VARBINARY类型
-
BINARY(M)
BINARY类型与CHAR类型相似,不过存储的是二进制字节字符串而不是非二进制字符串。M表示列的长度(以字节为单位)
-
VARBINARY(M)
VARBINARY类型与VARCHAR类型相似,不过存储的是二进制字节字符串而不是非二进制字符串。M表示最大列长度(以字节为单位)
BLOB和TEXT类型
-
TINYBLOB
这种BLOB列的最大长度是65535(2^8-1)个字节。存储每个TINYBLOB列时使用一个字节的前缀长度记录内容占用的字节数。
-
BLOB[(M)]
BLOB列的最大长度是65535(2^16-1)个字节。存储每个BLOB列时使用两个字节的前缀长度记录内容占用的字节数。
-
MEDIUMBLOB
这种BLOB列的最大长度是16777215(2^24-1)个字节。存储每个MEDIUMBLOB列时使用三个字节的前缀长度记录内容占用的字节数。
-
LONGBLOB
这种BLOB列的最大长度是4294967295或者(2^32-1)个字节。LONGBLOB列的最大有效长度取决于客户端/服务器协议配置的最大包大小和可用内存。存储每个LONGBLOB列时使用四个字节的前缀长度记录内容占用的字节数。
-
TINYTEXT [CHARACTER SET charset_name ] [COLLATE collation_name ]
这种TEXT列的最大长度是255(2^8-1)个字节。如果内容包含多字节字符,那么最大有效长度将减少。存储每个TINYTEXT时使用一个字节的前缀长度记录内容占用的字节数。
-
TEXT[( M )] [CHARACTER SET charset_name ] [COLLATE collation_name ]
TEXT列的最大长度是65535(2^16-1)个字节。如果内容包含多字节字符,那么最大有效长度将减少。存储每个TEXT列时使用两个字节的前缀长度记录内容占用的字节。
可以给出该类型的可选长度M。如果指定了,那么MySQL会创建一个最小的,但可以容纳M字节长度的TEXT列。
-
MEDIUMTEXT [CHARACTER SET charset_name ] [COLLATE collation_name ]
这种TEXT列的最大长度是16,777,215(224-1)个字节。如果内容包含多字节字符,那么最大有效长度将减少。存储每个MEDIUMTEXT列时使用三个字节的前缀长度记录内容占用的字节数。
-
LONGTEXT [CHARACTER SET charset_name ] [COLLATE collation_name ]
这种TEXT列的最大长度是4,294,967,295或者4GB(232-1)个字节。如果内容包含多字节字符,那么最大有效长度将减少。LONGTEXT列的最大有效长度取决于客户端/服务器协议中配置的最大包大小和可用内存。存储每个LONGTEXT列时使用四个字节的前缀长度记录内容占用的字节数。
-
ENUM类型
ENUM( ’ value1 ‘,’ value2 ',…) [CHARACTER SET charset_name ] [COLLATE collation_name ]
枚举类型。属于字符串对象,允许从’ value1 ‘,’ value2 ‘,…,NULL或特殊的’'错误值 列表中选取一个值。ENUM值内部用整数表示。
ENUM列最多可以有65,535个不同的元素(实际的限制小于3000个元素)。一张表可以不超过255个唯一的元素,列表定义中ENUM和SET列视为一组。
SET类型
-
SET(’ value1 ‘,’ value2 ',…) [CHARACTER SET charset_name ] [COLLATE collation_name ]
设置类型。属于字符串对象,可以有零个或多个值,必须从’ value1 ‘,’ value2 ',…中选取值,SET值内部用整数表示。
SET列最多可以有64个不同的元素。一张表可以不超过255个唯一的元素,列表定义中ENUM和SET列视为一组。
字符型数据存储格式
解析环境描述
表结构定义
1 | CREATE TABLE `string_table` ( |
2 | `col1` varchar(500) COLLATE utf8_bin DEFAULT NULL, |
3 | `col2` char(60) COLLATE utf8_bin DEFAULT NULL, |
4 | `col3` blob, |
5 | `col4` set('a','b','c') COLLATE utf8_bin DEFAULT NULL, |
6 | `col5` enum('one','two','three') COLLATE utf8_bin DEFAULT NULL |
7 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin |
表数据展示
1 | root@localhost : gangshen 07:37:31> insert into string_table values('abcdefg','abc','abcdefghijklmnopqrstuvwxyz','c','two'); |
2 | Query OK, 1 row affected (0.04 sec) |
3 | |
4 | root@localhost : gangshen 07:40:27> select * from string_table; |
5 | +---------+------+----------------------------+------+------+ |
6 | | col1 | col2 | col3 | col4 | col5 | |
7 | +---------+------+----------------------------+------+------+ |
8 | | abcdefg | abc | abcdefghijklmnopqrstuvwxyz | c | two | |
9 | +---------+------+----------------------------+------+------+ |
10 | 1 row in set (0.00 sec) |
然后从binlog文件中拿到,对应的Table_map_event
和Writes_rows_event
对应的字节内容,开始解析
元数据解析
Table_map_event字节解析
1 | 公共头部部分省略。。。 |
2 | 61 00 00 00 00 00 //table_id:小端存储,16进制转换成10进制为97 |
3 | 01 00 //flag |
4 | 08 67 61 6e 67 73 68 65 6e 00 //database_name:1个字节是字符串长度,后接一个null-terminated string 第一个字节表示字符串长度为8,后面内容将16进制转换为ascii字符为gangshen |
5 | 0c 73 74 72 69 6e 67 5f 74 61 62 6c 65 00//table_name: 1个字节是字符串长度,后接一个null-terminated string第一个字节表示字符串长度为5,后面内容将16进制转换为ascii字符为string_table |
6 | 05 //columns count :packet integer类型,转换之后,数值为5 表示表中有5个字段 |
7 | 0f fe fc fe fe //column type : |
8 | 09 //metadata length : packet integer类型,转换之后,数值为9,表示记录表中的metadata内容占用9个字节 |
9 | dc 05 //col1 |
10 | fe b4 //col2 |
11 | 02 //col3 |
12 | f8 01 //col4 |
13 | f7 01 //col5 |
14 | 1f //null_bits :int((column_count + 7) / 8)个字节 一个bit表示一个字段是否可以为null |
15 | 22 6b 17 48 //checksum |
从Table_map_event中可以按照上面的讲述,解析出表中所有的字段类型以及对应的元数据,按照顺序分别是:
第一个字段:0x0f=MYSQL_TYPE_VARCHAR 元数据0xdc05表示字段最多占用的字节数,因为是小端存储,所以该字段最多占用0x05dc=1500字节。这个占用的字节数取决于表的字符集,因为表的字符集为utf8,所以每个字符最多占用3个字节,所以该字段最多占用500*3个字节。
第二个字段:0xfe=MYSQL_TYPE_STRING,因为是MYSQL_TYPE_STRING类型,所以其真实的字段类型应该是元数据第一个字节(0xfe)和0x30做与运算之后的结果, 第二个字节表示该字段最多占用的字节数,所以该字段最多占用0xb4=180个字节
第三个字段:0x12=MYSQL_TYPE_BLOB 元数据0x02表示该字段类型为BLOB/TEXT类型
第四个字段:0x11=MYSQL_TYPE_STRING,所以其真实的字段类型应该是元数据的第一个字节(0xf8)和0x30做与运算之后的结果,即其真实类型是MYSQLTYPE_SET第二个字节表示该字段最多占用的字节数,所以该字段最多占用0x01个字节的内容。
第五个字段:0x11=MYSQL_TYPE_STRING,所以其真实的字段类型应该是元数据的第一个字节(0xf8)和0x30做与运算之后的结果,即其真实类型是MYSQLTYPE_SET第二个字节表示该字段最多占用的字节数,所以该字段最多占用0x01个字节的内容。
字符型数据“值”解析
Write_rows_log_event字节解析
1 | 公共头部部分省略。。。 |
2 | 61 00 00 00 00 00 //table_id: 小端存储,16进制转换为10进制为97 |
3 | 01 00 //flag: |
4 | 02 00 //var header length :小端存储,16进制转换为10进制为2 |
5 | 05 //m_width :packet integer,表示表中字段数量 |
6 | ff //before image: (m_width + 7) / 8字节 |
7 | e0 //bitmap_bits :表中两个字段,插入的值都不为NULL |
8 | 07 00 61 62 63 64 65 66 67 //col1 |
9 | 03 61 62 63 //col2 |
10 | 1a 00 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a //col3 |
11 | 04 //col4 |
12 | 02 //col5 |
13 | 46 1d 2a 69 //checksum |
MYSQL_TYPE_VARCHAR字段解析
MYSQL_TYPE_VARCHAR类型字段由长度和字符串内容两部分组成,长度由由字段的元数据决定,字段的元数据表示该字段最大占用字节数。如果元数据小于255,则MYSQL_TYPE_VARCHAR类型使用一个字节表示字符串长度,长度后面紧接着就是字符串的内容;如果元数据大于等于255,则MYSQL_TYPE_VARCHAR类型使用两个字节表示字符串长度(小端存储),长度后面紧接这就是字符串的内容。
在上面的例子中,col1的字段类型为MYSQL_TYPE_VARCHAR类型,且元数据为0x05dc=2500大于255,所以使用两个字节表示字符串长度,所以字符串长度为0x0700,因为是小端存储,所以实际为0x0007,即字符串长度为7个字节。紧接着读取7个字节的内容0x61626364656667转换为ascii为abcdefg,所以col1的字段值为abcdefg
MYSQL_TYPE_STRING字段解析
MYSQL_TYPE_STRING类型字段由长度和字符串内容两部分组成,长度由由字段的元数据决定,字段的元数据表示该字段最大占用字节数。如果元数据小于255,则MYSQL_TYPE_STRING类型使用一个字节表示字符串长度,长度后面紧接着就是字符串的内容;如果元数据大于等于255,则MYSQL_TYPE_STRING类型使用两个字节表示字符串长度(小端存储),长度后面紧接这就是字符串的内容。
在上面的例子中,col2的字段类型是MYSQL_TYPE_STRING类型,且元数据中最大占用字节数为0xb4=180小于255,所以使用一个字节表示字符串长度,所以字符串长度为0x03=3个字节。紧接着读取3个字节的内容0x616263专户为ascii为abc,所以col2的字段值为abc
MYSQL_TYPE_BLOB字段解析
MYSQL_TYPE_BLOB类型字段由长度和字符串内容两部分组成,长度由字段的元数据决定,字段的元数据表示BLOB的类型。如果元数据是1,表示是TINYBLOB/TINYTEXT类型,则MYSQL_TYPE_BLOB类型使用一个字节表示字符串长度,长度后面紧接着就是字符串的内容;如果元数据是2,表示是BLOB/TEXT类型,则MYSQL_TYPE_BLOB类型使用两个字节表示字符串长度(小端存储),长度后面紧接着就是字符串内容;如果元数据是3,表示是MEDIUMBLOB/MEDIUMTEXT类型,则MYSQL_TYPE_BLOB类型使用三个字节表示字符串长度(小端存储),长度后面紧接着就是字符串内容;如果元数据是4,表示是LONGBLOB/LONGTEXT类型,则MYSQL_TYPE_BLOB类型使用四个字节表示字符串长度(小端存储),长度后面紧接着就是字符串内容。
在上面的例子中,col3字段的类型是MYSQL_TYPE_BLOB类型,且元数据为0x02,表示类型为BLOB/TEXT类型,说明该字段使用两个字节表示字符串的长度。所以字符串长度为0x1a00,因为是小端存储,所以字符串长度为0x001a=26,紧接着读取26个字节的内容0x6162636465666768696a6b6c6d6e6f707172737475767778797a转换为ascii为abcdefghijklmnopqrstuvwxyz,所以col3的字段值为abcdefghijklmnopqrstuvwxyz。
MYSQL_TYPE_SET字段解析
MYSQL_TYPE_SET类型字段内容取决于字段定义SET的数量,表示字段值所需字节数量可以从字段元数据的第二个字节获取到,第二个字节表示内容长度。MYSQL_TYPE_SET类型字段内容采用压缩的表示方法,对应比特位置1则表示值是该位,但是从binlog中无法解析出SET类型内容的具体类型,只能解析出对应的比特位表示方式。
在上面的例子中,col4字段的类型是MYSQL_TYPE_SET,且元数据中表示该字段使用0x01个字节表示字段值,所以col4在Write_rows_log_event占用1个字节的内容,为0x04,转换为二进制为b’100’,从右往左第3位比特位上是1,表示值为set定义中第三个。从Write_rows_log_event中,无法解析出set定义的每个项,查看建表语句,得知set定义中第三个为c,即col4字段的值为c
MYSQL_TYPE_ENUM字段解析
MYSQL_TYPE_ENUM类型字段内容取决于字段定义ENUM数量,表示字段值所需字节数量可以从字段元数据的第二个字节获取到,第二个字节表示内容长度。
当元数据第二个字节等于1的时候,MYSQL_TYPE_ENUM字段值使用一个字节表示,当元数据第二个字节等于2的时候,MYSQL_TYPE_ENUM字段值使用两个字节表示。
在上面的例子中,col5字段的类型是MYSQL_TYPE_ENUM,且元数据中表示该字段使用0x01个字节表示字段值,所以col5在Write_rows_log_event中占用1个字节的内容,为0x02,转换为10进制为2,即表示值为enum定义中的第2个。在Write_rows_log_event中,无法解析出enum定义的每个项,查看建表语句,得知enum定义中第二个值为two,即col5字段的值为two。
MySQL binlog event解析实例
基于前面的知识,进行实际binlog的解析,看看常见的insert、update、delete语句在binlog中存储的形式。
INSERT语句在binlog中event表现形式
下面,我们就看一下一条INSERT语句在会产生哪些类型的event,这些event在数据库中查看是怎么样的,在binlog文件中查看是怎么样的。
ROW格式下表现形式
以下显示内容是在MySQL-5.6.34版本上,且binlog日志记录格式为ROW模式的情况下。
1 | root@localhost : gangshen 09:34:02> show binary logs; |
2 | +------------------+-----------+ |
3 | | Log_name | File_size | |
4 | +------------------+-----------+ |
5 | | mysql-bin.000001 | 120 | |
6 | +------------------+-----------+ |
7 | 1 row in set (0.00 sec) |
8 | |
9 | root@localhost : gangshen 09:34:22> show binlog events in 'mysql-bin.000001'; |
10 | +------------------+-----+-------------+-----------+-------------+---------------------------------------+ |
11 | | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | |
12 | +------------------+-----+-------------+-----------+-------------+---------------------------------------+ |
13 | | mysql-bin.000001 | 4 | Format_desc | 330619 | 120 | Server ver: 5.6.34-log, Binlog ver: 4 | |
14 | +------------------+-----+-------------+-----------+-------------+---------------------------------------+ |
15 | 1 row in set (0.00 sec) |
16 | |
17 | root@localhost : gangshen 09:34:32> insert into test1(`name`) values('woqutech'); |
18 | Query OK, 1 row affected (0.03 sec) |
19 | |
20 | root@localhost : gangshen 09:36:01> show binlog events in 'mysql-bin.000001'; |
21 | +------------------+-----+-------------+-----------+-------------+------------------------------------------------+ |
22 | | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | |
23 | +------------------+-----+-------------+-----------+-------------+------------------------------------------------+ |
24 | | mysql-bin.000001 | 4 | Format_desc | 330619 | 120 | Server ver: 5.6.34-log, Binlog ver: 4 | |
25 | | mysql-bin.000001 | 120 | Query | 330619 | 201 | BEGIN | |
26 | | mysql-bin.000001 | 201 | Rows_query | 330619 | 269 | # insert into test1(`name`) values('woqutech') | |
27 | | mysql-bin.000001 | 269 | Table_map | 330619 | 324 | table_id: 70 (gangshen.test1) | |
28 | | mysql-bin.000001 | 324 | Write_rows | 330619 | 373 | table_id: 70 flags: STMT_END_F | |
29 | | mysql-bin.000001 | 373 | Xid | 330619 | 404 | COMMIT /* xid=13 */ | |
30 | +------------------+-----+-------------+-----------+-------------+------------------------------------------------+ |
31 | 6 rows in set (0.00 sec) |
从数据库中,我们可以看到insert into test1(name) values(‘woqutech’);语句,在binlog日志文件中转换成了5个event存储,分别为Query,Rows_query,Table_map,Write_rows,Xid类型的event,这些event中,Query event表示一个更新语句的开始,Rows_query event记录了更新语句的语句内容,Table_map event中记录了insert语句操作的表的信息,Write_rows event记录了真实更新的记录的内容,最后一个Xid event中表示COMMIT操作。
在数据库中查看binlog文件内容可以很直观的看到,event的类型以及主要内容,那我们接着来看看,在binlog文件中,通过mysqlbinlog工具可以看到的具体内容是什么样的。
可以看到通过mysqlbinlog工具查看binlog文件,可以看到每个event中更加详细的内容。
STATEMENT格式下表现形式
以下显示内容是在MySQL-5.6.34版本上,且binlog日志记录格式为STATEMENT模式的情况下。
1 | root@localhost : gangshen 04:31:35> show binary logs; |
2 | +------------------+-----------+ |
3 | | Log_name | File_size | |
4 | +------------------+-----------+ |
5 | | mysql-bin.000001 | 143 | |
6 | | mysql-bin.000002 | 120 | |
7 | +------------------+-----------+ |
8 | 2 rows in set (0.00 sec) |
9 | |
10 | root@localhost : gangshen 04:31:48> show binlog events in 'mysql-bin.000002'; |
11 | +------------------+-----+-------------+-----------+-------------+---------------------------------------+ |
12 | | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | |
13 | +------------------+-----+-------------+-----------+-------------+---------------------------------------+ |
14 | | mysql-bin.000002 | 4 | Format_desc | 330619 | 120 | Server ver: 5.6.34-log, Binlog ver: 4 | |
15 | +------------------+-----+-------------+-----------+-------------+---------------------------------------+ |
16 | 1 row in set (0.00 sec) |
17 | |
18 | root@localhost : gangshen 04:32:02> insert into test1(`name`) values('woqu112233'); |
19 | Query OK, 1 row affected (0.05 sec) |
20 | |
21 | root@localhost : gangshen 04:32:29> show binlog events in 'mysql-bin.000002'; |
22 | +------------------+-----+-------------+-----------+-------------+----------------------------------------------------------------+ |
23 | | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | |
24 | +------------------+-----+-------------+-----------+-------------+----------------------------------------------------------------+ |
25 | | mysql-bin.000002 | 4 | Format_desc | 330619 | 120 | Server ver: 5.6.34-log, Binlog ver: 4 | |
26 | | mysql-bin.000002 | 120 | Query | 330619 | 212 | BEGIN | |
27 | | mysql-bin.000002 | 212 | Intvar | 330619 | 244 | INSERT_ID=8 | |
28 | | mysql-bin.000002 | 244 | Query | 330619 | 377 | use `gangshen`; insert into test1(`name`) values('woqu112233') | |
29 | | mysql-bin.000002 | 377 | Xid | 330619 | 408 | COMMIT /* xid=17 */ | |
30 | +------------------+-----+-------------+-----------+-------------+----------------------------------------------------------------+ |
31 | 5 rows in set (0.00 sec) |
从数据库中我们可以看到insert into test1(name) values('woqu112233');
这个insert语句转换成了4个event,分别是Query,Intvar,Quert,Xid
4个event。
第一个Query的event表示语句开始执行,第二个Intvar的event,是因为表结构中的id字段定义的是auto_increment,这个Intvar event是对自增的主键生成主键值的,接着的Query的event就是真实执行的语句了,最后一个Xid表示语句的提交。
在数据库中查看binlog文件内容可以很直观的看到,event的类型以及主要内容,那我们接着来看看,在binlog文件中,通过mysqlbinlog工具可以看到的具体内容是什么样的。
UPDATE语句在binlog中event表现形式
下面,我们就看一下一条UPDATE语句在会产生哪些类型的event,这些event在数据库中查看是怎么样的,在binlog文件中查看是怎么样的。
ROW格式下表现形式
以下显示内容是在MySQL-5.6.34版本上,且binlog日志记录格式为ROW模式的情况下。
1 | mysql> show binary logs; |
2 | +------------------+-----------+ |
3 | | Log_name | File_size | |
4 | +------------------+-----------+ |
5 | | mysql-bin.000001 | 120 | |
6 | +------------------+-----------+ |
7 | 1 row in set (0.00 sec) |
8 | |
9 | mysql> show binlog events in 'mysql-bin.000001'; |
10 | +------------------+-----+-------------+-----------+-------------+---------------------------------------------+ |
11 | | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | |
12 | +------------------+-----+-------------+-----------+-------------+---------------------------------------------+ |
13 | | mysql-bin.000001 | 4 | Format_desc | 9999 | 120 | Server ver: 5.6.34-debug-log, Binlog ver: 4 | |
14 | +------------------+-----+-------------+-----------+-------------+---------------------------------------------+ |
15 | 1 row in set (0.00 sec) |
16 | |
17 | mysql> update test1 set name='woqutech_new' where name='woqutech'; |
18 | Query OK, 1 row affected (0.01 sec) |
19 | Rows matched: 1 Changed: 1 Warnings: 0 |
20 | |
21 | mysql> show binlog events in 'mysql-bin.000001'; |
22 | +------------------+-----+-------------+-----------+-------------+--------------------------------------------------------------+ |
23 | | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | |
24 | +------------------+-----+-------------+-----------+-------------+--------------------------------------------------------------+ |
25 | | mysql-bin.000001 | 4 | Format_desc | 9999 | 120 | Server ver: 5.6.34-debug-log, Binlog ver: 4 | |
26 | | mysql-bin.000001 | 120 | Query | 9999 | 196 | BEGIN | |
27 | | mysql-bin.000001 | 196 | Rows_query | 9999 | 278 | # update test1 set name='woqutech_new' where name='woqutech' | |
28 | | mysql-bin.000001 | 278 | Table_map | 9999 | 333 | table_id: 70 (gangshen.test1) | |
29 | | mysql-bin.000001 | 333 | Update_rows | 9999 | 401 | table_id: 70 flags: STMT_END_F | |
30 | | mysql-bin.000001 | 401 | Xid | 9999 | 432 | COMMIT /* xid=16 */ | |
31 | +------------------+-----+-------------+-----------+-------------+--------------------------------------------------------------+ |
32 | 6 rows in set (0.00 sec) |
从数据库中,我们可以看到update test1 set name='woqutech_new' where name='woqutech';
语句,在binlog日志文件中转换成了5个event存储,分别为Query,Rows_query,Table_map,Update_rows,Xid类型的event,这些event中,Query event表示一个更新语句的开始,Rows_query event记录了更新语句的语句内容,Table_map event中记录了insert语句操作的表的信息,Update_rows event记录了真实更新的记录的内容,最后一个Xid event中表示COMMIT操作。
在数据库中查看binlog文件内容可以很直观的看到,event的类型以及主要内容,那我们接着来看看,在binlog文件中,通过mysqlbinlog工具可以看到的具体内容是什么样的。
可以看到通过mysqlbinlog工具查看binlog文件,可以看到每个event中更加详细的内容。
STATEMENT格式下表现形式
以下显示内容是在MySQL-5.6.34版本上,且binlog日志记录格式为STATEMENT模式的情况下。
1 | mysql> show binary logs; |
2 | +------------------+-----------+ |
3 | | Log_name | File_size | |
4 | +------------------+-----------+ |
5 | | mysql-bin.000001 | 120 | |
6 | +------------------+-----------+ |
7 | 1 row in set (0.00 sec) |
8 | |
9 | mysql> show binlog events in 'mysql-bin.000001'; |
10 | +------------------+-----+-------------+-----------+-------------+---------------------------------------------+ |
11 | | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | |
12 | +------------------+-----+-------------+-----------+-------------+---------------------------------------------+ |
13 | | mysql-bin.000001 | 4 | Format_desc | 9999 | 120 | Server ver: 5.6.34-debug-log, Binlog ver: 4 | |
14 | +------------------+-----+-------------+-----------+-------------+---------------------------------------------+ |
15 | 1 row in set (0.00 sec) |
16 | |
17 | mysql> update test1 set name='woqutech_new' where name='woqutech'; |
18 | Query OK, 1 row affected (0.01 sec) |
19 | Rows matched: 1 Changed: 1 Warnings: 0 |
20 | |
21 | mysql> show binlog events in 'mysql-bin.000001'; |
22 | +------------------+-----+-------------+-----------+-------------+----------------------------------------------------------------------------+ |
23 | | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | |
24 | +------------------+-----+-------------+-----------+-------------+----------------------------------------------------------------------------+ |
25 | | mysql-bin.000001 | 4 | Format_desc | 9999 | 120 | Server ver: 5.6.34-debug-log, Binlog ver: 4 | |
26 | | mysql-bin.000001 | 120 | Query | 9999 | 207 | BEGIN | |
27 | | mysql-bin.000001 | 207 | Query | 9999 | 347 | use `gangshen`; update test1 set name='woqutech_new' where name='woqutech' | |
28 | | mysql-bin.000001 | 347 | Xid | 9999 | 378 | COMMIT /* xid=46 */ | |
29 | +------------------+-----+-------------+-----------+-------------+----------------------------------------------------------------------------+ |
30 | 4 rows in set (0.00 sec) |
从数据库中我们可以看到update test1 set name='woqutech_new' where name='woqutech';
这个insert语句转换成了3个event,分别是Query,Quert,Xid
3个event。第一个Query的event表示语句开始执行,接着的Query的event就是真实执行的语句了,最后一个Xid表示语句的提交。
在数据库中查看binlog文件内容可以很直观的看到,event的类型以及主要内容,那我们接着来看看,在binlog文件中,通过mysqlbinlog工具可以看到的具体内容是什么样的。
DELETE语句在binlog中event表现形式
下面,我们就看一下一条DELETE语句在会产生哪些类型的event,这些event在数据库中查看是怎么样的,在binlog文件中查看是怎么样的。
ROW格式下表现形式
以下显示内容是在MySQL-5.6.34版本上,且binlog日志记录格式为ROW模式的情况下。
1 | root@localhost : gangshen 09:34:02> show binary logs; |
2 | +------------------+-----------+ |
3 | | Log_name | File_size | |
4 | +------------------+-----------+ |
5 | | mysql-bin.000001 | 120 | |
6 | +------------------+-----------+ |
7 | 1 row in set (0.00 sec) |
8 | |
9 | root@localhost : gangshen 09:34:22> show binlog events in 'mysql-bin.000001'; |
10 | +------------------+-----+-------------+-----------+-------------+---------------------------------------+ |
11 | | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | |
12 | +------------------+-----+-------------+-----------+-------------+---------------------------------------+ |
13 | | mysql-bin.000001 | 4 | Format_desc | 330619 | 120 | Server ver: 5.6.34-log, Binlog ver: 4 | |
14 | +------------------+-----+-------------+-----------+-------------+---------------------------------------+ |
15 | 1 row in set (0.00 sec) |
16 | |
17 | mysql> delete from test1 where id = 1; |
18 | Query OK, 1 row affected (0.02 sec) |
19 | |
20 | mysql> show binlog events in 'mysql-bin.000001'; |
21 | +------------------+-----+-------------+-----------+-------------+---------------------------------------------+ |
22 | | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | |
23 | +------------------+-----+-------------+-----------+-------------+---------------------------------------------+ |
24 | | mysql-bin.000001 | 4 | Format_desc | 9999 | 120 | Server ver: 5.6.34-debug-log, Binlog ver: 4 | |
25 | | mysql-bin.000001 | 120 | Query | 9999 | 196 | BEGIN | |
26 | | mysql-bin.000001 | 196 | Rows_query | 9999 | 250 | # delete from test1 where id = 1 | |
27 | | mysql-bin.000001 | 250 | Table_map | 9999 | 305 | table_id: 70 (gangshen.test1) | |
28 | | mysql-bin.000001 | 305 | Delete_rows | 9999 | 358 | table_id: 70 flags: STMT_END_F | |
29 | | mysql-bin.000001 | 358 | Xid | 9999 | 389 | COMMIT /* xid=27 */ | |
30 | +------------------+-----+-------------+-----------+-------------+---------------------------------------------+ |
31 | 6 rows in set (0.00 sec) |
从数据库中,我们可以看到delete from test1 where id = 1;
语句,在binlog日志文件中转换成了5个event存储,分别为Query,Rows_query,Table_map,Delete_rows,Xid
类型的event,这些event中,Query event表示一个更新语句的开始,Rows_query event记录了更新语句的语句内容,Table_map event中记录了insert语句操作的表的信息,Delete_rows event记录了真实更新的记录的内容,最后一个Xid event中表示COMMIT操作。
在数据库中查看binlog文件内容可以很直观的看到,event的类型以及主要内容,那我们接着来看看,在binlog文件中,通过mysqlbinlog工具可以看到的具体内容是什么样的。
可以看到通过mysqlbinlog工具查看binlog文件,可以看到每个event中更加详细的内容。
STATEMENT格式下表现形式
以下显示内容是在MySQL-5.6.34版本上,且binlog日志记录格式为STATEMENT模式的情况下。
1 | mysql> show binary logs; |
2 | +------------------+-----------+ |
3 | | Log_name | File_size | |
4 | +------------------+-----------+ |
5 | | mysql-bin.000001 | 120 | |
6 | +------------------+-----------+ |
7 | 1 row in set (0.00 sec) |
8 | |
9 | mysql> show binlog events in 'mysql-bin.000001'; |
10 | +------------------+-----+-------------+-----------+-------------+---------------------------------------------+ |
11 | | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | |
12 | +------------------+-----+-------------+-----------+-------------+---------------------------------------------+ |
13 | | mysql-bin.000001 | 4 | Format_desc | 9999 | 120 | Server ver: 5.6.34-debug-log, Binlog ver: 4 | |
14 | +------------------+-----+-------------+-----------+-------------+---------------------------------------------+ |
15 | 1 row in set (0.00 sec) |
16 | |
17 | mysql> delete from test1 where name = 'woqutech_new'; |
18 | Query OK, 1 row affected (0.01 sec) |
19 | |
20 | mysql> show binlog events in 'mysql-bin.000001'; |
21 | +------------------+-----+-------------+-----------+-------------+---------------------------------------------------------------+ |
22 | | Log_name | Pos | Event_type | Server_id | End_log_pos | Info | |
23 | +------------------+-----+-------------+-----------+-------------+---------------------------------------------------------------+ |
24 | | mysql-bin.000001 | 4 | Format_desc | 9999 | 120 | Server ver: 5.6.34-debug-log, Binlog ver: 4 | |
25 | | mysql-bin.000001 | 120 | Query | 9999 | 207 | BEGIN | |
26 | | mysql-bin.000001 | 207 | Query | 9999 | 334 | use `gangshen`; delete from test1 where name = 'woqutech_new' | |
27 | | mysql-bin.000001 | 334 | Xid | 9999 | 365 | COMMIT /* xid=58 */ | |
28 | +------------------+-----+-------------+-----------+-------------+---------------------------------------------------------------+ |
29 | 4 rows in set (0.00 sec) |
从数据库中我们可以看到delete from test1 where name = 'woqutech_new';
这个insert语句转换成了3个event,分别是Query,Quert,Xid
3个event。第一个Query的event表示语句开始执行,接着的Query的event就是真实执行的语句了,最后一个Xid表示语句的提交。
在数据库中查看binlog文件内容可以很直观的看到,event的类型以及主要内容,那我们接着来看看,在binlog文件中,通过mysqlbinlog工具可以看到的具体内容是什么样的。