为什么需要redo 日志(redo log)
redo log又称为 重做日志 ,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性。
程序实现的增删改查操作在真正访问页面之前,需要把在磁盘上的页缓存到内存中的 Buffer Pool 之后才可以访问。但这只是内存级别的,如果我们只在内存的 Buffer Pool 中修改了页面,假设在事务提交后突然发生了某个故障,导致内存中的数据都失效了,那么这个已经提交了的事务对数据库中所做的更改也就跟着丢失了,这是我们所不能忍受的,也不符合事务应该满足持久性的特性。
????Buffer Pool的东西我好像没记???后面补。。。
我们要求的是对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失。
为了保证这个持久性,有一个很简单的做法就是在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,但是这个做法有些问题:
1、刷新一个完整的数据页太浪费了
有时候我们仅仅修改了某个页面中的一个字节,但是我们知道在 InnoDB 中是以页为单位来进行磁盘IO的,
也就是说我们在该事务提交时不得不将一个完整的页面从内存中刷新到磁盘,
我们又知道一个页面默认是16KB大小,只修改一个字节就要刷新16KB的数据到磁盘上显然是太浪费了。
2、随机IO刷起来比较慢
一个事务可能包含很多语句,即使是一条语句也可能修改许多页面,若是该事务修改的这些页面并不相邻,
这就意味着在将某个事务修改的 Buffer Pool 中的页面刷新到磁盘时,需要进行很多的随机IO,
随机IO比顺序IO要慢,尤其对于传统的机械硬盘来说。
就是说:刷整页的代价好大,可能只是改一点数据就刷一次,又慢,很不理想。能不能不刷那么多?我们想的是对数据的操作在宕机的情况下能恢复。
频繁的IO不好,那就减少IO,但对数据的操作呢?噢!我们可以建一个待办(redo log),
里面记下来哪些数据有做什么操作。比如第几页的某一列数据改了什么值等等,
这样还用改一下就刷下盘嘛?不用啦,挑个时间将待办里的事项做了就是,减少了频繁的IO。
宕机了咋办,恢复后照着待办里记录的做不就行了?
这时候就有个问题了:不都是要写磁盘?在写不及时的时候内存的数据不都会丢失?
首先虽然都是写磁盘,但是redo log记录的东西少呀,你改一个字节你刷整页的话就16kb了,
我只记录了一句话呀,其次呢是redo log的写入是按顺序来的,就像一页纸,你从上往下写就是了,
但你刷盘你还得去找在哪一页,这就是随机IO了。
所以说,redo log的好处有以下两点
redo 日志占用的空间非常小
记录一条条日志总体来看怎么也好过改一条记录就是一个页吧?
redo 日志是顺序写入磁盘的
在执行事务的过程中,每执行一条语句,就可能产生若干条 redo 日志,这些日志是按照产生的顺序写入磁
盘的,也就是使用顺序IO。
redo日志格式
redo 日志本质上只是记录了一下事务对数据库做了哪些修改。 InnoDB 针对事务对数据库的不同修改场景定义了多种类型的 redo 日志,但是绝大部分类型的 redo 日志都有下边这种通用的结构:
字段 | 释义 |
---|---|
type | 该条 redo 日志的类型 |
space ID | 表空间ID |
page number | 页号 |
data | 该条 redo 日志的具体内容 |
redo日志类型
略了,TM的,都不知道怎么写,照抄吗?
Mini-Transaction
个人翻译—小事务?
以组的形式写入redo日志
所有的增删改对这些页面的操作都发生在 Buffer Pool 中,所以在修改完页面之后,需要记录一下相应的 redo 日志。在执行语句的过程中产生的 redo 日志InnoDB划分成了若干个不可分割的组,如:
更新修改表空间属性产生的redo日志是不可分割的
向聚簇索引对应 B+ 树的页面中插入一条记录时产生的 redo 日志是不可分割的。
向某个二级索引对应 B+ 树的页面中插入一条记录时产生的 redo 日志是不可分割的。
等等……
所谓不可分割的意思:就按照它的名字来理解好了,小事务也是事务,事务嘛,就是要么都成功,要么都失败,不可分割就是作为一个整体嘛(原子性)。
eg:向某个索引对应的 B+ 树插入一条记录为例,在向 B+ 树中插入这条记录之前,需要先定位到这条记录应该被插入到哪个叶子节点代表的数据页中,定位到具体的数据页之后,有两种可能的情况:
1、乐观插入(就是该数据页的剩余的空闲空间充足,足够容纳这一条待插入记录,所以直接插入并记录日志好了)
2、悲观插入(数据页剩余的空闲空间不足,要进行页分裂操作,涉及到原页面的复制迁移。这个过程要对多个页面进行修改,也就意味着会产生多条 redo 日志)
以上的操作必须是一起完成的,比方说在悲观插入过程中,新的页面已经分配好了,数据也复制过去了,新的记录也插入到页面中了,可是没有向内节点中插入一条 目录项记录 ,这个插入过程就是不完整的,这样会形成一棵不正确的 B+ 树。这是不允许的。
不可分割的实现
执行这些需要保证原子性的操作时必须以 组 的形式来记录的 redo 日志,在进行系统崩溃重启恢复时,针对某个组中的 redo 日志,怎么才能要么全部的日志都恢复掉,要么一条也不恢复呢?
1、多条日志一组
在该组中的最后一条 redo 日志后边加上一条特殊类型的 redo 日志(MLOG_MULTI_REC_END), 该类型的 redo 日志结构很简单,只有一个 type 字段:
某个需要保证原子性的操作产生的一系列 redo 日志必须要以一个类型为 MLOG_MULTI_REC_END 结尾:
在系统崩溃重启进行恢复时,只有当解析到类型为 MLOG_MULTI_REC_END 的 redo 日志,才认为解析到了
一组完整的 redo 日志,才会进行恢复。否则的话直接放弃前边解析到的 redo 日志。
2、单条日志
在redo日志的结构中有个type字段,将里面一个比特位用来表示该需要保证原子性的操作只产生单一的一条
redo 日志。
如果 type 字段的第一个比特位为 1 ,代表该需要保证原子性的操作只产生了单一的一条 redo 日志,否则
表示该需要保证原子性的操作产生了一系列的 redo 日志。
Mini-Transaction的概念
MySQL 把对底层页面中的一次原子访问的过程称之为一个 Mini-Transaction ,简称 mtr ,一个所谓的 mtr 可以包含一组 redo 日志,在进行崩溃恢复时这一组 redo 日志作为一个不可分割的整体。
一个事务可以包含若干条语句,每一条语句其实是由若干个 mtr 组成,每一个 mtr 又可以包含若干条 redo 日志,其关系如下:
可能会不理解为啥一条语句能有多个mtr,在之前就说到“更新修改表空间属性产生的redo日志是不可分割的”,某些语句可能会涉及到修改其他表的数据页和系统表空间,两者是分开的,所以会有多个mtr
redo日志的写入过程
redo log block
为了更好的进行系统奔溃恢复,InnoDB把通过 mtr 生成的 redo 日志都放在了大小为 512字节的 页 中。一个 redo log block 的示意图如下:
真正的 redo 日志都是存储到占用 496 字节大小的 log block body 中,图中的 log block header 和 log block
trailer 存储的是一些管理信息
log block header 的几个属性的意思分别如下:
LOG_BLOCK_HDR_NO :每一个block都有一个大于0的唯一标号,本属性就表示该标号值。
LOG_BLOCK_HDR_DATA_LEN :表示block中已经使用了多少字节,初始值为 12 (因为 log block body 从第
12个字节处开始)。随着往block中写入的redo日志越来也多,本属性值也跟着增长。如果 log block body
已经被全部写满,那么本属性的值被设置为 512 。
LOG_BLOCK_FIRST_REC_GROUP :一条 redo 日志也可以称之为一条 redo 日志记录( redo log record ),
一个 mtr 会生产多条 redo 日志记录,这些 redo 日志记录被称之为一个 redo 日志记录组( redo log
record group )。 LOG_BLOCK_FIRST_REC_GROUP 就代表该block中第一个 mtr 生成的 redo 日志记录组的偏
移量(其实也就是这个block里第一个 mtr 生成的第一条 redo 日志的偏移量)。
LOG_BLOCK_CHECKPOINT_NO :表示所谓的 checkpoint 的序号
log block trailer 中属性的意思如下:
LOG_BLOCK_CHECKSUM :表示block的校验值,用于正确性校验
redo日志缓冲区
InnoDB为了解决磁盘速度过慢的问题而引入了 Buffer Pool 。同理,写入 redo 日志时也不能直接直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为 redo log buffer 的连续内存空间,翻译成中文就是 redo日志缓冲区 ,我们也可以简称为 log buffer 。这片内存空间被划分成若干个连续的 redo log block
redo日志写入log buffer
向 log buffer 中写入 redo 日志的过程是顺序的,也就是先往前边的block中写,当该block的空闲空间用完之后
再往下一个block中写。当我们想往 log buffer 中写入 redo 日志时,第一个遇到的问题就是应该写在哪个block 的哪个偏移量处,所以InnoDB特意提供了一个称之为 buf_free 的全局变量,该变量指明后续写入的 redo 日志应该写入到 log buffer 中的哪个位置,如图所示:
一个 mtr 执行过程中可能产生若干条 redo 日志,这些 redo 日志是一个不可分割的组,所以其实
并不是每生成一条 redo 日志,就将其插入到 log buffer 中,而是每个 mtr 运行过程中产生的日志先暂时存到 一个地方,当该 mtr 结束的时候,将过程中产生的一组 redo 日志再全部复制到 log buffer 中
eg:加入有两个事务并发写入日志,最后的效果:
不同的 mtr 产生的一组 redo 日志占用的存储空间可能不一样,有的 mtr 产生的
redo 日志量很少,比如 mtr_t1_1 、 mtr_t2_1 就被放到同一个block中存储,有的 mtr 产生的 redo 日志量非常大,比如 mtr_t1_2 产生的 redo 日志甚至占用了3个block来存储。
redo日志文件
redo日志刷盘时机
mtr 运行过程中产生的一组 redo 日志在 mtr 结束时会被复制到 log buffer 中,但这些日志只是处在内存里的,所以需要在一些情况下将其刷新到磁盘里:
1、log buffer 空间不足时
InnoDB认为如果当前写入 log buffer 的redo 日志量已经占满了 log buffer 总容量的大约一半左右,
就需要把这些日志刷新到磁盘上
2、事务提交时
在事务提交时可以不把修改过的 Buffer Pool 页面刷新到磁盘,但是为了保证持久性,
必须要把修改这些页面对应的 redo 日志刷新到磁盘。这里会有个参数可以调节刷盘的方式;
innodb_flush_log_at_trx_commit
这部分后面再说
3、后台线程不停的刷刷刷
后台有一个线程,大约每秒都会刷新一次 log buffer 中的 redo 日志到磁盘。
4、正常关闭服务器时
5、做所谓的 checkpoint 时
redo日志文件组
MySQL 的数据目录下默认有两个名为 ib_logfile0 和ib_logfile1 的文件,这是redo日志的物理文件,即磁盘上的。
redo文件在Windows下就没那么好找了,使用以下命令只能找到一个目录(我的目录下没有……)
使用 SHOW VARIABLES LIKE 'datadir' 查看
通过如下命令来查看 redo log 文件相关的配置
show variables like 'innodb_log%'
innodb_log_group_home_dir :指定 redo log 文件组所在的路径,默认值为 ./ ,表示在数据库的数据目录下。
innodb_log_files_in_group:指明redo log file的个数,命名方式如:ib_logfile0,iblogfile1...iblogfilen。
默认2个,最大100个。
innodb_flush_log_at_trx_commit:控制 redo log 刷新到磁盘的策略,默认为1。
innodb_log_file_size:单个 redo log 文件设置大小,默认值为 48M 。
总共的 redo 日志文件大小其实就是: innodb_log_file_size × innodb_log_files_in_group 。
……
磁盘上的 redo 日志文件不只一个,而是以一个 日志文件组 的形式出现的。将 redo 日志写入 日志文件组 时,是从 ib_logfile0 开始写,如果 ib_logfile0 写满了,就接着 ib_logfile1 写……如果写满了,就进行覆盖写,所以这几个文件看起来的效果是这样的:
redo日志文件格式
log buffer 本质上是一片连续的内存空间,被划分成了若干个 512 字节大小的 block 。将log buffer中的redo日志刷新到磁盘的本质就是把block的镜像写入日志文件中,所以 redo 日志文件其实也是由若干个 512 字节大小的block组成。
redo 日志文件组中的每个文件大小都一样,格式也一样,都是由两部分组成:
前2048个字节,也就是前4个block是用来存储一些管理信息的。
从第2048字节往后是用来存储 log buffer 中的block镜像的。
所以我们前边所说的 循环 使用redo日志文件,其实是从每个日志文件的第2048个字节开始算,画个示意图就是
这样:
Log Sequeue Number
简称lsn。这个其实就是log buffer中的一个偏移量,标识log buffer用了多少了。与buf_free 不同的是,虽然两者的位置都是相同的,但buf_free是标记下一条 redo 日志应该写入到 log buffer的位置的变量。一个是数值,一个是地址。
以上面的日志文件图为例,lsn的数值是要加上蓝色的空间的,但buf_free只是在橙色处标识一下,日志已经存到这里了
flushed_to_disk_lsn
redo 日志是首先写到 log buffer 中,之后才会被刷新到磁盘上的 redo 日志文件。所以InnoDB提出了一个称之为 buf_next_to_write 的全局变量,标记当前 log buffer 中已经有哪些日志被刷新到磁盘中了。
画个图表示就是这样:
当有新的 redo 日志写入到 log buffer 时,首先 lsn 的值会增长,但 flushed_to_disk_lsn 不变,
随后随着不断有 log buffer 中的日志被刷新到磁盘上, flushed_to_disk_lsn 的值也跟着增长。如果两者的值
相同时,说明log buffer中的所有redo日志都已经刷新到磁盘中了。
刷盘前
刷盘后:
应用程序向磁盘写入文件时其实是先写到操作系统的缓冲区中去,如果某个写入操作要等到操作系统确
认已经写到磁盘时才返回,那需要调用一下操作系统提供的fsync函数。其实只有当系统执行了fsync函
数后,flushed_to_disk_lsn的值才会跟着增长,当仅仅把log buffer中的日志写入到操作系统缓冲区
却没有显式的刷新到磁盘时,另外的一个称之为write_lsn的值跟着增长。为了理解方便,这里将flushed_to_disk_lsn和write_lsn的概念混淆了起来。
flush链表中的LSN
flush链表是啥?后面文章再补一下
一个 mtr 代表一次对底层页面的原子访问,在mtr 结束时,会把这一组 redo 日志写入到 log buffer 中。除此之外,在 mtr 结束时还有一件非常重要的事情要做,就是把在mtr执行过程中可能修改过的页面加入到Buffer Pool的flush链表。
这时候哪怕你的redo日志已经写完了,但对应的数据没有被更新到磁盘,系统总要找时间把数据写入磁盘,这个写入过程的术语就是 flush。
当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。
盗图了我,这里来源于博客:https://blog.csdn.net/j1231230/article/details/107160553
flush链表
当第一次修改某个缓存在 Buffer Pool 中的页面时,就会把这个页面对应的控制块插入到 flush链表 的头部,之 后再修改该页面时由于它已经在 flush 链表中了,就不再次插入了。也就是说flush链表中的脏页是按照页面的第一次修改时间从大到小进行排序的
其实这里只要知道脏页会放到flush链表中就可以了。至于链表中控制块的属性什么的会在页面多次修改进行更新这些的可以不用管,不影响理解
checkpoint
redo日志只是为了系统崩溃后恢复脏页用的,如果对应的脏页已经刷新到了磁盘,也就是说即使现在系统崩溃,那么在重启后也用不着使用redo日志恢复该页面了,所以该redo日志也就没有存在的必要了,那么它占用的磁盘空间就可以被后续的redo日志所重用。也就是说:判断某些redo日志占用的磁盘空间是否可以覆盖的依据就是它对应的脏页是否已经刷新到磁盘里。
其实就是一个指针,指向磁盘上的redo日志某一个点,
标识这个点后面的数据都是已经刷好盘了的(即脏页刷进磁盘),可以被覆盖
innodb_flush_log_at_trx_commit的用法
为了保证事务的 持久性 ,用户线程在事务提交时需要将该事务执行过程中产生的所有 redo 日志都刷新到磁盘上。此操作会很明显的降低数据库性能。所以该变量有3个可选值:
0 – 每N秒将Redo Log Buffer的记录写入Redo Log文件,并且将文件刷入硬件存储1次。
N由innodb_flush_log_at_timeout控制。
1 – 每个事务提交时,将记录从Redo Log Buffer写入Redo Log文件,并且将文件刷入硬件存储。
2 – 每个事务提交时,仅将记录从Redo Log Buffer写入Redo Log文件。
Redo Log何时刷入硬件存储由操作系统和innodb_flush_log_at_timeout决定。
这个选项可以保证在MySQL宕机,而操作系统正常工作时,数据的完整性。
崩溃恢复
这部分不摘抄了,因为前面少了点东西,这里记起来会有断层。只要知道从最新的一个checkpoint点开始,但最后一个日志页就行。
怎么知道哪一个是最后一个日志页?毕竟是循环使用的,可能都是有数据的?
普通block的 log block header 部分有一个称之为 LOG_BLOCK_HDR_DATA_LEN 的属性,该属性值记录了当前block
里使用了多少字节的空间。对于被填满的block来说,该值永远为 512 。如果该属性的值不为 512 ,那么就是它
了,它就是此次奔溃恢复中需要扫描的最后一个block。
怎么恢复
可以按照 redo 日志的顺序依次扫描checkpoint_lsn 之后的各条redo日志,按照日志中记载的内容将对应的页面恢复出来。
InnoDB对此的优化
1、使用哈希表
这个就是将日志中是同一个页的日志放在一起,这样就不用多次io取页面了,一起处理
2、跳过已经刷新到磁盘的页面
这里说的是因为后台有线程会一直刷盘的,可能刷盘后还没来的及更新checkpoint点,那么就会有
一些已经更新的页面,这里可以读取要更新页面的一个称之为 File Header 的部分,
在 File Header 里有一个称之为FIL_PAGE_LSN 的属性,这个属性如果比我们的checkpoint大,那就说明已经
更新了,可以跳过