背景
众所周知,MySQL 的数据是存在磁盘中的,但如果每次读取数据都需要经过磁盘 io 的话,那么这成本是很高,效率很低的。那么 innodb 就提供一个缓存 buffer,这个 buffer 中呢包含了磁盘中的部分数据页的映射,作为访问数据库的一个缓冲。当数据库读取这个数据呢,就会先从这个 buffer 中取。如果 buffer 中没有呢就从磁盘中取,读取完之后再放到 buffer 缓冲区中。当向数据库写入数据时也会首先向这个 buffer 中写入数据,定期将 buffer 中的数据刷新到磁盘上进行持久化的操作。那么这个时候就存在一个问题,虽然读写效率提升了,那么它也增加了丢失数据的风险。如果 buffer 中的数据还没有来得及刷新到磁盘上,这个时候 MySQL 宕机了,那么 buffer 中的数据就会丢失掉,进而造成数据的丢失。数据丢失了,这个事务的持久性也就无法保证了。正是因为这个背景,redo log 就被引入进来。
持久性的原理-redo log
redo log,即重做日志。当你提交事务的时候,绝对是保证把你对缓存也做得修改以日志的形式写入 redo log 日志,哪怕此时突然宕机了,也没关系。因为 MySQL 重启之后,把你之前事务更新做的修改根据 redo log 在 Buffer Pool 里重做一遍就可以了,然后找时机再把缓存页刷入磁盘文件里去。
那事务提交的时候把修改过的缓存页都刷入磁盘,跟事务提交的时候把你做的修改的 redo log 都写入日志文件,他们不都是写磁盘么?差别在哪里呢?
实际上,如果你把修改过的缓存页都刷入磁盘,这首先缓存页一个就是 16kb,数据比较大,刷入磁盘比较耗时,而且你可能就改了缓存页里的几个字节的数据,我们不可能把完整的缓存页刷入磁盘。而且,缓存页刷入磁盘是随机写磁盘,性能很差。写 redo log 是顺序写,写入磁盘速度快。所以用 redo log 的形式记录下来你做的修改,性能会远远超过刷缓存页的方式,这也可以让你的数据库的并发能力更强。
redo log 长什么样
它里面需要记录的大致为 日志类型 + 表空间号 + 数据页号 + 偏移量 + 修改几个字节的值 + 具体的值。
其中,日志类型是记录了你修改了数据页里的几个字节的值,redo log 就划分为了不同的类型,MLOG_1BYTE 类型的日志就是修改了 1 个字节的值,MLOG_2BYTE 类型指的是修改了2 个字节的值,以此类推。但如果你要是一下子修改了一大串的值,类型就是 MLOG_WRITE_STRING,就是代表你一下子在那个数据页的某个偏移量的位置插入或者修改了一大串的值。
redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。只要事务提交了,那么它对数据的修改时永久性的。
加入 redo log 后,数据要刷新到磁盘的流程会改进为:当数据库的数据要进行新增和修改的时候,除了要修改 buffer 中的数据,还要将整条数据记录到 redo log 中。即使 MySQL 宕机了,那么它还有 redo log 去恢复数据。
redo log 本质是保证事务提交之后,修改的数据绝对不会丢失(持久性)。
可能有聪明的同学会提出疑问,既然事务提交的时候把修改过的缓存页都刷入磁盘,跟你事务提交的时候把你做的修改的 redo log 都写入文件,他们不都是写磁盘么?差别在哪?
为了确保每次日志都能写入到事务日志文件中,在每次将 log buffer 中的日志写入日志文件的过程中都会调用一次操作系统的 fsync 操作(即fsync()系统调用)。因为 MySQL 是工作在用户空间的,MySQL 的 log buffer 处于用户空间的内存中。要写入到磁盘上的 log file 中 (redo:ib_logfileN文件,undo:share tablespace或.ibd文件),中间还要经过操作系统内核空间的 os buffer,调用 fsync() 的作用就是将 OS buffer 中的日志刷到磁盘上的 log file 中。
下图是从 redo log buffer 写日志到磁盘的 redo log file中的过程:
redo log 是预写式日志,就是先写日志再写磁盘。既然 redo log 也需要提交日志再写磁盘,那它为什么比直接将这个 buffer 的数据写入磁盘要快呢。一是因为 buffer 的持久化呢是随机写的 io,每次修改数据位置呢都是随机的。但是 redo log 是追加模式的,它是在文件的尾部去追加,是一种顺序 io 的操作。随机 io 要比顺序 io 要慢,尤其是在传统的机械硬盘上。二是 buffer 持久化数据是以数据页 page 为单位,MySQL 默认这个页大小是 16k,一个数据页中小小的修改都要把整个页写入,那么 redo log 只需要把需要的部分写入就可以了。大大减少了无效 io。
redo log block
刚刚我们了解了 redo log 的日志格式,那么 redo log 是按照格式,一条一条的直接就写入到磁盘上的日志文件里去了吗?
当然不是!其实 MySQL 内有另外一个数据结构,叫做 redo log block,redo log 不是单行单行地写入日志文件的,它是用一个 redo log block 来存放多个单行日志。redo log block 大小为 512 字节,当 block 满了的时候就会写入 redo log。
redo log buffer
redo log 是如何通过内存缓冲区进入磁盘文件里去的呢?这就涉及到了一个新的组件, redo log buffer,他是 MySQL 专门为了用来缓冲 redo log 写入的。
这个 redo log buffer 其实是 MySQL 启动的时候,跟操作系统申请的一块连续的内存空间,然后里面划分了 N 多个空的 redo log block,写满了一个就会写下一个,以此类推,知道所有 redo log block 都写满。此时必然会强制把 redo log block 刷入磁盘文件中去的。
如果一组 redo log 实在太多了,那么就可能会存放在两个 redo log block 中,反之,如果说一个 redo log group 比较小,那么也可能多个 redo log group 是在一个 redo log block里。
所以呢,平时我们一个一个的事务里产生的多条 redo log,会形成一个 redo log group,一组 redo log 会写入 redo log buffer 的 block 里,然后 redo block 再写入 redo log 磁盘文件,这样全流程就清楚了。
redo log 的刷盘时机

- log buffer 空间不足时
log buffer 的大小是有限的(通过系统变量 innodb_log_buffer_size指定),如果不停的往这个有限大小的 log buffer 里塞日志,很快它就会被填满。设计者认为写入 log buffer 日志量沾满了 log buffer 总容量的大约 一半 左右,就需要把这些日志刷新到磁盘上。
- 事务提交时
MySQL支持用户自定义在 commit 时如何将 log buffer 中的日志刷 log file 中。这种控制通过变量 innodb_flush_log_at_trx_commit 的值来决定。该变量有3种值:0、1、2,默认为1。但注意,这个变量只是控制 commit 动作是否刷新log buffer到磁盘。
当设置为1的时候,事务每次提交都会将 log buffer 中的日志写入os buffer 并调用 fsync() 刷到log file on disk中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO的性能较差。
当设置为0的时候,事务提交时不会将log buffer中日志写入到os buffer,而是每秒写入os buffer并调用fsync()写入到log file on disk中。也就是说设置为0时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据。
当设置为2的时候,每次提交都仅写入到os buffer,然后是每秒调用fsync()将os buffer中的日志写入到log file on disk。
- 后台线程不停的刷刷刷
- 正常关闭服务器时
- 做所谓的 checkpoint 时
-
redo log 日志文件格式
MySQL 的数据目录下默认有两个名为 ib_logfile0 和 ib_logfile1 的文件,log buffer 中的日志默认情况下就是刷新到这两个磁盘文件中。
在将 redolog 写入日志文件组时,是从 ib_logfile0 开始写,如果 ib_logfile0 写满了,就接着 ib_logfile1 写,以此类推…如果写到最后一个文件咋办?那就重新转到 ib_logfile0 继续写。
在这张图里,checkpoint 是往后推移并且循环的,擦除记录前要把记录更新到数据文件。write pos 和 checkpoint 之间是空着的部分,可以用来记录新的操作。如果 write pos 追上了 checkpoint,会覆盖掉前面还没来得及更新到磁盘的数据。站在巨人肩膀上
参考
《详细分析MySQL事务日志》www.cnblogs.com/f-ck-need-u…
- 极客时间.《MySQL 45讲》
- 《MySQL 是怎样运行的:从根儿上理解 MySQL》
