作者 | 曹金霖
杏仁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 记录的是物理页的情况,具有幂等性,因此记录的日志极其简练。
日志刷入磁盘
为了确保每次日志都能写入到事务日志文件中,在每次将 log buffer 中的日志写入日志文件中的过程中,都会调用一次操作系统的 fsync 操作。是工作在用户空间的,所以 log buffer 处于用户空间的内存中。要写入磁盘中的 log file 中,中间需要经过操作系统的内核空间os buffer,调用 fsync 将 OS buffer 中的日志刷到磁盘中的 log file 中。- 当设置为 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。
数据页刷盘的规则及 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 后,就强制刷一部分脏页到磁盘。
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 变化
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 的两阶段提交
- 写 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.cn/post/6844903681091977229 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/ 全文完以下文章您可能也会感兴趣:
- MongoDB应用介绍
- 数据库索引优化
- 聊聊移动端跨平台数据库 Realm
- 数据介绍一个 MySQL 自动化运维利器 - Inception
- Web 与 App 数据交互原理和实现
- 如何成为一名数据分析师:数据的初步认知
- 复杂业务状态的处理:从状态模式到 FSM
- 浅谈 BI 与数据分析的可视化
- 函数扩展 数据可视化过程不完全指南
- 如何进行系统分析与设计