天天看点

MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))

  • 事务隔离性可以使用前面介绍的锁来实现。原子性、一致性、持久性通过数据库的redo log和undo log来完成:
  • redo log:称为重做日志。用来保证事务的原子性和持久性
  • undo log:用来保证事务的一致性
  • redo和undo的作用都可以视为一种恢复操作:
  • redo恢复提交事务修改的页操作
  • undo回滚行记录到某个特定版本
  • 因此两者记录的内容也不同:
  • redo通常是物理日志,记录的是页的物理修改操作
  • undo是逻辑日志,根据每行记录进行记录

一、redo基本概述

  • 重做日志用来实现事务的持久性,即事务ACID中的D。其由两部分组成:
  • 一是内存中的重做日志缓冲(redo log buffer),其是易失的
  • 二是重做日志文件(redo log file),其是持久的

工作原理

  • InnoDB事务的存储引擎,其通过Force Log at Commit机制实现事务的持久性,即当事务提交(commit)时,必须先将该事务的所有日志写入重做日志文件进行持久化,待事务的COMMIT操作完成才算完成
  • 这里的日志是指重做日志,在InnoDB存储引擎中,由两部分组成:即redo log和undo log
  • redo log:用来保证事务的持久性。基本上都是顺序写的,在数据库运行时不需要对redo log的文件进行读取操作
  • undo log:用来帮助事务回滚及MVCC的功能。是需要进行随机读写的

fsync操作

  • 为了确保每次日志都写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后,InnoDB都需要调用一次fsync操作
  • 由于重做日志文件打开并没有使用O_DIRECT选项,因此重做日志缓冲先写入文件系统缓存。为了确保重做日志写入磁盘,必须进行一次fsync操作
  • 由于fsync的效率取决于磁盘的性能,因此磁盘的性能决定了事务提交的性能,也就是数据库的性能
  • InnoDB允许用户手工设置非持久的情况发生,以此提高数据库的性能。即当事务提交时,日志不写入重做日志文件,而是等待 一个事件周期后再执行fsync操作。由于并非强制在事务提交时进行一次fsync操作,显然这可以显著提高数据库的性能。但是当数据库发生宕机时,由于部分日志未刷新到磁盘,因此会丢失最后一段事件的事务(具体见下面的innodb_flush_log_at_trx_commit参数)

innodb_flush_log_at_trx_commit参数

  • 该参数用来控制重做日志刷新到磁盘的策略
  • 取值如下:
  • 0:表示事务提交时不进行写入重做日志操作,这个操作仅在master thread中完成,而master thread中每1秒会进行一次重做日志文件的fsync操作
  • 1(默认值):表示事务提交时必须调用一次fsync操作
  • 2:表示事务提交时将重做日志写入重做日志文件,但仅写入文件系统的缓存中,不进行fsync操作。在这个设置下,当MySQL数据库发生宕机而操作系统不发生宕机时,并不会导致事务的丢失。而当操作系统宕机时,重启数据库会丢失未从文件系统缓存刷新到重做日志文件那部分事务
MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))

演示案例

  • 下面看一个例子,在不同的参数值下数据库的工作效率
  • 创建一个表test_load和一个存储过程p_load:
  • 存储过程的作用是将数据不断地插入表中,并且每插入一条就进行因此显式的commit操作

create table test_load(

a int,

b char(80)

)engine=innodb;

delimiter //

create procedure p_load(count int unsigned)

begin

declare s int unsigned default 1;

declare c char(80) default repeat('a',80);

while s<=count do

insert into test_load select NULL,c;

commit;

set s=s+1;

end while;

end;

//

delimiter ;

MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))
  • innodb_flush_log_at_trx_commit的值为1时:执行下面的命令,向表中插入50万行的记录,并执行50万次的fsync操作:
  • 看到插入50万条记录差不多需要2分钟的时间,在实际生产环境中这个时间是用户不能接受的(时间长的主要原因就是fsync操作所需的时间)
call p_load(500000);
MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))
  • innodb_flush_log_at_trx_commit的值为0时:删除表中的数据,然后再向表中插入50万行的记录,结果如下:
  • 看到插入50万条记录只需要8秒左右

delete from test_load;

set global innodb_flush_log_at_trx_commit=0;

show variables like 'innodb_flush_log_at_trx_commit';

call p_load(500000);

MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))
  • innodb_flush_log_at_trx_commit的值为2时:删除表中的数据,然后再向表中插入50万行的记录,结果如下:
  • 看到插入50万条记录差不多需要14秒左右

delete from test_load;

set global innodb_flush_log_at_trx_commit=2;

show variables like 'innodb_flush_log_at_trx_commit';

call p_load(500000);

MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))
  • 由此可以看出,fsync的操作减少,数据库执行的性能也提高。但是将innodb_flush_log_at_trx_commit设置为0或2来提高事务提交的性能,但是却丧失了事务的ACID特性
  • 对于上面的存储过程,为了提高事务的提交性能,应该在将50万行记录插入表后进行一次总的COMMIT操作,而不是在每插入一条记录后就进行一次COMMIT。这样做的好处是还可以使事务回滚时回滚到事务最开始的确定状态

二、重做日志与二进制日志的不同

  • 二进制日志介绍参阅:​​MySQL(InnoDB剖析):11---文件之(日志文件:错误日志(error log)、慢查询日志(slow query log)、查询日志(query log)、二进制日志(bin log))_董哥的黑板报的博客-​​
  • 二进制日志其用来进行POINT-IN-TIME(PIT)的恢复以及主从复制(Replication)环境的建立。从表面上看其和重做日志非常相似,都是记录对于数据库操作的日志。然而,从本质上看,两者有着非常大的不同

不同点①

  • 日志是在InnoDB存储引擎层产生
  • 二进制日志是在MySQL数据库的上层产生的,并且二进制日志不仅仅针对于InnoDB而言,MySQL数据库中的任何存储引擎对于数据库的更改都会产生二进制日志

不同点②

  • 两种日志记录的内容形式不同:
  • 二进制日志是一种逻辑日志,其记录的是对应的SQL语句
  • 重做日志是物理格式日志,其记录的是对于每个页的修改

不同点③

  • 两种日志记录写入磁盘的时间点不同
  • 二进制日志只在事务提交完成后进行一次写入
  • 重做日志在事务进行中不断地被写入,这表现为日志并不是随事务提交的顺序写入的
  • 从下图可以看出:
  • 二进制日志仅在事务提交时记录,并且对于每一个事务,仅包含对应事务的一个日志
  • 重做日志其记录的物理操作日志,因此每个事务对应多个日志条目,并且事务的重做日志写入是并发的,并非在事务提交时写入,故其在文件中记录的顺序并非是事务开始的顺序(下图中带有*的,意为该事务的提交)
MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))

三、重做日志块(log block)

  • 在InnoDB中,重做日志都是以512字节进行存储的
  • 这意味着重做日志缓存、重做日志文件都是以块(block)的方式进行保存的,称之为重做日志块(redo log block),每块的大小为512字节
  • 若一个页中产生的重做日志数量大于512字节,那么需要分割为多个重做日志块进行存储。此外,由于重做日志块的大小和磁盘扇区大小一样,都是512字节,因此重做日志的写入可以保证原子性,不需要doublewrite技术(doublewrite技术参阅:​​MySQL(InnoDB剖析):08---InnoDB关键特性(插入缓冲(Insert Buffer)、两次写(doublewrite)、自适应哈希索引(AHI)、异步IO(AIO)、刷新邻接页)_董哥的黑板报的博客-​​​

重做日志块结构

  • 重做日志块除了日志本身之外,还由日志块头(log block header)以及日志块尾(log block tailer)两部分组成
  • 重做日志头一共占用12字节
  • 重做日志尾占用8字节
  • 故每个重做日志块实际可以存储的大小为492字节(512-12-8)
MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))

重做日志缓存结构

  • 下图显示了重做日志缓存的结构,由每个为512字节大小的日志块所组成
MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))

重做日志头(log block header)

  • 重做日志头一共占用12字节,由4部分组成,如下图所示:
MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))

①LOG_BLOCK_HDR_NO

  • 该标记用来标识这个块(log block)位于重做日志块缓存数组中的位置
  • 其实递增并且循环使用的,占用4字节,由于第一位用来判断是否有flush bit,所以最大的值为2G

②LOG_BLOCK_HDR_DATA_LEN

  • 占用2字节,表示该块所占用的大小
  • 当log block被写满时,该值为0x200,表示使用全部log block空间,即占用512字节

③LOG_BLOCK_FIRST_REC_GROUP

  • 占用2字节,表示log block中第一个日志所在的偏移量
  • 如果该值的大小和LOG_BLOCK_HDR_DATA_LEN相同,则表示当前log block不包含新的日志
  • 若事务T1的重做日志1占用792字节,事务T2的重做日志占用100字节。由于每个log block实际只能保存492个字节,因此其在log buffer中的情况如下图所示:
  • 事务T1的重做日志占用792字节,因此需要占用两个log block。左侧的log block中的LOG_BLOCK_FIRST_REC_GROUP为12,即log block第一个日志的开始位置
  • 在第二个log block中,由于包含了之前事务T1的重做日志,事务T2的日志才是log block中第一个日志,因此该log block的LOG_BLOCK_FIRST_REC_GROUP为282(270+12)
MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))

④LOG_BLOCK_CHECKPOINT_NO

  • 占用4字节,表示该log block最后被写入时的检查点第4字节的值

重做日志尾(log block tailer)

  • 重做日志尾占用8字节,由1部分组成,如下图所示:
MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))

①LOG_BLOCK_TRL_NO

  • 其值和LOG_BLOCK_HDR_NO相同,并在函数lob_block_init中被初始化

四、重做日志组(log group)

  • log group为重做日志组,其中有多个重做日志文件。虽然源码中已经支持log group的镜像功能,但是在ha_innodbase.cc文件中进制了该功能,因此InnoDB存储一你请实际只有一个log group
  • log group是一个逻辑上的概念,并没有一个实际存储的物理文件来表示log group信息

重做日志文件

  • log group由多个重做日志文件组成,每个log group中的日志文件大小是相同的:
  • 且在InnoDB 1.2版本之前,重做日志文件的总大小要小于4GB(不能等于4GB)
  • 从InnoDB 1.2版本开始重做日志文件总大小的限制提高为了512GB
  • InnoSQL版本的InnoDB存储引擎在1.1版本就支持大于4GB的重做日志
  • 重做日志文件存储的就是之前在log buffer中保存的log block,因此其也是根据块的方式进行物理存储的管理,每个块的大小与log block一样,同样为512字节
  • 在InnoDB存储引擎运行过程中,log buffer根据一定的规则将内存中的log block刷新到磁盘。这个规则是:
  • 事务提交时
  • 当log buffer中有一般的内存空间已经被使用时
  • log checkpoint时

重做日志文件的格式与重做日志文件组格式

  • 对于log block的写入追加在redo log file的最后部分,当一个redo log file被写满时,会接着写入下一个redo log file,其使用方式为round-robin
  • 虽然log block总是在redo log file的最后部分进行写入,有的读者可能以为对redo log file的写入都是顺序的。其实不然,因为redo log file除了保存log buffer刷新到磁盘的log block,还保存了一些其他信息,这些信息一共占用2KB大小,即每个redo log file的前2KB的部分不保存log block的信息
  • 对于log group中的第一个redo log file,其前2KB的部分保存4个512字节大小的块,其中存放的内容如下图所示:
MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))
  • 需要注意的是:
  • 上述信息仅在每个log group的第一个redo log file中进行存储,log group中的其余redo log file仅保留这些空间,但不保存上述信息
  • 正因为保存了这些信息,就意味着对redo log file的写入并不是完全顺序的。因为其除了log block的写入操作,还需要更新前2KB部分的信息,这些信息对于InnoDB的恢复操作来说非常关键和重要
  • log group与redo log file之间的关系如下图所示:
MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))
  • 在log file header后面的部分为InnoDB保存的checkpoint(检查点)值,其设计是交替写入,这样的设计避免了因介质失败而导致无法找到可用的checkpoint的情况

五、重做日志格式

  • 上面介绍的是重做日志的存储格式,下面介绍的是重做日志的内容格式
  • 不同的数据库操作会有对应的重做日志格式。此外,由于InnoDB的存储管理是基于页的,故其重做日志格式也是基于页的
  • 虽然有着不同的重做日志格式,但是他们有着通用的头部结构:
MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))
  • 通用的头部格式由以下3部分组成:
  • redo_log_type:重做日志的类型
  • space:表空间的ID
  • page_no:页的偏移量
  • 之后的redo log body的部分,根据重做日志类型的不同,会有不同的存储内容。例如,对于页上记录的插入和删除操作,分别对应下面所示的格式:
MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))
  • 到InnoDB 1.2版本时,一共有51种重做日志类型。随着功能不断地增加,相信会加入越来越多的重做日志类型

六、LSN

  • LSN是Log Sequence Number的缩写,其代表的是日志序列号。在InnoDB存储引擎中,LSN占用8字节,并且单调递增
  • LSN表示的含义有:
  • 重做日志写入的总量
  • checkpoint的位置
  • 页的版本

重做日志写入的总量

  • LSN表示事务写入重做日志的字节的总量
  • 例如:当前重做日志的LSN为1000,有一个事务T1写入了100字节的重做日志,那么LSN就变为了1100,若有事务T2写入了200字节的重做日志,那么LSN就变为了1300
  • 可见LSN记录的是重做日志的总量,其单位为字节

checkpoint的位置

  • LSN不仅记录在重做日志中,还存在于每个页中。在每个页的头部,有一个值FIL_PAGE_LSN,记录了该页的LSN
  • 在页中,LSN表示该页最后刷新时LSN的大小
  • 因为重做日志记录的是每个页的日志,因此页中的LSN用来判断页是否需要进行恢复操作。例如:
  • 页P1的LSN为10000,而数据库启动时,InnoDB检测到写入重做日志中的LSN为13000,并且该事务已经提交,那么数据库需要进行恢复操作,将重做日志应用与P1页中
  • 同样的,对于重做日志中LSN小于P1页的LSN,不需要进行重做,因为P1页中的LSN表示页已经被刷新到该位置

查看LSN

  • 用户可以通过下面的命令查看LSN的情况:
  • Log sequence number:表示当前的LSN
  • Log flushed up to:表示刷新到重做日志文件的LSN
  • Last checkpoint at:表示刷新带磁盘的LSN
show engine innodb status\G
MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))
  • 虽然在上面的例子中,Log sequence number和Log flushed up to的值是相同的大,但是在实际生产环境中,该值有可能是不同的。因为在一个事务中从日志缓冲刷新到重做日志文件并不只是在事务提交时发生,每秒都会有从日志缓冲刷新到重做日志文件的东西。下面是在生产环境下重做日志的信息的实例
MySQL(InnoDB剖析):39---事务之(事务的实现:redo log(重做日志))
  • 可以看到,在生产环境下Log sequence number、Log flushed up to、Last checkpoint at三个值可能是不同的

七、恢复

  • InnoDB在启动时不管上次数据库运行时是否正常关闭,都会尝试进行恢复操作
  • 因为重做日志记录的是物理日志,因此恢复的速度比逻辑日志(如二进制日志)要快很多。与此同时,InnoDB自身也会恢复进行了一定程度的优化,如顺序读取及并行应用重做日志,这样可以进一步地提高数据库恢复的速度
  • 由于checkpoint表示已经刷新到磁盘页上的LSN,因此在恢复过程中仅需恢复checkpoint开始的日志部分
  • 对于下图所示的例子,当数据库在checkpoint的LSN为10000时发生宕机,恢复操作仅恢复LSN 10000~13000范围内的日志
  • InnoDB的重做日志是物理日志,因此其恢复速度较之二进制日志恢复快得多。例如对于INSERT操作,其记录的是每个页上的变化。对于下面的表:
create table t(
    a int,
    b int,
    primary key(a),
    key(b)
);      
  • 若执行SQL语句
insert into t select 1,2;      
  • 由于需要聚集索引和辅助索引页进行操作,其记录的重做日志大致为:
  • 可以看到记录的是页的物理修改操作,若插入设计B+树的split,可能会有更多的页需要记录日志。此外,由于重做日志是物理日志,因此其是幂等的。幂等的概念如下:
  • 有的DBA或开发人员错误的认为只要将二进制日志的格式设置为ROW,那么二进制日志也是幂等的。这显然是错误的,举个简单的例子,INSERT操作在二进制日志中就不是幂等的,重复执行可能会插入多条重复的记录。而上述INSERT操作的重做日志是幂等的

继续阅读