MySql 日志共有错误日志、查询日志等等几个大类的日志,其中比较重要的主要是三种日志:二进制日志 binlog(归档日志)、事务日志 redolog(重做日志)、undolog(回滚日志)。每种日志的作用不尽相同,下文将对这三种日志进行详细介绍。
我们可以想象一个场景,我们知道我们对数据库进行增删改的时候,其实是需要读取磁盘中的页,然后对页进行修改后,修改后将页写回磁盘,但单纯的以这种方式会造成下面两个问题:
1、刷新一个完成的数据页太过浪费,有时候我们只需要修改其中的一个字节,但是由于 InnoDB 是以页为单位进行磁盘 I/O 的,也就是说在该事务提交的时候不得不将一个完整的页面从内存中刷新到磁盘,我们又知道,一个页的大小是 16 K,如果修改一个字节就要刷新 16K 的数据到磁盘上,显然太浪费
2、随机 I/O 刷新起来十分慢,一个事务可能涉及到多个不同的页面,这些页面可能不相邻,这就意味着 Buffer Pool 可能会进行很多随机 I/O。
如何解决这两个问题?
我们的目的是想让已经提交的事务对数据库的修改能永久的生效,即使后来系统崩溃,在重启后也能把这种修改恢复过来,所以其实没有必要在每次事务提交的时候就把所有修改的全部页面刷进磁盘,只需要把这个修改的内容记录一下就好。
比如:“将第 0 号表空间的 100 页号中偏移量为 1000 处的值更新为 2”
这样在事务提交时,就会把上述内容刷进磁盘,这样即使系统崩溃,重启的时候更新一下数据页,这样数据库的修改就能恢复过来,即保证了数据库的 “持久性” —— 说过的话一定要做到。这样的日志称为 redo 日志。这样做的好处:
1、日志的存储空间非常小,在存储表空间 ID 、页号、偏移量以及需要更新的值,
2、redo 日志是按顺序写入磁盘的,在执行事务过程中,每执行一条语句,就可能产生若干条 redo 日志,这些日志是按照顺序写入磁盘,也就是使用顺序 I/O
再思考一个问题:我们知道我们修改表会有很多种情况,比如修改索引,分页等等,如果把这些一步一步的操作直接写下来会非常大,那如何解决?
这里 InnoDB的解决办法就是设定很多种类的日志,我们在遇见不同种类的操作的时候,只需记得其参数(如页号、偏移量等),在需要执行的时候,识别不同的日志,调用不用的函数进行对应操作,而不是记录具体操作步骤。
InnoDB中日志被划分成许多不可分割的组,如:
向聚簇索引对应的 B+ 树的页面插入一条记录产生的 redo 是一组,是不可分割的
向二级索引对应的 B+ 树插入一条记录产生的 redo 是一组,不可分割
不可分割的指:日志的操作必须是原子操作,我们在完成一项任务的时候,要就不做,要就全部做完,所以在每一个不可分割的块中,都有开始和结束的标志,一旦开始执行,就需要执行到结束标志,否则丢弃前面的执行。
而一个不可分割的过程称为一个 Mini-Transaction(MTR),一个MTR包含若干个 redo 日志。
那日志直接是写入磁盘嘛? 并不是,redo 日志也有自己对应的日志缓冲区。
在服务器启动的时候就向操作系统申请了一大块称为 redo log buffer 的连续内存空间,这片空间被划分为若干个 redo log block
因此日志不是直接就写入磁盘,而是先写入磁盘缓冲区,那什么时候将缓冲区的日子刷入磁盘呢?有以下几种情况
1、log buffer空间不足的时候,当前 log buffer占用 50% 以上的时候,就会进行刷盘,将日志刷进磁盘
2、事务提交的时候:之所以提出 redo 日志的原因,就是其占用的空间小,而且可以顺序的写入磁盘,引入 redo 日志后,虽然在事务提交的时候可以不把修改过的 buffer pool 页面立即刷入磁盘,但为了保证持久性,必须把页面修改时对应的 redo 日志刷新进磁盘。
3、将某个脏页刷进磁盘前,必须保证对应的 redo 日志已经刷进磁盘(由于 redo 是顺序刷新的,所以在把脏页的 redo 的刷进磁盘的前提就是,前面的日志也全部刷近磁盘)
4、后台有个线程,大约每一秒会将 log buffer 中 redo 日志刷进磁盘
5、正常关闭服务器
6、做 checkpoint 时
什么是 checkpoint ?
由于我们的日志文件组的容量少有限的,所以不得不循环使用 redo 日志文件组中的文件,但这会导致最后写入的日志 和 最先写入的 日志 冲突,所以 InnoDB 使用标志标记现在已经刷到哪了,另一个标志去标记新的日志写在哪,若即将发生冲突,则进行刷盘,防止覆盖,这个过程称为 checkpoint
其实 MySql 也提供了参数让我们去选择刷盘的策略:
InnoDB
存储引擎为 redo log
的刷盘策略提供了 innodb_flush_log_at_trx_commit
参数,它支持三种策略:
innodb_flush_log_at_trx_commit
参数默认为 1 ,也就是说当事务提交时会调用 fsync
对 redo log 进行刷盘
另外,InnoDB
存储引擎有一个后台线程,每隔1
秒,就会把 redo log buffer
中的内容写到文件系统缓存(page cache
),然后调用 fsync
刷盘。
而数据库会在合适的时候将 redo 日志更新的内容刷新进磁盘,这通常是在数据库空闲的时候进行。
下面是 三种参数的刷盘策略
刚刚介绍的 redo 日志是在 InnoDB 引擎上的日志,而 binlog 则是在 Server 层进行操作的日志:
既然是在 Server 层的日志,没有区别引擎,说明所有引擎的数据库都有 binlog 日志,那为什么 InnoDB 有一套日志,MySql本身也有一套日志呢?
因为以前的MySQL没有InnoDB引擎,MySQL5.5前使用的 MyISAM引擎,但是 MyISAM 没有 crash-safe 的能力,而 binlog 只能用于归档。InnoDB 是后来作为 MySQL 的引擎以插件形式引入的。既然只靠 binlog 无法实现 crash-safe 的能力,所以 InnoDB 使用另一套日志系统——redo log 来实现。 那我们利用一条 updata 语句的执行过程,来理解这两个日志分别行使的作用:假如我们要将 A 数据进行 +1 操作:
为什么 redo log有两种状态(prepare、commit)?即两阶段提交?
为什么要有两阶段提交,就是为了让 redo log 和 binlog 两个文件保持一致。我们还是用反证法来说明,假设没有两阶段提交会发生什么问题:
如果没有“两阶段提交”,会导致 redo log 和 binlog 记录的操作不一致,那么数据库的状态就有可能和用它的日志恢复出来的库数据不一致。
所以,能够保证 redo log 和 binlog 的操作记录一致的流程是,将操作先更新到内存,再写入 redo log,此时标记为 prepare 状态,再写入 binlog,此时再提交事务,将 redo log 标记为 commit 状态。
那 binlog 的作用是什么?如图:
binlog在MySQL的server层产生,不属于任何引擎,主要记录用户对数据库操作的SQL语句(除了查询语句)。之所以将binlog称为归档日志,是因为binlog不会像redo log一样擦掉之前的记录循环写,而是一直记录(超过有效期才会被清理),如果超过单日志的最大值(默认1G,可以通过变量 max_binlog_size 设置),则会新起一个文件继续记录。但由于日志可能是基于事务来记录的(如InnoDB表类型),而事务是绝对不可能也不应该跨文件记录的,如果正好binlog日志文件达到了最大值但事务还没有提交则不会切换新的文件记录,而是继续增大日志,所以 max_binlog_size 指定的值和实际的binlog日志大小不一定相等。
正是由于binlog有归档的作用,所以binlog主要用作主从同步和数据库基于时间点的还原。
那么回到刚才的问题,binlog可以简化掉吗?这里需要分场景来看:
binlog 的写入机制:binlog 同样是先写入到 binlog cache,事务提交的时候再把 binlog cache 的内容写入磁盘,如果内存不足的时候可以利用 磁盘来进行 swap 存储 。
binlog 和 redolog :
update T set c=c+1 where ID=2;
这条SQL,redo log 中记录的是 :xx页号,xx偏移量的数据修改为xxx;
binlog 中记录的是:id = 2 这一行的 c 字段 +1
讲到这里,我们来想一个完整的问题,如果 MySQL 在提交事务的时候突然崩溃,重启的时候数据是如何恢复的?
下面就是 MySQL 在重启恢复后,在提供服务前需要做的事情:
简单来说共有两步:
1、检查已经刷盘的 redo 日志是否已经更新进入数据库,即日志是否和数据一致
2、保证 redo log 和 binlog一致,奔溃重启后会检查redo log中是完整并且处于prepare状态的事务,然后根据XID(事务ID),从binlog中找到对应的事务,如果找不到,则回滚,找到并且事务完整则重新commit redo log,完成事务的提交。
那这里我们崩溃的时刻有几种情况:
参考 : https://zhuanlan.zhihu.com/p/142491549
那这里涉及到的 undo 日志就是下面我要介绍的。
前面说到用 redo 和 binlog 保证服务器崩溃的时候数据的恢复,这样可以保证已提交事务的持久性,但前面介绍过一种情况,在事务还没有提交的时候,写的 redo 日志很可能已经刷盘,那么这些未提交事务的修改过的页在 MySql 重启的时候可能也被恢复了,但为了保证事务的原子性 —— 要么不做,要么全部做完,这里就可能会出现需要回滚数据的任务,这就要用到 undo 日志 —— 做过的事情想反悔。
undo 日志记载了回滚一个操作所需的必要内容。而其原理设计到一个概念 : 事务 ID
事务 ID:
在事务对表中记录进行改动的时候(如 UpDate),才会为这个事务分配一个唯一的事务 ID,这个事务 ID 是一个递增的数字,未被分配事务 ID 的事务默认为 0, 聚簇索引记录中有一个 trx_id 隐藏列,它代表对这个聚簇索引记录进行改动的语句所在的事务对应的事务 ID。(在 undo 在 mvcc 中的应用中会用到)
InnoDB 利用专门的页面去存储 undo 日志,而在记录中有着专门指向 undo 记录的指针,如下图所示:
当需要回滚的时候,就会调用之前的 undo 记录,进行数据的还原。