> 文章列表 > Mysql日志系统-InnoDB引擎层

Mysql日志系统-InnoDB引擎层

Mysql日志系统-InnoDB引擎层

一、redo log日志

接下来的两个日志,是innodb为解决不同问题而引出的两类日志文件。

redo log(重做日志)的设计主要是为了防止因系统崩溃而导致的数据丢失,其实解决因系统崩溃导致数据丢失的思路如下:

1、每次提交事务之前,必须将所有和当前事务相关的【buffer pool中的脏页】刷入磁盘,但是,这个效率比较低,可能会影响主线程的效率,产生用户等待,降低响应速度,因为刷盘是I/O操作,同时一个事务的读写操作也不是顺序读写,是随机I/O。

2、把当前事务中修改的数据内容在日志中记录下来,日志记录是顺序写,性能很高。其实mysql就是这么做的,这个日志被称为redo log。执行事务中,每执行一条语句,就可能有若干redo日志,并按产生的顺序写入磁盘,redo日志占用的空间非常小,当redo log空间满了之后又会从头开始以循环的方式进行覆盖式的写入。

3、需要理解:为什么不直接更新数据,而是记录数据的变化 ?

4、总结:redo log记录的是数据变化的值,保存的数据再内存后修改的值,当系统崩溃后,我们就可以查看redo log有哪些值被修改了但是还没有被刷盘,这样在启动时进行恢复

redo log的格式比较简单,包含一下几个部分:

  • type:该日志的类型,在5.7版本中,大概有53种不同类型的redo log,占用一个字节

  • space id:表空间id

  • page number:页号

  • data:日志数据

1、MTR

在innodb执行任务时,有很多操作,必须具有原子性,我们把这一类操作称之为MIni Transaction。

我们以下边的例子为例:

在我们向B+树中插入一条记录的时候,需要定位这条数据将要插入的【数据页】,因为插入的位置不同,可能会有以下情况:

1、待插入的页拥有【充足的剩余空间】,足以容纳这条数据,那就直接插入就好了,这种情况需要记录一条【MLOG_COMP_REC_INSERT类型】的redo日志就好了,这种情况成为乐观插入。
Mysql日志系统-InnoDB引擎层

2、待插入的页【剩余空间不足】以容纳该条记录,这样就比较麻烦了,必须进行【页分裂】了。必须新建一个页面,将原始页面的数据拷贝一部分到新页面,然后插入数据。这其中对应了好几个操作,必须记录多条rede log,包括申请新的数据页、修改段、区的信息、修改各种链表信息等操作,需要记录的redo log可能就有二三十条,但是本次操作必须是一个【原子性操作】,在记录的过程中,要全部记录,要么全部失败,这种情况就被称之为一个MIni Transaction(最小事务)。

Mysql日志系统-InnoDB引擎层

(1)MTR的按组写入

对于一个【MTR】操作必须是原子的,为了保证原子性,innodb使用了组的形式来记录redo 日志,在恢复时,要么这一组的的日志全部恢复,要么一条也不恢复。innodb使用一条类型为MLO_MULTI_REC_END类型的redo log作为组的结尾标志,在系统崩溃恢复时只有解析到该项日志,才认为解析到了一组完整的redo log,否则直接放弃前边解析的日志。

Mysql日志系统-InnoDB引擎层

(2)单条redolog的标识方法

有些操作只会产生一条redo log,innodb是通过【类型标识】的第一个字符来判断,这个日志是单一日志还是组日志,如下图:

Mysql日志系统-InnoDB引擎层

(3)事务、sql、MTR、redolog的关系如下

  • 一个事务包含一条或多条sql
  • 一条sql包含一个或多个MTR
  • 一个MTR包含一个或多个redo log

2、log buffer

任何可能产生大量I/O的操作,一般情况下都会设计【缓冲层】,mysql启动时也会向操作系统申请一片空间作为redo log的【缓冲区】,innodb使用一个变量buf_free来标记下一条redo log的插入位置(标记偏移量),log buffer会在合适的时机进行刷盘:

  • log buffer空间不足。logbuffer的容量由innodb_log_buffer_size指定,当写入log buffer的日志大于容量的50%,就会进行刷盘。
  • 提交事务时,如果需要实现崩溃恢复,保证数据的持久性,提交事务时必须提交redo log,当然你也可以为了效率不去提交,可以通过修改配置文件设置该项目。
  • 后台有独立线程大约每隔一秒会刷新盘一次。
  • 正常关闭服务器。
  • 做checkpoint时,后边会讲。

有缓冲就可能存在数据不一致,咱们接着往下看。

3、checkpoint

redolog日志文件容量是有限的,需要循环使用,redo log的作用仅仅是为了在崩溃时恢复脏页数据使用的,如果脏页已经刷到磁盘上,其对应的redo log也就没用了,他也就可以被重复利用了。checkpoint的作用就是用来标记哪些旧的redo log可以被覆盖。

我们已经知道,判断redo log占用的磁盘空间是否可以被重新利用的标志就是,对应的脏页有没有被刷新到磁盘。为了实现这个目的,我们需要了解一下下边几个记录值的作用:

(1)lsn

lsn(log sequence number)是一个全局变量。mysql在运行期间,会不断的产生redo log,日志的量会不断增加,innodb使用lsn来记录当前总计写入的日志量,lsn的初始值不是0,而是8704,原因未知。系统在记录lsn时是按照【偏移量】不断累加的。lsn的值越小说明redo log产生的越早。

每一组redo log都有一个唯一的lsn值和他对应,你可以理解为lsn是redo log的年龄。

(2)flush_to_disk_lsn

flush_to_disk_lsn也是一个全局变量,表示已经刷入磁盘的redo log的量,他小于等于lsn,举个例子:

1、将redo log写入log buffer,lsn增加,假如:8704+1024 = 9728,此时flush_to_disk_lsn不变。

2、刷如512字节到磁盘,此时flush_to_disk_lsn=8704+512=9256。

如果两者数据相同,说明已经全部刷盘。

(3)flush链中的lsn

其实要保证数据不丢失,核心的工作是要将buffer pool中的脏页进行刷盘,但是刷盘工作比较损耗性能,需要独立的线程在后台静默操作。

回顾一下flush链,当第一次修改某个已经加载到buffer pool中的页面时,他会变成【脏页】,会把他放置在flush链表的头部,flush链表是按照第一次修改的时间排序的。再第一次修改缓冲页时,会在【缓冲页对应的控制块】中,记录以下两个属性:

  • oldest_modification:第一次修改缓冲页时,就将【修改该页面的第一组redo log的lsn值】记录在对应的控制块。
  • newest_modification:每一次修改缓冲页时,就将【修改该页面的最后组redo log的lsn值】记录在对应的控制块。

既然flush链表是按照修改日期排序的,那么也就意味着,oldest_modification的值也是有序的。

(4)checkpoint过程

执行一个check point可以分为两个步骤

第一步:计算当前redo log文件中可以被覆盖的redo日志对应的lsn的值是多少:

1、flush链是按照第一次修改的时间排序的,当然控制块内的【oldest_modification】记录的lsn值也是有序的。

2、我们找到flush链表的头节点上的【oldest_modification】所记录的lsn值,也就找到了一个可以刷盘的最大的lsn值,小于这个值的脏页,肯定已经刷入磁盘。

3、所有小于这个lsn值的redo log,都可以被覆盖重用。

4、将这个lsn值赋值给一个全局变量checkpoint_lsn,他代表可以被覆盖的量。

第二步:将checkpoint_lsn与对应的redo log日志文件组偏移量以及此次checkpoint的编号(checkpoint_no也是一个变量,记录了checkpoint的次数)全部记录在日志文件的管理信息内。

4、一个事务的执行流程

Mysql日志系统-InnoDB引擎层

主线程

1、客户端访问mysql服务,在buffer pool中进行操作(如果目标页不在缓冲区,需要加载进入缓冲区),此时会形成脏页。

2、记录redo log,可能产生很多组日志,redo log优先记录在缓冲区,会在提交事务前刷盘。

3、刷盘时根据checkpoint的结果,选择可以使用的日志空间进行记录。

4、成功后即可返回,此时数据不会落盘,这个过程很多操作只在内存进行,只需要记录redo log(顺序写),所以速度很快。

线程1:

1、不断的对flush链表的脏页进行刷盘,对响应时间没有过高要求。

线程2:

1、不断的进行checkpoin操作,保证redo log可以及时写入。

5、系统崩溃的影响

(1)log buffer中的日志丢失,log buffer中的日志会在每次事务前进行刷盘,如果在事务进行中崩溃,事务本来就需要回滚。

(2)buffer pool中的脏页丢失,崩溃后可以通过redo log恢复,通过checkpoint操作,我们可以确保,内存中脏页对应的记录都会在redo log日志中存在。

redo log保证了崩溃后,数据不丢失,但是一个事务进行中,如果一部分redo log已经刷盘,那么系统会将本应回滚的数据同样恢复,为了解决回滚的问题,innodb提出了undo log。

二、undo log日志

1、概述

undo log(也叫撤销日志或者回滚日志),他的主要作用是为了实现回滚操作。同时,他是MVCC多版本控制的核心模块。undo log保存在共享表空间【ibdata1文件】中。

Mysql日志系统-InnoDB引擎层

注意:8.0以后undolog有了独立的表空间:

Mysql日志系统-InnoDB引擎层

在讲undo log之前需要先了解行数据中的两个隐藏列:

2、事务id(trx_id)

我们已经讲过,在innodb的行数据中,会自动添加两个隐藏列,一个是【trx_id】,一个是【roll_pointer】,本章节会详细介绍这两列的具体作用,如果该表中没有定义主键,也没有定义【非空唯一】列,则还会生成一个隐藏列【row_id】,这个我们之间也讲过,是为了生成聚簇索引使用的。

事务id是一个自增的全局变量,如果一个【事务】对任意表做了【增删改】的操作,那么innodb就会给他分配一个独一无二的事务id。

tip:

  • 事务id保存在一个全局变量【MAX_TRX_ID】上,每次事务需要分配事务id,就会从这个全局变量中获取,然后自增1。
  • 该变量每次自增到256的倍数会进行一个落盘(保存在表空间页号为5的页面中),发生服务停止或者系统崩溃后,再起启动服务,会读取这个数字,然后再加256。这样做既保证不会有太多I/O操作,还能保证id的有序增长。比如:读到256进行落盘,后来有涨到302,突然崩溃了,下次启动后,第一个事务的id就是256+256=512,保证新的事务id一定大。

3、roll_pointer

undo log在记录日志时是这样记录的,每次修改数据,都会将修改的数据标记一个【新的版本】,同时,这个版本的数据的地址会保存在修改之前的数据的roll_pointer列中,如下:

Mysql日志系统-InnoDB引擎层

4、分类

当我们对数据库的数据进行一个操作时必须记录之前的信息,将来才能【悔棋】,如下:

  • 插入一条数据时,至少要把这条数据的主键记录下来,以后不想要了直接根据主键删除。
  • 删除一条数据时,至少要把这个数据所有的内容全部记录下来,以后才能全量恢复。但事实上不需要,每行数据都有一个delete_flag,事务中将其置1,记录id,如需要回滚根据id复原即可,提交事务后又purge线程处理垃圾。
  • 修改一条数据时,至少要将修改前后的数据都保存下来。

innodb将undo log分为两类:

  1. 一类日志只记录插入类型的操作(insert)
  2. 一类日志只记录修改类型的操作(delete,update)

什么分为这两类呢?

  • 插入型的记录不需要记录版本,事务提交以后这一片空间就可以重复利用了。
  • 修改型的必须将每次修改作为一个版本记录下来,即使当前事务已经提交,也不一定能回收空间,应为其他事务可能在用。

5、物理存储结构

undo同样是以页的形式进行存储的,多个页是使用链表的形式进行管理,针对【普通表和零时表】,【插入型和修改型】的数据,一个事务可能会产生以下四种链表:

Mysql日志系统-InnoDB引擎层

这是物理存储模型,分成四种类型,是为了更好的管理。

6、记录流程

  1. 开启事务,执行【增删改】时获得【事务id】。

  2. 在系统表空间中第5号页中,分配一个回滚段,回滚段是轮动分配的,比如,当前事务使用第5个回滚段,下个事务就使用第6个。

    【回滚段】是一个【数据页】,里边划分了1024个undo slot,用来存储日志链表的头节点地址。

  3. 在当前回滚段的cached链表(回收可复用的)和空闲solt中,找到一个可用的slot,找不到就报错。

  4. 创建或复用一个undo log页,作为first undo page,并把他的地址写入undo solt中。

Mysql日志系统-InnoDB引擎层

7、回滚过程

  1. 服务再次启动时,通过表空间5号页面定位到128个回滚段的位置,
  2. 遍历所有的slot,找到所有状态不为空闲的slot,并且通过undolog的标记为找到现在活跃(未提交)的所有的事务id
  3. 根据undo log的记录,将数据全部回滚