目录
Buffer Pool是什么?
redo log(Innodb独有)
为什么需要redolog?
类比的方式巧记redolog
binlog(Server层独有)
binlog是干啥的?
为什么有了 binlog, 还要有 redo log?
redo log 和 binlog 有什么区别?
update流程在两个日志中是怎样的?
两阶段提交详细过程
异常重启会出现什么现象?
事务还没提交的时候,redo log 能不能被持久化到磁盘呢?
为什么说事务还没提交的时候,redolog 也有可能被持久化到磁盘呢?
那就有了一个新问题:因为事务还没提交,如果在redolog刷盘之后宕机或者是发现该事物中出现错误需要回滚,又该对redolog做什么操作呢?是否应该删除redolog对该事务的记录?
怎样让数据库恢复到半个月内任意一秒的状态?
定期全量备份的周期“取决于系统重要性,有的是一天一备,有的是一周一备”。那么在什么场景下,一天一备会比一周一备更有优势呢?或者说,它影响了这个数据库系统的哪个指标?
笔记来自MySQL45讲+小林coding+自己总结归纳
Buffer Pool是什么?
首先我们知道MySQL的所有数据都是存储在磁盘中的,当我们要更新一条数据时,是需要从磁盘中把这条数据拿出来才能更新,那修改完之后肯定要缓存起来,因为后面有语句命中的时候就不需要再从磁盘里读了,提高了数据库的读写性能。
架构图:
有了Buffer Pool后有两个好处
- 查数据时,直接从Buffer Pool中取,如果没有命中就去磁盘中读取。
- 更新数据时,如果这条数据存在于Buffer Pool中,就直接更新这条数据所在的页,然后把这页标记为脏页,后续由后台线程选择一个合适的时机更新到磁盘中。
redo log(Innodb独有)
为什么需要redolog?
我们知道Buffer Pool提高了数据库的读写性能,但是Buffer Pool是存在于内存中的,一旦断电,内存里还没落盘的脏页数据就会丢失,所以为了解决这个问题,redolog就诞生了。
那以后要更新记录时,除了在Buffer Pool标记脏页外,还会把这条记录所在的页的修改以redo log的形式记录下来,后续在合适的时机由后台线程刷新到磁盘中,这就是WAL (Write-Ahead Logging)技术,说白了就是先写日志(写到redolog buffer中,提交事务时会刷到redolog文件中),此时更新操作就算是完成了,再写磁盘(把脏页刷新到磁盘)。
类比的方式巧记redolog
《孔乙己》这篇文章,酒店掌柜有一个粉板,专门用来记录客人的赊账记录。如果赊账的人不多,那么他可以把顾客名和账目写在板上。但如果赊账的人多了,粉板总会有记不下的时候,这个时候掌柜一定还有一个专门记录赊账的账本。
如果有人要赊账或者还账的话,掌柜一般有两种做法:
- 一种做法是直接把账本翻出来,把这次赊的账加上去或者扣除掉;
- 另一种做法是先在粉板上记下这次的账,等打烊以后再把账本翻出来核算。
在生意红火柜台很忙时,掌柜一定会选择后者,因为前者操作实在是太麻烦了。首先,你得找到这个人的赊账总额那条记录。你想想,密密麻麻几十页,掌柜要找到那个名字,再拿出算盘计算,最后再将结果写回到账本上。所以有了粉板,我们的效率就非常高了。
同样,在 MySQL 里也有这个问题,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。为了解决这个问题,MySQL 的设计者就用了类似酒店掌柜粉板的思路来提升更新效率。
而粉板和账本配合的整个过程,也就是WAL技术,关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写账本。
具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log(粉板)里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,这就像打烊以后掌柜做的事。
但如果某天赊账的特别多,粉板写满了,又怎么办呢?这个时候掌柜只好放下手中的活儿,把粉板中的一部分赊账记录更新到账本中,然后把这些记录从粉板上擦掉,为记新账腾出空间。
与此类似,InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么这块“粉板”总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
InnoDB 用 write pos 表示 redo log 当前记录写到的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。
checkpoint 表示当前要擦除的位置。
- write pos ~ checkpoint 之间的部分(图中的红色部分),用来记录新的更新操作;
- check point ~ write pos 之间的部分(图中蓝色部分):待落盘的脏数据页记录
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。
要理解 crash-safe 这个概念,可以想想我们前面赊账记录的例子。只要赊账记录记在了粉板上或写在了账本上,之后即使掌柜忘记了,比如突然停业几天,恢复生意后依然可以通过账本和粉板上的数据明确赊账账目。
binlog(Server层独有)
binlog是干啥的?
binlog 文件记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT 和 SHOW 操作。
MySQL 在完成一条更新操作后,Server 层还会生成一条 binlog,等之后事务提交的时候,会将该事务执行过程中产生的所有 binlog 统一写 入 binlog 文件。
为什么有了 binlog, 还要有 redo log?
这个问题跟 MySQL 的时间线有关系。
最开始 MySQL 里并没有 InnoDB 引擎,MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。
而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用 redo log 来实现 crash-safe 能力
redo log 和 binlog 有什么区别?
1、适用对象不同:
- binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用;
- redo log 是 Innodb 存储引擎实现的日志;
2、文件格式不同:
- binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED,区别如下:
- STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端再根据 SQL 语句重现。但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致;
- ROW:记录行数据最终被修改成什么样了(这种格式的日志,就不能称为逻辑日志了),不会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已;
- MIXED:包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式;
- redo log 是物理日志,记录的是在某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新;
3、写入方式不同:
- binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。
- redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志。
4、用途不同:
- binlog 用于备份恢复、主从复制;
- redo log 用于掉电等故障恢复
update流程在两个日志中是怎样的?
- 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
- 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
- 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
- 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成
两阶段提交详细过程
- prepare 阶段:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作用);
- commit 阶段:把 XID 写入到 binlog,然后将 binlog 持久化到磁盘(sync_binlog = 1 的作用),接着调用引擎的提交事务接口,将 redo log 状态设置为 commit
异常重启会出现什么现象?
下图中有时刻 A 和时刻 B 都有可能发生崩溃:
不管是时刻 A(redo log 已经写入磁盘, binlog 还没写入磁盘),还是时刻 B (redo log 和 binlog 都已经写入磁盘,还没写入 commit 标识)崩溃,此时的 redo log 都处于 prepare 状态。
在 MySQL 重启后会按顺序扫描 redo log 文件,碰到处于 prepare 状态的 redo log,就拿着 redo log 中的 XID 去 binlog 查看是否存在此 XID:
- 如果 binlog 中没有当前内部 XA 事务的 XID,说明 redolog 完成刷盘,但是 binlog 还没有刷盘,则回滚事务。对应时刻 A 崩溃恢复的情况。
- 如果 binlog 中有当前内部 XA 事务的 XID,说明 redolog 和 binlog 都已经完成了刷盘,则提交事务。对应时刻 B 崩溃恢复的情况。
对于处于 prepare 阶段的 redo log,即可以提交事务,也可以回滚事务,这取决于是否能在 binlog 中查找到与 redo log 相同的 XID
事务还没提交的时候,redo log 能不能被持久化到磁盘呢?
先说答案,答案就是有可能。
redolog刷盘由 innodb_flush_log_at_trx_commit 参数控制,可取的值有:0、1、2,默认值为 1,这三个值分别代表的策略如下:
- 当设置该参数为 0 时,表示每次事务提交时 ,还是将 redo log 留在 redo log buffer 中
- 当设置该参数为 1 时,表示每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘
- 当设置该参数为 2 时,表示每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 操作系统层面的page cache中,但是没有执行 fsync 操作持久化到磁盘
为什么说事务还没提交的时候,redolog 也有可能被持久化到磁盘呢?
主要有三种可能的原因:
- 第一种情况:InnoDB 有一个后台线程,每隔 1 秒轮询一次,调用 write 将 redolog buffer 中的日志写到文件系统的 page cache,然后调用 fsync 持久化到磁盘。所以,一个没有提交的事务的 redolog,也是有可能会被后台线程一起持久化到磁盘的。
- 第二种情况:innodb_flush_log_at_trx_commit 设置是 1,这个参数的意思就是,每次事务提交的时候,都执行 fsync 将 redolog 直接持久化到磁盘(还有 0 和 2 的选择,0 表示每次事务提交的时候,都只是把 redolog 留在 redolog buffer 中;2 表示每次事务提交的时候,都只执行 write 将 redolog 写到文件系统的 page cache 中)。举个例子,假设事务 A 执行到一半,已经写了一些 redolog 到 redolog buffer 中,这时候有另外一个事务 B 提交,按照 innodb_flush_log_at_trx_commit = 1 的逻辑,事务 B 要把 redolog buffer 里的日志全部持久化到磁盘,这时候,就会连带上事务 A 在 redolog buffer 里的日志一起持久化到磁盘。
- 第三种情况:redo log buffer 占用的空间达到 redolog buffer 大小(由参数 innodb_log_buffer_size 控制,默认是 8MB)一半的时候,后台线程会主动写盘。不过由于这个事务并没有提交,所以这个写盘动作只是 write 到了文件系统的 page cache,仍然是在内存中,并没有调用 fsync 真正落盘。
那就有了一个新问题:因为事务还没提交,如果在redolog刷盘之后宕机或者是发现该事物中出现错误需要回滚,又该对redolog做什么操作呢?是否应该删除redolog对该事务的记录?
MySQL 不会直接删除已写入磁盘的 redo log,而是通过以下方式处理:
正常事务回滚(未崩溃时)
- InnoDB 会在内存中生成对应的 undo log(记录如何撤销修改)
- 执行 undo log 回滚数据页修改
- redo log 仍保留在磁盘,但会被标记为"无效"
- 后续 checkpoint 机制会跳过这些无效日志
崩溃恢复时(应用两阶段提交)
根据 redo log 的 prepare 状态和 binlog 完整性决定:
- 如果 binlog 不完整:回滚事务(使用 undo log)
- 如果 binlog 完整:提交事务
怎样让数据库恢复到半个月内任意一秒的状态?
如果你的 DBA 承诺说半个月内可以恢复,那么备份系统中一定会保存最近半个月的所有 binlog,同时系统会定期做整库备份。这里的“定期”取决于系统的重要性,可以是一天一备,也可以是一周一备。
当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那你可以这么做:
- 首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
- 然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻。
这样你的临时库就跟误删之前的线上库一样了,然后你可以把表数据从临时库取出来,按需要恢复到线上库去。
定期全量备份的周期“取决于系统重要性,有的是一天一备,有的是一周一备”。那么在什么场景下,一天一备会比一周一备更有优势呢?或者说,它影响了这个数据库系统的哪个指标?
- 核心业务(如订单、支付)推荐 一天一备 + binlog实时同步(RPO近0)。
- 非关键业务(如日志、测试库)可 一周一备,甚至结合快照备份。
一天一备的最长恢复时间更短,最坏情况下需要应用一天的binlog,比如,你每天0点做一次全量备份,而要恢复出一个到昨天晚上23点的备份。
一周一备最坏情况就要应用一周的binlog了。