数据库的ACID:A原子性,C一致性,I隔离性,D持久性;
redolog:保证 持久性;
redolog: 系统奔溃重启时需要按照上述内容所记录的步骤重新更新数据页,特点:
1、redo 日志占用空间非常小:存储表空间ID、页号、偏移量以及需要更新的值所需的存储空间。
2、redo 日志顺序写入磁盘:执行事务时,产生若干条 redo 日志,按顺序写入磁盘,顺序IO。
type:该条 redo 日志的类型。
space ID:表空间ID。
page number:页号。
data:该条 redo 日志的具体内容。
悲观插入:产生页分裂,一条数据插入会产生多条redlog,最后一条必须以一个特殊的redolog结尾MLOG_MULTI_REC_END,type 字 段对应的十进制数字为 31。
把对底层页面中的一次原子访问的过程称之为一个 Mini-Transaction ,简称 mtr。比如:向某个索引对应的 B+ 树中插入一条记录的过程 也算是一个 Mini-Transaction 。一个所谓的 mtr 可以包含一组 redo 日志,在进 行奔溃恢复时这一组 redo 日志作为一个不可分割的整体。
通过 mtr 生成的 redo 日志都放在了大小为 512字节 的页中。为了和我们前边提到的表空间中的页做区别,我们这里把用来存储 redo 日志的页称为 block。
log block header 的几个属性的意思分别如下:
1、LOG_BLOCK_HDR_NO:每一个block都有一个大于0的唯一标号,本属性就表示该标号值。
2、LOG_BLOCK_HDR_DATA_LEN :表示block中已经使用了多少字节,初始值为 12 (因为 log block body 从第12个字节处开始)。随着往block中写入的redo日志越来也多,本属性值也跟着增长。如果 log block body已经被全部写满,那么本属性的值被设置为 512 。
3、LOG_BLOCK_FIRST_REC_GROUP :一条 redo 日志也可以称之为一条 redo 日志记录( redo log record ),一个 mtr 会生产多条 redo 日志记录,这些 redo 日志记录被称之为一个 redo 日志记录组( redo logrecord group )。 LOG_BLOCK_FIRST_REC_GROUP 就代表该block中第一个 mtr 生成的 redo 日志记录组的偏移量(其实也就是这个block里第一个 mtr 生成的第一条 redo 日志的偏移量)。
4、LOG_BLOCK_CHECKPOINT_NO :表示所谓的 checkpoint 的序号.
log block trailer 中属性的意思如下:
LOG_BLOCK_CHECKSUM :表示block的校验值,用于正确性校验,我们暂时不关心它。
写入 redo 日 志时也不能直接直接写到磁盘上,实际上在服务器启动时就向操作系统申请了一大片称之为 redo log buffer 的 连续内存空间,参数innodb_log_buffer_size(默认16M)。结构如下:
log buffer 中写入 redo 日志的过程是顺序的,需要知道下一个往哪个block(页?)中写。
buf_free:该变量指明后 续写入的 redo 日志应该写入到 log buffer 中的哪个位置:
一个 mtr 执行过程中可能产生若干条 redo 日志,这些 redo 日志是一个不可分割的组,每个 mtr 运行过程中产生的日志先暂时存到 一个地方,当该 mtr 结束的时候,将过程中产生的一组 redo 日志再全部复制到 log buffer 中。我们现在假设 有两个名为 T1 、 T2 的事务,每个事务都包含2个 mtr ,我们给这几个 mtr 命名一下:
事务 T1 的两个 mtr 分别称为 mtr_T1_1 和 mtr_T1_2 。
事务 T2 的两个 mtr 分别称为 mtr_T2_1 和 mtr_T2_2 。
每个 mtr 都会产生一组 redo 日志,用示意图来描述一下这些 mtr 产生的日志情况:
不同的事务可能是并发执行的,所以 T1 、 T2 之间的 mtr 可能是交替执行。每当一个 mtr 执行完成时,伴随该 mtr 生成的一组 redo 日志就需要被复制到 log buffer 中,也就是说不同事务的 mtr 可能是交替写入 log buffer 的,我们画个示意图:
不同的 mtr 产生的一组 redo 日志占用的存储空间可能不一样,有的 mtr 产生的 redo 日志量很少,比如 mtr_t1_1 、 mtr_t2_1 就被放到同一个block中存储,有的 mtr 产生的 redo 日志量非常 大,比如 mtr_t1_2 产生的 redo 日志甚至占用了3个block来存储。
1、log buffer 空间不足时:log buffer 的 redo 日志量已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。(通过系统变量 innodb_log_buffer_size 指定)
2、事务提交时:保证持久性,必须要把修改这些页面对应的 redo 日志刷新到 磁盘。
3、后台线程不停的刷:大约每秒都会刷新一次 log buffer 中的 redo 日志到磁盘。
4、正常关闭服务器时和 checkpoint 时
log buffer默认刷新到ib_logfile0和ib_logfile1两个文件中,
innodb_log_group_home_dir:ib_logfile0和ib_logfile1文件目录。
innodb_log_file_size:两个文件大小,默认值为 48MB 。
innodb_log_files_in_group:redo 日志文件的个数,默认值为2,最大值为100。
写入日志组过程
在将 redo 日志写入 日志文件组时,从 ib_logfile0 开始写,如果 ib_logfile0 写满了,就接着 ib_logfile1 写,ib_logfile1 写满了就去 写 ib_logfile2 ,依此类推。如果写到最后一个文件该咋办?那就重新转到 ib_logfile0 继续写;
5.3 redo log文件格式
****log buffer中的redo日志刷新到磁盘的本质,就是把block的镜像写入日志文件中 ****,redo 日志文件其实也是由若干 个 512 字节大小的block组成:
前2048个字节,也就是前4个block是用来存储一些管理信息的。
从第2048字节往后是用来存储 log buffer 中的block镜像的。
lsn: 记录已经写入的 redo 日志量(日志序列号),初始的 lsn 值为 8704;数据写入示例如下:
系统第一次启动后初始化 log buffer 时, buf_free (就是标记下一条 redo 日志应该写入到 log buffer 的位置的变量)就会指向第一个 block 的偏移量为12字节( log block header 的大小)的地方,那么 lsn 值也会跟着增加12:
如果某个 mtr 产生的一组 redo 日志占用的存储空间比较小,待插入的block剩余空闲空间能容纳这 个 mtr 提交的日志时, lsn 增长的量就是该 mtr 生成的 redo 日志占用的字节数,就像这样:
假设上图中 mtr_1 产生的 redo 日志量为200字节,lsn 就要在 8716 的基础上增加 200 ,为 8916
如果某个 mtr 产生的一组 redo 日志占用的存储空间比较大,也就是待插入的block剩余空闲空间不足以容纳 这个 mtr 提交的日志时, lsn 增长的量就是该 mtr 生成的 redo 日志占用的字节数加上额外占用的 log block header 和 log block trailer 的字节数,就像这样:
我们假设上图中 mtr_2 产生的 redo 日志量为1000字节,为了将 mtr_2 产生的 redo 日志写入 log buffer ,我们不得不额外多分配两个block,所以 lsn 的值需要在 8916 的基础上增加 1000 + 12×2 + 4 × 2 = 1032
buf_next_to_write:记录log buffer中哪些被刷盘(刷盘之前先写到log buffer)。
初始时的 LSN 值是 8704 ,对应文件偏移量 2048 ,之后每个 mtr 向磁盘中写入多少字节日志, lsn 的值就增长 多少。
1、可能会产生一组不可分割的 redo 日志,在mtr结束时,会把这一组 redo 日志写入到 log buffer
2、把在mtr执行过程中可能修改过的页面加入到Buffer Pool的flush链表
oldest_modification :如果某个页面被加载到 Buffer Pool 后进行第一次修改,那么就将修改该页面的 mtr 开始时对应的 lsn 值写入这个属性。
newest_modification :每修改一次页面,都会将修改该页面的 mtr 结束时对应的 lsn 值写入这个属性。 也就是说该属性表示页面最近一次修改后对应的系统 lsn 值。
页面写入过程示意图:
flush链表中的脏页按修改发生的时间顺序进行排序,按照 oldest_modification代表的LSN值进行排序,被多次更新的页面不会重复插入到flush链表中,但会更新 newest_modification属性值。
判断某些redo log占用的磁盘空间是否可以覆盖的依据就是它对应的脏页是否已经刷新到磁盘里。
flush链表,log buffer,log file 日志写入示意图:
如图,虽然 mtr_1 和 mtr_2 生成的 redo 日志都已经被写到了磁盘上,但是它们修改的脏页仍然留在 Buffer Pool 中,所以它们生成的 redo 日志在磁盘上的空间是不可以被覆盖的。之后随着系统的运行,如果 页a 被刷新 到了磁盘,那么它对应的控制块就会从 flush链表 中移除,就像这样子:
代表当前系统中可以被覆盖的 redo 日志总量是多少,这个变量初始值也是 8704。页a 被刷新到磁盘, mtr_1 生成的 redo 日志就可以被覆盖,所以我们可以进行一个增加 checkpoint_lsn 的操作,我们把这个过程称之为做一次 checkpoint。步骤如下:
1、计算一下当前系统中可以被覆盖的 redo 日志对应的 lsn 值最大是多少。
redo 日志可以被覆盖,意味着它对应的脏页被刷到了磁盘,只要我们计算出当前系统中被最早修改的脏页 对应的 oldest_modification 值,那凡是在系统lsn值小于该节点的oldest_modification值时产生的redo日志 都是可以被覆盖掉的,我们就把该脏页的 oldest_modification 赋值给 checkpoint_lsn 。 比方说当前系统中 页a 已经被刷新到磁盘,那么 flush链表 的尾节点就是 页c ,该节点就是当前系统中最 早修改的脏页了,它的 oldest_modification 值为8916,我们就把8916赋值给 checkpoint_lsn (也就是说 在redo日志对应的lsn值小于8916时就可以被覆盖掉)。
2、checkpoint_lsn 和对应的 redo 日志文件组偏移量以及此次 checkpint 的编号写到日志文件的 管理信息(就是 checkpoint1 或者 checkpoint2 )中。
0: 事务提交时,每次都会写入logbuffer ,但是只会定时(每秒)写入(调用fsync)osbuffer ,在写入到磁盘文件,可能会丢失1 秒内的数据。
1: mysql默认规则,事务提交时每次都写入 logbuffer、osbuffer、刷新到磁盘。数据完整性能低。
2: 事务提交时每次都写入osbuffer,但是每秒执行一次写入磁盘操作。
对于 checkpoint_lsn 之后的 redo 日志,它 们对应的脏页可能没被刷盘,也可能被刷盘了,我们不能确定,所以需要从 checkpoint_lsn 开始读取 redo 日志 来恢复页面。要把 checkpoint1 和 checkpoint2 这两个block中的 checkpoint_no 值读出来比一下大小,哪个的 checkpoint_no 值更大,说明哪个block存储的就是最近的一次 checkpoint 信息。这样我们就能拿到最近发生 的 checkpoint 对应的 checkpoint_lsn 值以及它在 redo 日志文件组中的偏移量 checkpoint_offset
根据block扫描到最后一个block(没写满的block)
根据 redo 日志的 space ID 和 page number 属性计算出散列值,把 space ID 和 page number 相同的 redo 日志放到哈希表的同一个槽里,如果有多个 space ID 和 page number 都相同的 redo 日志,那么它们之间 使用链表连接起来,按照生成的先后顺序链接起来的
同一个页面进行修改的 redo 日志都放在了一个槽里,所以可以一次性将一 个页面修复好(避免了很多读取页面的随机IO),这样可以加快恢复速度。另外需要注意一点的是,同一个 页面的 redo 日志是按照生成时间顺序进行排序的,所以恢复的时候也是按照这个顺序进行恢复,如果不按 照生成时间顺序进行排序的话,那么可能出现错误。比如原先的修改操作是先插入一条记录,再删除该条记 录,如果恢复时不按照这个顺序来,就可能变成先删除一条记录,再插入一条记录,这显然是错误的。