接下来的两个日志,是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:日志数据
在innodb执行任务时,有很多操作,必须具有原子性,我们把这一类操作称之为MIni Transaction。
我们以下边的例子为例:
在我们向B+树中插入一条记录的时候,需要定位这条数据将要插入的【数据页】,因为插入的位置不同,可能会有以下情况:
1、待插入的页拥有【充足的剩余空间】,足以容纳这条数据,那就直接插入就好了,这种情况需要记录一条【MLOG_COMP_REC_INSERT类型】的redo日志就好了,这种情况成为乐观插入。
2、待插入的页【剩余空间不足】以容纳该条记录,这样就比较麻烦了,必须进行【页分裂】了。必须新建一个页面,将原始页面的数据拷贝一部分到新页面,然后插入数据。这其中对应了好几个操作,必须记录多条rede log,包括申请新的数据页、修改段、区的信息、修改各种链表信息等操作,需要记录的redo log可能就有二三十条,但是本次操作必须是一个【原子性操作】,在记录的过程中,要全部记录,要么全部失败,这种情况就被称之为一个MIni Transaction(最小事务)。
(1)MTR的按组写入
对于一个【MTR】操作必须是原子的,为了保证原子性,innodb使用了组的形式来记录redo 日志,在恢复时,要么这一组的的日志全部恢复,要么一条也不恢复。innodb使用一条类型为MLO_MULTI_REC_END
类型的redo log作为组的结尾标志,在系统崩溃恢复时只有解析到该项日志,才认为解析到了一组完整的redo log,否则直接放弃前边解析的日志。
(2)单条redolog的标识方法
有些操作只会产生一条redo log,innodb是通过【类型标识】的第一个字符来判断,这个日志是单一日志还是组日志,如下图:
(3)事务、sql、MTR、redolog的关系如下
任何可能产生大量I/O的操作,一般情况下都会设计【缓冲层】,mysql启动时也会向操作系统申请一片空间作为redo log的【缓冲区】,innodb使用一个变量buf_free
来标记下一条redo log的插入位置(标记偏移量),log buffer会在合适的时机进行刷盘:
innodb_log_buffer_size
指定,当写入log buffer的日志大于容量的50%,就会进行刷盘。有缓冲就可能存在数据不一致,咱们接着往下看。
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链表是按照第一次修改的时间排序的。再第一次修改缓冲页时,会在【缓冲页对应的控制块】中,记录以下两个属性:
既然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的次数)全部记录在日志文件的管理信息内。
主线程
1、客户端访问mysql服务,在buffer pool中进行操作(如果目标页不在缓冲区,需要加载进入缓冲区),此时会形成脏页。
2、记录redo log,可能产生很多组日志,redo log优先记录在缓冲区,会在提交事务前刷盘。
3、刷盘时根据checkpoint的结果,选择可以使用的日志空间进行记录。
4、成功后即可返回,此时数据不会落盘,这个过程很多操作只在内存进行,只需要记录redo log(顺序写),所以速度很快。
线程1:
1、不断的对flush链表的脏页进行刷盘,对响应时间没有过高要求。
线程2:
1、不断的进行checkpoin操作,保证redo log可以及时写入。
(1)log buffer中的日志丢失,log buffer中的日志会在每次事务前进行刷盘,如果在事务进行中崩溃,事务本来就需要回滚。
(2)buffer pool中的脏页丢失,崩溃后可以通过redo log恢复,通过checkpoint操作,我们可以确保,内存中脏页对应的记录都会在redo log日志中存在。
redo log保证了崩溃后,数据不丢失,但是一个事务进行中,如果一部分redo log已经刷盘,那么系统会将本应回滚的数据同样恢复,为了解决回滚的问题,innodb提出了undo log。
undo log(也叫撤销日志或者回滚日志),他的主要作用是为了实现回滚操作。同时,他是MVCC多版本控制的核心模块。undo log保存在共享表空间【ibdata1文件】中。
注意:8.0以后undolog有了独立的表空间:
在讲undo log之前需要先了解行数据中的两个隐藏列:
我们已经讲过,在innodb的行数据中,会自动添加两个隐藏列,一个是【trx_id】,一个是【roll_pointer】,本章节会详细介绍这两列的具体作用,如果该表中没有定义主键,也没有定义【非空唯一】列,则还会生成一个隐藏列【row_id】,这个我们之间也讲过,是为了生成聚簇索引使用的。
事务id是一个自增的全局变量,如果一个【事务】对任意表做了【增删改】的操作,那么innodb就会给他分配一个独一无二的事务id。
tip:
undo log在记录日志时是这样记录的,每次修改数据,都会将修改的数据标记一个【新的版本】,同时,这个版本的数据的地址会保存在修改之前的数据的roll_pointer列中,如下:
当我们对数据库的数据进行一个操作时必须记录之前的信息,将来才能【悔棋】,如下:
innodb将undo log分为两类:
什么分为这两类呢?
undo同样是以页的形式进行存储的,多个页是使用链表的形式进行管理,针对【普通表和零时表】,【插入型和修改型】的数据,一个事务可能会产生以下四种链表:
这是物理存储模型,分成四种类型,是为了更好的管理。
开启事务,执行【增删改】时获得【事务id】。
在系统表空间中第5号页中,分配一个回滚段,回滚段是轮动分配的,比如,当前事务使用第5个回滚段,下个事务就使用第6个。
【回滚段】是一个【数据页】,里边划分了1024个undo slot,用来存储日志链表的头节点地址。
在当前回滚段的cached链表(回收可复用的)和空闲solt中,找到一个可用的slot,找不到就报错。
创建或复用一个undo log页,作为first undo page,并把他的地址写入undo solt中。