前言
今天开始,会陆续更新 MySQL 相关知识。Java 多线程方面慢慢更新,因为后面要记录的面试偶尔会问,所以还是希望把大部分时间用在经常问的知识上。
MySQL 开篇不想讲索引,以后会说到,但还是想聊聊比较有意思的,会陆续更新 MySQL 的日志。今天主要讲事务日志之一,redo log。
背景
在介绍 redo log 前呢我先简单介绍一下它存在的背景。MySQL 的数据是存在磁盘中的,但如果每次读取数据都需要经过磁盘 io 的话,那么这成本是很高,效率很低的。那么 innodb 就提供一个缓存 buffer,这个 buffer 中呢包含了磁盘中的部分数据页的映射,作为访问数据库的一个缓冲。当数据库读取这个数据呢,就会先从这个 buffer 中取。如果 buffer 中没有呢就从磁盘中取,读取完之后再放到 buffer 缓冲区中。当向数据库写入数据时也会首先向这个 buffer 中写入数据,定期将 buffer 中的数据刷新到磁盘上进行持久化的操作。那么这个时候就存在一个问题,虽然读写效率提升了,那么它也增加了丢失数据的风险。如果 buffer 中的数据还没有来得及刷新到磁盘上,这个时候 MySQL 宕机了,那么 buffer 中的数据就会丢失掉,进而造成数据的丢失。数据丢失了,这个事务的持久性也就无法保证了。正是因为这个背景,redo log 就被引入进来。
持久性的原理- redo log
redo log,即重做日志。redo log包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。只要事务提交了,那么它对数据的修改时永久性的。
加入 redo log 后,数据要刷新到磁盘的流程会改进为:当数据库的数据要进行新增和修改的时候,除了要修改 buffer 中的数据,还要将整条数据记录到 redo log 中。即使 MySQL 宕机了,那么它还有 redo log 去恢复数据。
在概念上,innodb通过 force log at commit 机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log file 和undo log file中进行持久化。
为了确保每次日志都能写入到事务日志文件中,在每次将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中的过程:
redolog 是预写式日志,就是先写日志再写磁盘。既然 redo log 也需要提交日志再写磁盘,那它为什么比直接将这个 buffer 的数据写入磁盘要快呢。一是因为 buffer 的持久化呢是随机写的 io,每次修改数据位置呢都是随机的。但是 redo log 是追加模式的,它是在文件的尾部去追加,是一种顺序 io 的操作。随机 io 要比顺序 io 要慢,尤其是在传统的机械硬盘上。二是 buffer 持久化数据是以数据页 page 为单位,MySQL 默认这个页大小是 16k,一个数据页中小小的修改都要把整个页写入,那么 redo log 只需要把需要的部分写入就可以了。大大减少了无效 io。
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 时
- 其他一些情况...
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 到磁盘中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,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。
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,会覆盖掉前面还没来得及更新到磁盘的数据。
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
结语
今天和大家一起过了一遍 redo log, 事务特性中持久化的底层原理就是由 redo log 实现的。介绍了 redo log 是如何保证持久化的:当数据库的数据要进行新增和修改的时候,除了要修改 buffer 中的数据,还要将整条数据记录到 redo log 中。即使 MySQL 宕机了,那么它还有 redo log 去恢复数据。并介绍了 redo log 刷盘的时机,其中在提交时会将 redo log 刷新到磁盘,通过 innodb_flush_log_at_trx_commit 这个参数控制,这个参数的值可以分别为 0、1 和 2。最后还和大家一起了解了一下 日志的文件格式及如何存储的。
掌握了今天的内容感觉面试就不用担心再被问到 redo log 了。下次会讲原子性的原理 undo log。
站在巨人肩膀上
参考
-
《详细分析MySQL事务日志》www.cnblogs.com/f-ck-need-u…
-
极客时间.《MySQL 45讲》
-
《MySQL 是怎样运行的:从根儿上理解 MySQL》