由之前的文章可以了解到,二进制日志在复制中起到举足轻重的作用,所以这一篇文章着重了解一下Mysql复制背后核心组件:二进制日志的庐山真面目。
二进制日志的结构
从概念上讲,二进制日志是一系列二进制日志事件。它包括一系列的binlog文件和一个binlog索引文件,当前服务器正在写入的binlog文件称之为active binlog。其文件名是通过配置文件中的log-bin和log-bin-index来定义的。
每个binlog文件是由若干binlog事件组成,以Format_description事件开始,以Rotate事件作为文件尾。
Format_description事件包含写binlog文件的服务器信息,以及关于文件状态的关键信息。如果服务器关闭或者重新启动,会创建一个新的binlog文件,同时写入新的Format_description事件,这个事件是必须的,因为服务器关闭和重启都会产生更新。服务器写完binlog文件后,在文件结尾添加一个Rotate事件,该事件包含下一个binlog文件的文件名及其开始读取的位置。除了Format_description和Rotate事件之外,binlog文件的其他事件都被分成 group进行管理。在事务存储引擎中,每个组大致对应一个事务,对于非事务存储引擎,每个语句本身就是一个组。通常情况下,每个组要么全部执行,要么去不执行。如果由于某种原因Slave在组执行的过程中停机,那么将从该组的起点而不是刚刚执行的语句开始复制。
binlog事件的结构
二进制日志版本4(binlog format 4)是在MySQL 5.0中引入,是专门为扩展而设计的。这里主要讨论二进制日志版本4。(MySQL 3.23 4.0 4.1版本都是使用二进制日志版本3)
每个binlog事件由三个部分组成:
- 通用头(common header):大小固定。事件的基本信息,其中重要的字段是事件类型和事件大小。
- 提交头(post header):大小固定。提交头与特定的事件类型相关
- 事件体(Event body):大小可变。事件体存储事件的主要数据,因事件类型不同而异。
具体看一下Format_description事件:
- binlog文件格式版本
- 服务器版本字符串:一般包括三部分,即版本号、连字符和其他构建项。例如:5.1.42-debug-log
- 通用头的长度:存储了通用头的长度。这里是指Format_description事件,所以不同binlog文件该字段的值不同。除了Format_description和Rotate事件外,其他事件的通用头长度都是可变的。Format_description事件的通用头长度是不变的,是因为任何版本的服务器都需要读取这个事件。Rotate事件的通用头长度也是不变的,是因为Slave连接Master时首先要用到该事件。
- 提交头的长度:binlog文件中所有事件的提交头长度是不变的,该字段存储了各个事件的提交头长度构成的数组。由于不同服务器间的事件数目不同,所以这个字段前面还存储了服务器的事件数目。
通过事件来记录数据库变更
首先,由于二进制日志是公共资源,所有线程都向它写入语句,为了避免两个线程同时更新二进制日志,在写之前需要获得一个互斥锁Lock_log,写完之后再释放。
所有涉及到数据库更新的语句都会以Query事件的形式写入二进制日志中,除了实际执行的语句外,Query事件还包含执行语句必需的上下文附加信息。下面给出了如何记录这些上下文信息
- 当前数据库:在Query事件添加一个特殊字段记录当前数据库。
- 用户自定义变量的值:User_var事件记录单个用户自定义的变量的变量名及其值。
- RAND函数的种子:Rand事件记录Rand函数所用的随机数种子。
- 当前时间:NOW,CURDATE,CURTIME,UNIX_TIMESTAMP和SYSDATE这五个函数会用到当前时间,针对这个事件会存储一个时间戳,表示事件何时开始执行。
- AUTO_INCREMENT字段的插入值:Intvar事件记录在语句开始前,表内部的自动增量计数器的值。
- 调用LAST_INSERTED_ID的返回值:Intvar事件记录这个函数在语句的返回值。
- 线程ID:主要是涉及到临时表的处理。线程ID也是作为一个独立的字段存储在Query事件中。
* 对于SYSDATE函数,它返回的是函数执行时的时间,这一点不同于NOW函数,NOW返回的是语句执行的时间。所以SYSDATE对于复制来说是不安全的,尽量少用。
LOAD DATA INFILE语句
LOAD DATA INFILE比较特殊,它的上下文是文件系统的文件。要正确地传递和执行LOAD DATA INFILE语句,需要引入新的事件类型:
- Begin_load_query:这个事件开始传输文件中的数据
- Append_block:如果这个文件超过了连接的数据包大小所允许的最大值,那么跟随在Begin_load_query事件后面的一个或多个Append_block事件的系列包含着这个文件的剩余部分
- Execute_load_query:Query事件的特殊变种,它包含了在Master上执行的LOAD DATA INFILE语句
对Master上执行的每个LOAD DATA INFILE语句而言,被读取的文件被映射到一个支持内部文件的缓冲区,并在接下来的处理流程中使用。此外,一个唯一的文件ID被分配给该执行语句,并用于指向该语句读取的文件。
当语句在执行的时,该文件的内容被写入二进制日志,作为以Begin_load_query事件开头的事件序列,Begin_load_query事件表示新文件的开始,且这个事件序列后面紧跟着零个或多个Append_block事件。每个写入二进制的事件都不会超过包大小所允许的最大值,这个最大值由max-allowed_packet选项指定。
当整个文件读取到表中后,通过写Execute_load_query事件到二进制日志来终止语句的执行。这个事件包含了执行语句和分配给该执行语句的文件ID。请注意,这并非是用户写的原始语句,而是重新创建的。
* Mysql 5.0.3之前的版本使用的事件名有点不一样,依次为Load_log_event,Execute_log_event,Create_file_log_event
二进制日志过滤器
my.cnf中有两个选项可用于过滤日志:binlog-do-db和binlog-ignore-db。这两个选项可以使用多次。
MySQL过滤事件的方式对于不熟悉的人来说可能有点奇怪。Mysql过滤是在语句级完成的,binlog-*-db使用当前数据库来决定是否应该过滤该语句,而不是由语句所影响的表所在的数据库决定的。对于下面的例子,使用binlog-ignore-db=bad筛选bad数据库,下例中一个都不会写入日志。
USE bad; INSERT INTO t1 VALUES (1),(2);
USE bad; INSERT INTO good.t2 VALUES (1),(2);
USE bad; UPDATE good.t1, ugly.t2 SET a = b;
至于为什么不是以语句所影响的表所在的数据库来决