作者 | 曹金霖
杏仁Java工程师,正在锻炼自制力的朴素程序猿。
redo log -> 物理日志
redo log 通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。
redo log 是 InnoDB 引擎独有的
redo log 包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。redo log在数据准备修改前写入缓存中的 redo log 中,然后才对缓存中的数据执行修改操作;而且保证在发出事务提交指令时,先将缓存中的redo log写入日志,写入完成后才执行提交动作。
redo log 的写入是使用了 WAL(Write-Ahead Logging)技术。
redo log 记录的是物理页的情况,具有幂等性,因此记录的日志极其简练。
WAL:用户如果对数据库中的数据进行了修改,必须保证日志先于数据落盘。当日志落盘后,就可以给用户返回操作成功,并不需要保证当时对数据的修改也落盘。如果数据库在日志落盘前 crash,那么相应的数据修改会回滚。在日志落盘后 crash,会保证相应的修改不丢失。
日志刷入磁盘
为了确保每次日志都能写入到事务日志文件中,在每次将 log buffer 中的日志写入日志文件中的过程中,都会调用一次操作系统的 fsync 操作。是工作在用户空间的,所以 log buffer 处于用户空间的内存中。要写入磁盘中的 log file 中,中间需要经过操作系统的内核空间os buffer,调用 fsync 将 OS buffer 中的日志刷到磁盘中的 log file 中。
用户可以自定义在 commit 的时候如何将 log buffer 中的日志刷入 log file 中。这种控制通过变量 innodb_flush_log_at_trx_commit 的值来决定。该变量有 3 种值:0,1,2,默认为 1。但注意,这个变量只是控制 commit 动作是否刷新 log buffer 到磁盘。
当设置为 0 的时候,事务提交时不会将 log buffer 中的日志写入到OS buffer,而是每秒写入 OS buffer,并调用 fsync() 写入到 log file中。也就是说设置为 0 时,系统崩溃,会丢失1秒的数据。
当设置为 1 的时候,事务每次提交都会将 log buffer 中的日志写入OS buffer并调用 fsync() 刷到 log file 中。这种方式即使系统崩溃都不会丢失数据,但是每次提交都写入磁盘,IO 的性能较差。
当设置为 2 的时候,每次提交都仅写入到 OS buffer,然后每秒调用 fsync() 将 OS buffer 中的日志写入 log file on disk。
log buffer 中未刷到磁盘的日志称为脏日志(dirty log)。
在上面的说过,默认情况下事务每次提交的时候都会刷事务日志到磁盘中,这是因为变量 innodb_flush_log_at_trx_commit 的值为 1。但是 InnoDB 不仅仅只会在有 commit 动作后才会刷日志到磁盘,这只是 InnoDB 存储引擎刷日志的规则之一。
刷日志到磁盘有以下几种规则:
1.发出 commit 动作时。已经说明过, commit 发出后是否刷日志由变量 innodb_flush_log_at_trx_commit 控制。
2.每秒刷一次。这个刷日志的频率由变量 innodb_flush_log_at_timeout 值决定,默认是1秒。要注意,这个刷日志频率和 commit 动作无关。
3.当 log buffer 中已经使用的内存超过一半时。
4.当有 checkpoint 时,checkpoint 在一定程度上代表了刷到磁盘时日志所处的LSN位置。
数据页刷盘的规则及 checkpoint
内存中(buffer pool)未刷到磁盘的数据称为脏数据(dirty data)。由于数据和日志都以页的形式存在,所以脏页表示脏数据和脏日志。
如果 redo log 可以无限地增大,同时缓冲池也足够大,意味着可以不将缓冲池中的脏页刷新回磁盘上。宕机时,完全可以通过 redo log 来恢复整个数据库系统中的数据。
显然,上述的前提条件是不满足的,这也就引入了 checkpoint 技术。
Checkpoint(检查点)的目的是为了解决以下几个问题:1、缩短数据库的恢复时间;2、缓冲池不够用时,将脏页刷新到磁盘;3、redo log不可用时,刷新脏页。
数据库宕机时,不需要重做所有的日志,因为checkpoint之前的脏页都已经刷新到磁盘了,只需要对 CheckPoint 后的 redo log 进行恢复即可,这样就打打缩短了恢复的时间。
当缓冲池不够用时,会根据LRU算法淘汰最近最少使用的页,若此页为脏页,那么需要强制执行 Checkpoint,将脏页刷回磁盘。
当前数据库对 redo log 的设计都是循环使用的,为了防止被覆盖,必须强制 Checkpoint,将缓冲池中的页刷新到当前 redo log 的位置。
Checkpoint 的分类
sharp checkpoint:在重用 redo log 文件(例如切换日志文件,正常关闭数据库)的时候,将所有已记录到redo log中对应的脏数据刷到磁盘。
fuzzy checkpoint:一次只刷一小部分的日志到磁盘,而非将所有脏日志刷盘。有以下几种情况会触发该检查点:
Master Thread Checkpoint:InnoDB 的主线程以每秒或每十秒的速度从缓冲池的脏页列表中刷新一定比例的页回磁盘,这个过程是异步的,此时 InnoDB 可以进行其他的操作,用户查询线程不会阻塞。
flush_lru_list checkpoint:从 MySQL5.6 开始可通过 innodb_page_cleaners 变量指定专门负责脏页刷盘的page cleaner线程的个数,该线程的目的是为了保证lru列表有可用的空闲页。空闲页为变量 innodb_lru_scan_depth 控制
async/sync flush checkpoint:在 redo log 日志文件不可用时,强制将脏页列表中的一些页刷新回磁盘,而此时脏页是从脏页列表中选取的,保证redo log文件可循环使用。
dirty page too much checkpoint:脏页太多时强制触发检查点,目的是为了保证缓存有足够的空闲空间。too much 的比例由变量 innodb_max_dirty_pages_pct 控制,MySQL5.6 默认的值为 75,即当脏页占缓冲池的百分之 75 后,就强制刷一部分脏页到磁盘。
由于刷脏页需要一定的时间来完成,所以记录检查点的位置是在每次刷盘结束之后才在 redo log 中标记的。
sharp checkpoint 发生时记录 LSN(log sequence number)到最后一个提交的事物的位置。当然,没有提交的事物是不会被刷新到磁盘当中的。fuzzy checkpoint 发生的时候会记录两次 LSN,也就是检查点发生的时间和检查点结束的时间。但是呢,被刷新的页在并不一定在某一个时间点是一致的,这也就是它为什么叫fuzzy的原因。较早刷入磁盘的数据可能已经修改了,较晚刷新的数据可能会有一个比前面 LSN 更新更小的一个 LSN 。fuzzy checkpoint 在某种意义上可以理解为 fuzzy checkpoint 从 redo log 的第一个 LSN 执行到最后一个 LSN。恢复以后的话,redo log 就会从最后一个检查点开始时候记录的 LSN 开始。
MySQL 停止时是否将脏数据和脏日志刷入磁盘,由变量 innodb_fast_shutdown={ 0|1|2 } 控制,默认值为 1,即停止时忽略所有 flush 操作,在下次启动的时候再 flush,实现 fast shutdown。
LNS 分析
LSN 称为日志的逻辑序列号(log sequence number),在 InnoDB 存储引擎中, lsn 占用 8 个字节。LSN 的值会随着日志的写入而逐渐增大。
在 InnoDB 每次都取最老的 modified page 对应的 LSN,并将此脏页的 LSN 作为 Checkpoint 点记录到日志文件,意思就是 “此 LSN 之前对应的日志和数据都已经刷新到磁盘” 。
LSN 不仅存在于 redo log 中,还存在于数据页中,在每个数据页的头部,有一个 file_page_lsn 记录了当前页最终的 LSN 值是多少。通过数据页中的 LSN 值和 redo log 中的 LSN 值比较,如果页中的 LSN 值小于 redo log 中 LSN 值,则表示数据丢失了一部分,这时候可以通过redo log 的记录来恢复到 redo log 中记录的 LSN 值时的状态。可以通过 show engine innodb status;
查看引擎中的 LSN
其中:
log sequence number 就是当前的 redo log(in buffer)中的 LSN;
log flushed up to 是当前已经写入日志文件做持久化的 LSN;
pages flushed up to 是已经刷到磁盘数据页上的 LSN;
last checkpoint at 是上一次检查点所在位置的 LSN。
InnoDB 的 LSN 变化
1. 12点开启事务,此时4个LSN的值都是相同的为100
log sequence number(100) = log flushed up to(100) = pages flushed up to = last checkpoint at
2. 位置(1)执行一条 update 语句,执行完成后,buffer中的数据页和redo log都记录好更新后的LSN值101,其他值不变
log sequence number(101) > log flushed up to(100) = pages flushed up to = last checkpoint at
3. 位置(2)12:00:01,触发redo log的刷盘规则(其中有一个规则是 innodb_flush_log_at_timeout 控制的默认日志刷盘频率为1秒),此时redo log 文件中的LSN会更新到和buffer中一致,其他值不变
log sequence number(101) = log flushed up to > pages flushed up to(100) = last checkpoint at
4. 位置(3)在执行一条update语句,此时buffer中的LSN更新为102
log sequence number(102) > log flushed up to(101) > pages flushed up to(100) = last checkpoint at
5. 位置(4)checkpoint出现,触发数据页和日志页刷盘,但是需要一定的时间来完成,所以在数据页刷盘未完成时,检查点的LSN还是上一次检查点的LSN,但是磁盘上的数据页和日志页的LSN已经变化了。
log sequence number(102) ? log flushed up to ? pages flushed up to > last checkpoint at
log flushed up to 和 pages flushed up to 的大小无法确定,因为日志刷盘的速度和数据刷盘的速度无法确认。但是checkpoint机制会保护数据刷盘的速度慢于日志刷盘的速度。
6.位置(5)数据页和日志页输盘完毕,此时所有的LSN都等于102
log sequence number(102) = log flushed up to = pages flushed up to = last checkpoint at
7.位置(6)执行一条insert语句,此时buffer中的LSN更新为103
log sequence number(103) > log flushed up to(102) = pages flushed up to = last checkpoint at
8.位置(7)提交事务,提交动作会触发日志刷盘,但是不会触发数据刷盘
log sequence number(103) = log flushed up to > pages flushed up to(102) = last checkpoint at
9.位置(8)checkpoint出现,这次checkpoint不会触发日志刷盘,因为日志的LSN在checkpoint出现之前就同步过了,所以4个LSN的值的情况都是103
InnoDB 的恢复
在启动 InnoDB 的时候,不管上次是正常关闭还是异常关闭,总是会进行恢复操作。
重启 InnoDB 时,checkpoint 表示已经完整刷到磁盘上的 LSN,因此恢复时仅需要恢复从 checkpoint 开始的日志部分。例如,数据库中上次的 checkpoint 的 LSN 为 10000,切事务已经提交过了。启动数据库时会检查磁盘中数据页的 LSN,如果数据页的 LSN小于日志中的 LSN,则会从检查点开始恢复。
redo log 的两阶段提交
事务的提交涉及到binlog及具体的存储的引擎的事务提交。所以 InnoDB 引擎用两阶段提交来保证的事务的完整性,在上图中可以看到,在数据页的数据修改完成后,首先进行了redo log的写入,同时将redo log的状态设置成prepare。然后进行了bin log的写入。最后提交事务的时候,将redo log的状态设置成commit
bin log 是逻辑日志,记录的是这个语句的原始逻辑,并且是采用“追加写”的形式。负责 的归档。这是 redo log 没有的能力。
redo log 和 binlog 都可以用于表示事务的提交状态,redo log 用于crash-safe,bin log 用于数据归档,两阶段提交就是让这2个状态保持逻辑上的一致
如果没有采用两阶段提交:
写 redo log 后写 binlog。假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1。但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。
先写 binlog 后写 redo log。如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。
有了两阶段提交,当系统出现异常宕机时:
binlog 有记录,redo log 状态 commit: 正常完成的事务,不需要恢复
binlog 有记录,redo log 状态 prepare: 在 binlog 写完提交事务之前的 crash, 恢复操作:提交事务
binlog 无记录,redo log 状态 prepare: 在 binlog写完之前的 crash, 恢复操作:回滚事务
binlog 无记录,redo log 无记录: 在 redo log 写之前 crash, 恢复操作:回滚事务
小结
这里大概了描述了一下 InnoDB 中 redo log 的相关知识,redo log 在 中有着很重要的地位,同时其中的一些设计也对我们的编程有帮助,比如两阶段提交。通过系统的学习也能了解到 在数据恢复上是如何实现的。
参考
1.详细分析MySQL事务日志(redo log和undo log)
https://juejin.im/entry/5ba0a254e51d450e735e4a1f
2.极客时间-MySQL实战45讲
https://time.geekbang.org/column/intro/139
3.InnoDB Checkpoint
https://jin-yang.github.io/post/mysql-innodb-checkpoint.html
4.阿里巴巴-数据库内核月报
http://mysql.taobao.org/monthly/
全文完
以下文章您可能也会感兴趣:
我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。