【聊聊MySQL】八.MySQL-InnoDB的数据库事务的执行之REDO_LOG
一.REDO LOG
数据库最主要的特性是 持久性
,即修改了数据库数据以后,无论发生什么事情,数据都不会丢失。MySQL-InnDB
就利用 REDO LOG
来满足持久性。REDO LOG
中文称为 重做日志
,那也可以简洁一点叫做 REDO 日志
。是数据库启动的时候,用于读取计算 BufferPool
中脏页数据的方式,这样可以避免由于其他原因导致数据库掉线但是脏页未刷新到硬盘,而导致数据丢失的尴尬局面。
二.高效且持久化的方式
由于 REDO 日志
存在的意义就是,重启数据库的时候能够重新将数据恢复回来。所以在数据库没有发生重启或者异常宕机的时候,他是没有任何意义的,甚至还多了一些开销…
所以为了减少开销,REDO 日志
应当尽量轻巧,记录一些必要的信息就可以了,比如:某个区,某个页号,的某一行,修改成xx
。第二个减少开销的方式就是顺序写入硬盘,先发生的事务,REDO 日志
先被写入硬盘,后边的排队顺序追加到后面去。
三.日志格式
首先明白一点,REDO 日志
所携带的数据是 MySQL_InnoDB
在重启系统的时候,需要调用某些函数进行恢复时所需要的参数。不过,为了减少容量的开销,又做了一些事情。
3.1 装载数据的通用格式
一条重做日志,肯定会包含上面这些信息。
日志类型,在现在常用的 5.7
中,有 53
种类型,这个类型字段就很重要了,决定着程序恢复数据的方式。
MLOG_1BYTE
(type = 1
):表示在页面的某个偏移量写入1byte
的数据的REDO 日志
类型;MLOG_2BYTE
(type = 2
):写入2byte
的数据;MLOG_4BYTE
(type = 4
):写入4byte
的数据;MLOG_8BYTE
(type = 8
):写入8byte
的数据;MLOG_WRITE_STRING
(type = 30
):写入一串数据,具体长度会放在上图中黄色
的部分;
可以看到第 5
中类型在具体内容中带有一个 len
表示数据的长度,也有点类似于我们做 RPC
的时候为了粘包写的长度。那我们业务中每次更新都会更新很多东西,可以说基本都是使用第 5
种日志类型了。
而 偏移量在上面所有类型的日志中都有
。
3.2 插入数据时日志格式
OK,现在我们来了解下日志存储哪些东西。
我们日常插入,比如说 INSERT INTO student values(1, 'Weidan', 'BOY')
这条插入语句,看似只需要插入一条数据即可,但是还记得吗,之前说过的页分裂问题呀,还有调整页参数信息的问题:
如果还记得这些内容的话,跳过我引用的内容
页头信息主要包含以下几个部分:
名称 大小(字节数) 主要作用 FILE_PAGE_SPACE_OR_CHKSUM 4 4.0.14
之前存放在哪个表空间
后面的版本存储checksum
值FILE_PAGE_OFFSET 4 表空间中页的页号 FILE_PAGE_PREV 4 上一页 FILE_PAGE_NEXT 4 下一页 FILE_PAGE_LSN 8 最后修改的日志序号 Log Swquence Number
FILE_PAGE_TYPE 2 页的类型: 在后面给出
FILE_PAGE_FILE_FLUSH_LSN 8 代表文件被更新到该指定的LSN值 FILE_PAGE_ARCH_LOG_NO_OR_SPACE_ID 4 属于哪个表空间
如果说
FileHeader
是用来记录文件的信息,那PageHeader
就是用来记录当前数据页的状态信息了。同样也是一堆
Key
:
名称 大小(字节) 主要作用 PAGE_N_DIR_SLOTS 2 Page Directory 页目录中的槽数 PAGE_HEAP_TOP 2 堆中第一个记录的指针 PAGE_N_HEAP 2 堆中记录数 PAGE_FREE 2 空闲列表首指针 PAGE_GARBAGE 2 已删除的字节数 PAGE_LAST_INSERT 2 最后插入的位置 PAGE_DIRECTION 2 插入数据的方向 PAGE_N_DIRECTION 2 一个方向连续插入记录的数量 PAGE_N_RECS 2 该页记录数 PAGE_MAX_TRX_ID 8 当前页最大事务ID PAGE_LEVEL 2 索引树中的位置 0x00代表叶节点 PAGE_INDEX_ID 8 当前页属于哪个索引的id PAGE_BTR_SEG_LEAF 10 叶子节点中文件段首指针位置(B+的Root页中定义) PAGE_BTR_SEG_TOP 10 非叶子节点中文件段首指针位置(B+的Root页中定义)
Infimum
记录比当前页最小主键还要小的“值”,Supremum
记录比当前页最大值还要大的“值”。值我打了双引号,并不是说他就是一个值,而是一个规定:一个页中,Infimum 记录的下一条记录就是本页中最小的记录,而本页中最大的记录的下一条记录就是 Supremum 记录 。 (这里的下一条
next_record
的字段是Row Format
规定的头,后面讲行信息的头部消息的时候我会重新说一下这句的)
页分裂:
好了,所以说,插入一条数据的时候需要调整的东西特别多,而且这些地方的调整都需要生成 REDO 日志
。
但是这些需要修改的地方特别零散,他并不是聚集在一起的
上图黄色的地方就是被修改的数据。
那现在有两种解决方案,一种是修改多少个地方就记录多少条日志,这样就需要 8
条 REDO 日志
。
那如果说将第一个修改的地方,和最后一个修改的地方,所有数据都进行记录的话,像上面那张图,最后一个因为拉的很远,中间那些没有修改的数据就都需要放到 REDO 日志
上去,那么这样又感觉不优雅(讲真我以前我还真的这么干过用户编辑信息)。
两种方案,一个太多,一个太大。所以就诞生了其他的 REDO 日志
类型:
MLOG_COMP_REC_INSERT
(type = 38
):表示插入一条使用紧凑行格式的记录时的日志类型;MLOG_COMP_PAGE_CREATE
(type = 58
):表示创建一个存储紧凑行格式记录的页面的日志类型;MLOG_COMP_REC_DELETE
(type = 42
):表示删除一条紧凑型行格式的数据的日志类型;- ……
太多了,不过看第三个表示 REC
和 PAGE
分别表示对应的行还是页做操作,第四个就是增删查改了。
但是有个比较特殊,那就是 批量删除
了:
MLOG_COMP_LIST_START_DELETE
(type = 44
)和 MLOG_COMP_LIST_END_DELETE
(type = 43
)表示批量删除的时候,开始删除跟结束删除的两条范围记录。
3.2 REDO 日志内容
而一个 REDO 日志
包含了两个层面的东西:
而逻辑层面的东西,并不是说在重启的时候直接回复到数据文件中,而是需要经过一系列的计算,然后再得出最后结果写入数据文件,类似于 Java
写 方法
的时候需要用到的一些参数信息。而上一节中的类型,指的是参数的不同,InnoDB
恰好可以根据这些参数计算出来对应的 PAGE_N_DIR_SLOTS
PAGE_N_HEAP
等等信息,然后恢复到数据库中。
四.重做LOG的’事务’
重做LOG
的’事务’ 也成为 Mini-Transaction
,因为我们知道系统每次写一页是 4kb
,磁盘一次 IO
是 512 byte
。那如果 重做LOG
在写入硬盘的时候,写少了点东西,势必导致这部分的日志被破坏不可用了,所以 InnoDB
规定,某些修改数据页的 重做LOG
必须一起被写入硬盘,这部分的 重做LOG
才算真的有效。
我们上面已经知道,一条修改语句或者新增语句将会更多多个地方,就真的可能出现了多个 重做LOG
,更何况我们一般开事务也不会无聊到只运行一条增删改语句,所以一个事务下来,就会出现很多条 重做LOG
,那从上面又知道,因为各个硬件写入的速率不同,所以需要有些 Mini-Transaction
:
以下这些常见情况,必须视为一个不可分割的 Mini-Transaction
:
- 没有指定
主键
的情况下,全局更新Max Row ID
时候产生的重做LOG
; - 向聚簇索引的
B+树
插入一条记录产生的重做LOG
; - 向二级索引对应的
B+树
插入一条记录的日志; - ……
五.重做LOG缓冲区
日志已经生成,这时候需要考虑的就是怎么落地的问题了,所以就有了 日志缓冲区
,区
放的是 重做LOG数据页(跟数据的数据页不同)
:
HEADER
和 FOOTER
并不打算细说了,主要就是为了校验,以及管理 BODY
中的内容。所以我们主要来说说 BODY
。
那么 重做LOG
不可能是串行写入的,要不然速度就上不去了,所以运行时 日志缓冲区
就看起来可能是这样的:
如图,事务1
和 事务2
的 MiniTransaction
是交叉运行的,而 事务3
更新的内容比较多,所以会占用多个 缓冲页
,有些甚至更大,会占用多个 缓冲页
。
六.触发重做LOG刷盘
上面那张图引出另外一个问题,已经写入 缓冲区
的 重做LOG
需要怎么落地,如果不落地,那已关机,内存中的 缓冲区
一旦清空,重做LOG
就没有意义了。所以就需要有个机制来刷新这些 重做LOG数据
到硬盘中,那么刷盘的触发情况分为以下几种情况:
- 空间不足,占用配置的
innodb_log_buffer_size
一半左右 的时候,就会触发刷盘; - 事务提交时刷盘,只有刷盘了,才能保证持久性;
- 后台线程,循环着做这件事情,这个前面已经有说过了;
- 关闭服务器;
- …
七.重做LOG刷到哪些文件里
既然要刷盘,那就肯定有对应的文件来接收这些数据,可以通过 SHOW VARIABLES LIKE '%datadir%'
来查看是哪些文件,默认是有 ib_logfile0
和 ib_logfile1
。那如果我们需要调整,调大或者调小就使用下面的参数进行调整:
innodb_log_group_home_dir
:存储日志的目录;innodb_log_file_size
:每个文件的大小,感觉大小这个度需要把握好,太小会导致很多重做LOG
不完整,太大又不利于防灾
;innodb_log_files_in_group
:分割日志的个数,默认是2
个;
那 InnoDB
在写 重做LOG
的时候就从编号 0
开始,写到最后一个。如果到了最后一个并且已经用完了所有空间了,那就重新从 0
开始,依次类推循环的写入硬盘。
循环从头开始写 重做LOG
的时候,那必须会碰到 前面的重做LOG
被覆盖的问题,那如何判断前面的 重做LOG
是否还有用,就是需要判断 BufferPool
中的 脏页
是否已经被刷新到硬盘了,如果已经刷新成功,那么这部分的 重做LOG
肯定是没有用的,可以放心的覆盖。这就回到了我们的初衷:持久化且高效的保存数据
。
那如何判断咧,需要结合之前我们在 BufferPool
中说到的 flush链表
了。
八.重做LOG缓冲页与日志文件的关系
8.1 重做日志文件的格式
上面 第一个管理单元
和 第二四管理单元
里面涉及到的 LSN
和 CheckPoint
是重点。
8.2 LSN - Log Swquence Number
LSN
是一个日志的序列号,这个值从开始有 REDO
开始,将会不停的自增,可以理解为数据表的主键一样,插入一条新的数据的 自增ID
变化情况。InnoDB
会有一个全局变量,专门用于记录这个值变化到哪个数据点,不过他不是从 0
开始而是从 8704
开始,也就是规定一条 重做LOG
都没有的情况下,这个 全局变量
的值就是 8704
。
而 LSN
的增长,是会加上 重做LOG页
的头部和尾部的,就是说当一个 MiniTransaction
跨越几个区的时候,那这个值里面就包含了覆盖到的 HEADER
和 FOOTER
值。
8.4 日志缓冲数据刷盘
由于需要刷盘,并且记录哪些数据已经被刷新到硬盘了,这样才能够释放内存空间让后面的 重做LOG
来用。所以上面提到的 LSN
就开始有用处了。
这时候,InnoDB
中又出现了几个全局变量(可以理解为指针):
buf_next_to_write
:此指针之前的数据表示已经准备刷盘的日志数据,这部分的日志数据已经可以被复用的;flushed_to_disk_lsn
:系统已经返回刷新成功的LSN
的值;buf_free
:此指针之后的内存空间表示是空闲的,如果有新的日志要进来,就需要追加到后面的内存中去,这个值其实就是跟上面提到的LSN
一样的值。
那 buf_next_to_write
到 buf_free
中间的数据就是已经存在于 内存中的
,但是 还没有提交刷入硬盘的请求给系统的数据
。
刷新硬盘:我们需要将文件写入硬盘的时候,其实是提交请求给系统,系统将数据放入数据缓冲区,在某个时刻被刷盘,只有调用了
fsync函数
之后,数据才是真正的进入硬盘。所以这时候需要两个变量来弄清楚哪些数据已经提交给系统,哪些数据已经完完整整落在硬盘上的,就分别对应buf_next_to_write
和flushed_to_disk_lsn
两个变量参数。
那我们就可以知道,刚开机的时候,LSN
的值和上面三个全局变量其实应该是相等的,随着系统的运行,SQL
不断的请求,这时候 buf_free
一定会跑得比别的参数快,然后随着写入硬盘的请求逐步完成,当四个值都相等的时候,就表示所有的日志已经刷入硬盘了。
8.5 flush链表和LSN
flush链表:
flush链表
主要是用来管理BufferPool
中已经被修改的数据页,因为此时内存中的数据页已经发生修改,和硬盘上原有的数据页不同,所以就需要在某个时刻由后台线程
刷新到硬盘上去。但是,这个时候总不可能去循环所有数据页,然后判断是不是脏页吧,所以又有一条链表产生,就是
flush链表
。
我们在对数据进行修改的时候,一直处于 重做LOG缓冲区
的视觉,现在需要将视觉切换到 BufferPool
中去,BufferPool
还有个东西,叫做 flush链表
,为了回忆我已经将上次写的东西放在了上面。
那我们需要结合 MiniTransaction
来说,每个控制块里面有两个属性,一个叫做 oldest_modification
一个叫做 newest_modification
。oldest_modification
会记录在开始做修改时 LSN
的值,修改完成后,newest_modification
记录修改后的 LSN
的值。
那 flush链表
会发生哪些变化,我直接画个动态图来说吧:
最后的状态:
归纳几个特点:
- 最新被修改的数据页,总是会被移动到链表的开始;
- 链表的尾部是比较早些时候发生修改的数据页;
- 如果一个数据页发生过修改已经存在于
flush链表
中的话,如果又有新的修改,om
保持不变,nm
会被修改成新的LSN
值。
8.6 Checkpoint
在 第七节
的时候说过:
那
InnoDB
在写重做LOG
的时候就从编号0
开始,写到最后一个。如果到了最后一个并且已经用完了所有空间了,那就重新从0
开始,依次类推循环的写入硬盘。
所以我们的两个日志文件 ib_logfile0
和 ib_logfile1
,ib_logfile1
在写到结尾的时候,会重新回到 ib_logfile0
开始写,那么 Checkpoint
的提出就是为了解决在回去写 ib_logfile0
的时候是否可以覆盖前面的日志的问题。
结合上面说的 flush链表
,那就可以说了,InnoDB
只需要判断前面的 重做LOG
对应的数据页是否存在 flush链表
,如果不存在,则表示这些数据页已经顺利写到数据页中去,也就表明前面的 重做LOG
已经可以覆盖掉了。
所以,InnoDB
又用了一个全局的变量 checkpoint_lsn
来表示数据页刷新到哪个 LSN
了,而这个值每次在脏页被写入硬盘的时候就会被修改成当前脏页的 newest_modification
。也是下一个脏页的 oldest_modification
。那么 重做LOG
刚开始的日志 LSN
如果小于这个 checkpoint_lsn
的话,表示这些日志已经没用了,可以被覆盖掉。而每进行一次 checkpoint
后,checkpoint_lsn
的值就会被写入日志实体文件的管理信息里面,就是下面蓝色部分:
那这时候我们就可以来说说管理信息中两个 Checkpoint
块信息的事情了。
LOG_CHECKPOINT_NO
是服务器做 Checkpoint
的次数,这个变量有个全局变量,每做一次,此变量就会自增加1,然后被一起写入到这个管理信息块中去。而 LOG_CHECKPOINT_OFFSET
则对应的日志数据的偏移量。那这个偏移量前面的数据就是可以丢弃的。而放入那个 Checkpoint块
中,InnoDB
会判断 LOG_CHECKPOINT_NO
,如果这个数是 偶数
就写入 第一个
块中,如果是 奇数
就写入 第二个
块中。
九.崩溃重做数据页
第八节
说了这么多记录重做的事情,那如果系统发生崩溃需要重做,这时候 重做LOG
就发挥用处了。
那硬盘中的 重做文件
的 头信息
将起到计算从哪里开始恢复的作用:
- 先读取两个文件的所有管理块信息;
- 从所有管理块信息中的两个
Checkpoint
信息中,确定哪个信息的checkpoint_no
比较大,则表示上次checkpoint
做到哪个信息中,这时候就可以顺势拿出checkpoint_log_offset
之后的日志数据了,那这一步就可以确定恢复的日志起点; - 确定哪个
重做LOG数据页
的BODY
没有被写满,则这个地方就是最后需要恢复的地方了; - 然后先排序所有的
重做LOG
数据,然后将相同表空间ID
和页号
的重做数据放在一起(Hash
的方式,可以想象成Map<TableIdAndPage, List<Log>>
的结构); - 由于后台线程在不停的刷新脏页,可能发生数据页已经刷盘但是还没来得及写
checkpoint
的情况,所以现在所有日志并不是都需要重做一次,如果出现数据页结构中的File Header
的FIL_PAGE_LSN
值大于checkpoint_lsn
的话,说明数据已经刷入硬盘了,就不需要再重做。否则,将重做LOG
中的修改某某地方为某个值重新按顺序执行一遍,就可以恢复到奔溃前的数据了。
十.配置重做日志的级别
重做日志是为了 持久化
,那总有一些业务要求的持久化没那么强烈。
如果不需要在每次提交的时候就刷新到硬盘的话,可以修改 innodb_flush_log_at_trx_commit
的值:
0
:表示提交的时候不立即刷新日志,交给后台系统循环去刷新;1
:默认值,表示每次提交需要同步到硬盘;2
:表示提交加入系统缓冲区
,交给系统去刷新,这种情况下如果数据库挂了系统没挂的话,那日志一般不会丢失。