我们在MYSQL执行过程文章中知道一条SQL语句执行流程是怎么样的,但SQL语句是怎么入库的呢?如下图:
在图中涉及三个日志: undo log(回滚日志)、redo log(重做日志) 、binlog (归档日志)
undo log(回滚日志):是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和MVCC。
redo log(重做日志):是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复;
binlog (归档日志):是 Server 层生成的日志,主要用于数据备份和主从复制
下面重点讲解着三个日志怎么样工作和发挥作用的,但是在这之前我们先了解一下什么是WAL和Buffer Pool。
WAL(Write Ahead Log)预写日志,指的是 MySQL 的写操作并不是立刻更新到磁盘上,而是先记录在日志上,然后在合适的时间再更新到磁盘上。也是数据库系统中常见的一种手段,用于保证数据操作的原子性和持久性。
缓冲池(Buffer Pool)主要是为了提高数据库的读写性能的。如下图:
从图中我们可以知道,Buffer Pool就是一块用于缓存MySQL磁盘数据的内存空间。有了这个缓冲池之后,SQL读取数据就变成了:
1. 当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取
2. 当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘
那就会有人会问为什么要缓存MySQL磁盘数据?
下面我们通过一个例子比较一下就知道这个问题的答案了,假如我们需要修改一条记录,而且user表里面只有一条记录,记录的age = 1,执行下面语句:
事务A:update user set age = 2
事务B:update user set age = 3
事务C:update user set age = 4
如果没有Buffer Pool,执行过程如下:
我们可以看出,每次更新都需要从磁盘拿数据(1次I/O),修改完了需要刷到磁盘(1次I/O),也就是每次更新都需要2次磁盘I/O,三次更新需要6次磁盘I/O。
如果有Buffer Pool,执行过程如下:
我们也可以看出,只需要在第一次执行的时候将数据从磁盘拿到Buffer Pool(1次I/O),第三次执行完将数据刷回磁盘(1次I/O),整个过程只需要2次磁盘I/O,比没有Buffer Pool节省了4次磁盘I/O的时间。
这里只是简单的了解一下Buffer Pool,对于Buffer Pool还有许多问题可以探索的,比如Buffer Pool 缓存什么数据,Buffer Pool什么时候刷磁盘,Buffer Pool还没有刷磁盘机器MYSQL宕机怎么办?等等这些问题可以参考:MySQL中Innodb 存储引擎的Buffer Pool详解文章
我们都知道在MYSQL的InnoDB引擎中,如果要执行一条“增删改”语句,即使我们没有输入 begin 开启事务和 commit 提交事务,其实这些语句也是按照事物来提交的,这就是MySQL 会隐式开启事务。而隐式开启事务的控制室通过autocommit
参数决定的,默认是开启。同时,我们也知道事务是可以回滚的。
那么问题来了,它是怎么回滚的呢?
答案:在每次执行事务过程中,会通过一个日志把所有回滚需要的信息记录下来,那么即使执行过程中MYSQL 崩溃了,也不用担心无法回滚到事务之前的数据,我们可以通过这个日志回滚到事务之前的数据。这就是undo log(回滚日志),它保证了事务ACID特征中的原子性(Atomicity)。
undo log就是一种用于撤销回退的日志,里面的记录就是逻辑日志,比如执行delete时,undo log记录的是insert日志,反正亦然;当执行update时,undo log记录的是与update相反的记录。使用undo log实现事务回滚的过程如下:
从上图可以看出回滚事务是通过undo log来实现的,那么undo log都记录那些数据信息呢?
1. 当插入一条记录时,undo log会把这条记录的主键值记录下来,当需要回滚的时候,会读取这条记录的主键值,然后执行删除
2. 当删除一条记录时,undo log会把这条记录的全部内容都记录下来,当需要回滚的时候,会读取这条记录的全部内容并组成语句插入表中
3. 当更新一条记录时,undo log会把这条记录中更新列的旧值记录下来,当需要回滚的时候,会读取这条记录更新列的旧值,然后把旧值更新回去。
到这里可以知道,不同的操作,在undo log中记录的内容也是不一样的,所以操作不同,undo log中记录的格式也不同。有那些具体的不同可以查看InnoDB之UNDO LOG文章。
但是一条记录的每一次更新操作产生的 undo log 格式都有一个 roll_pointer 指针和一个 trx_id 事务id:
通过 trx_id 可以知道该记录是被哪个事务修改的;
通过 roll_pointer 指针可以将这些 undo log 串成一个链表,这个链表就被称为版本链;
另外,undo log 还有一个作用,通过 ReadView + undo log 实现 MVCC(多版本并发控制)。有关这个作用可以阅读MYSQL 事务、事务隔离级别和MVCC,幻读文章
在前面我们知道,Buffer Pool 可以提高读写效率,但是它是基于内存的,所以数据总是不可靠的。比如万一断电了,那么没有及时刷入磁盘的数据有可能丢失。
为了防止断电导致数据丢失的问题,当需要修改记录的时候,InnoDB引擎会把修改之后的值记录到redo log里面,并更新Buffer Pool内存数据。这时候就算断电了,那么也可以从redo log恢复回来。所以这样我们即用到了Buffer Pool内存的高效性,也保证了数据不会丢失。这种先预写日志后面再将数据刷盘的机制就是WAL。
那么什么是redo log?
redo log就是重做日志的意思。它是记录事务产生的物理日志,即记录对那个数据页做了什么修改,对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个事务就会产生这样的一条物理日志。
由于redo log记录的信息很清楚,所以在事务提交时,我们只需要保证redo log文件的持久化就可以,不需要保证Buffer Pool是否持久化。因为即使系统崩溃了,我们也可以在MYSQL重启的时候通过redo log文件恢复回来。
有人也会问redo log文件也是在磁盘里面,数据文件也是在磁盘里面,这不是多余了吗?
当然不多余了。因为redo log是磁盘顺序写(即追加操作),数据刷盘是磁盘随机写(先找到位置,再写入),磁盘的顺序写比随机写高效的多。这也说明了WAL 技术的另外一个优点:MySQL 的写操作从磁盘的「随机写」变成了「顺序写」。所以,为什么需要 redo log 的原因,总结起来如下:
1. 实现事务的持久性,让 MySQL 有 crash-safe(崩溃恢复) 的能力,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失
2. 将写操作从「随机写」变成了「顺序写」,提升 MySQL 写入磁盘的性能
从上面我们知道redo log文件也是写入磁盘的,那么也会有人问下面这个问题:
redo log是直接写入磁盘,还是也有缓存呢?
我们都知道随机写还是顺序写磁盘I/O,它都比内存操作慢得多。所以答案当然不是直接写入磁盘的,它也有缓存,叫做 redo log buffer缓存。即每当产生一条 redo log 时,会先写入到 redo log buffer,后续在持久化到磁盘如下:
而redo log buffer 默认大小 16 MB,可以通过 innodb_log_Buffer_size
参数动态的调整大小,增大它的大小可以让 MySQL 处理「大事务」是不必写入磁盘,进而提升写 IO 性能。
那么问题又来了:
redo log 什么时候写入磁盘,数据也有可能丢失吧?
首先redo log刷盘的机制:
1. MySQL 正常关闭时
2. 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘
3. InnoDB 的后台线程每隔 1 秒,将 redo log buffer 持久化到磁盘
4. 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘(这个策略可由 innodb_flush_log_at_trx_commit 参数控制
从redo log刷磁的机制的第4点,我们知道,可以通过 innodb_flush_log_at_trx_commit 参数来控制刷盘。首先我们了解一下默认的redo log 刷盘:
在执行更新语句的过程中,生成的 redo log 先写入到 redo log buffer 中,然后等事务提交的时候,再将缓存在 redo log buffer 中的 redo log 按组的方式「顺序写」到磁盘,这就是默认的redo log 刷盘。
但是,我们应该了解更多的 innodb_flush_log_at_trx_commit 参数,是怎么样控制刷盘的。即innodb_flush_log_at_trx_commit有三个值:0、1、2,默认值为 1。这三个值的代表的策略如下:
1. innodb_flush_log_at_trx_commit = 0:延迟写,延迟刷。即每次事务提交时 ,还是将 redo log 留在 redo log buffer 中 ,该模式下在事务提交时不会主动触发写入磁盘的操作
2. innodb_flush_log_at_trx_commit = 1:实时写,实时刷。即每次事务提交时,都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘,这样可以保证 MySQL 异常重启之后数据不会丢失。也是默认的模式
3. innodb_flush_log_at_trx_commit = 2:实时写,延迟刷。即每次事务提交时,都只是缓存在 redo log buffer 里的 redo log 写到 redo log 文件,注意写入到「 redo log 文件」并不意味着写入到了磁盘,因为操作系统的文件系统中有个 Page Cache(操作系统的os cache)
可以通过下图更加详细的显示出来:
所以,当innodb_flush_log_at_trx_commit 参数值不为1的时候,它应该通过redo log刷盘的机制的第3点来操作的,即InnoDB 的后台线程每隔 1 秒:
1. innodb_flush_log_at_trx_commit = 0时,会把缓存在 redo log buffer 中的 redo log ,通过调用
write()
写到操作系统的 Page Cache,然后调用fsync()
持久化到磁盘。所以该策略,MySQL 进程的崩溃会导致上一秒钟所有事务数据的丢失2. innodb_flush_log_at_trx_commit = 2 时, 调用 fsync,将缓存在操作系统中 Page Cache 里的 redo log 持久化到磁盘。所以该策略,较innodb_flush_log_at_trx_commit = 0情况下更安全,因为 MySQL 进程的崩溃并不会丢失数据,只有在操作系统崩溃或者系统断电的情况下,上一秒钟所有事务数据才可能丢失
所以,innodb_flush_log_at_trx_commit参数不同值的安全性和写入性能,就不言而喻了:
数据安全性:1 > 2 > 0
写入性能: 0 > 2> 1
所以说,鱼和熊掌不可兼得。即要不追求数据安全性,牺牲性能;要不追求性能,牺牲数据安全性。
我们都知道一个文件总是有大小的,而 redo log文件的大小是由innodb_log_file_size参数控制的,默认是48M。那么问题就来了:
redo log 文件写满了怎么办?
首先,默认情况下,InnoDB 存储引擎有 1 个重做日志文件组( redo log Group),「重做日志文件组」由有 2 个 redo log 文件组成,这两个 redo 日志的文件名:ib_logfile0
和 ib_logfile1
而且,在重做日志文件组中,每个文件大小固定而且一致的。它是以循环写的方式工作的,从头开始写,写到末尾就又回到开头,相当于一个环形。即先写 ib_logfile0 文件,写满了,就会切换到 ib_logfile1 文件,再写满了,就会再切换到ib_logfile0 文件从头开始写。如下:
因为redo log 是为了防止Buffer Pool 中的脏页丢失而设计的,所以当Buffer Pool 的脏页刷新到了磁盘中,redo log 对应的记录也就没用了。所以重做日志文件组设计为循环写的原因。那么是怎么个循环写法呢?即重做日志文件组 相当于一个环形,在InnoDB 用 write pos 表示 redo log 当前记录写到的位置,用 checkpoint 表示当前要擦除的位置,如图:
图解析说明:
1. write pos 和 checkpoint 的移动都是顺时针方向
2. write pos ~ checkpoint 之间的部分(图中的红色部分),用来记录新的更新操作
3. check point ~ write pos 之间的部分(图中蓝色部分):待落盘的脏数据页记录
因为是一个环形,所以当 write pos 追上了 checkpoint,就意味着 redo log 文件满了,这时 MySQL 不能再执行新的更新操作,也就是说 MySQL 会被阻塞(因此所以针对并发量大的系统,适当设置 redo log 的文件大小非常重要),此时会停下来将 Buffer Pool 中的脏页刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动(图中顺时针),然后 MySQL 恢复正常运行,继续执行新的更新操作。
所以,一次 checkpoint 的过程就是脏页刷新到磁盘中变成干净页,然后标记 redo log 哪些记录可以被覆盖的过程。
到这里,我们基本对undo log介绍完了。但是还有一些问题需要了解的
问题1 :被修改 Undo 页面,需要记录对应 redo log 吗?
当然需要。它是这样记录的:开启事务后,InnoDB 层更新记录前,首先要记录相应的 undo log,如果是更新操作,需要把被更新的列的旧值记下来,也就是要生成一条 undo log,undo log 会写入 Buffer Pool 中的 Undo 页面。不过,在修改该 Undo 页面前需要先记录对应的 redo log,所以先记录修改 Undo 页面的 redo log ,然后再真正的修改 Undo 页面。
问题2: redo log 和 undo log 区别?
这两种日志是属于 InnoDB 存储引擎的日志,区别如下:
1. redo log 记录了此次事务「完成后」的数据状态,记录的是更新之「后」的值
2. undo log 记录了此次事务「开始前」的数据状态,记录的是更新之「前」的值
3. redo log 是保证事务四大特性中的持久性,undo log是保证事务四大特性中的原子性
所以,事务提交之前发生了崩溃,重启后会通过 undo log 回滚事务,事务提交之后发生了崩溃,重启后会通过 redo log 恢复事务,如下图:
前面介绍了undo log 和 redo log 这两个日志都是 Innodb 存储引擎生成的。
但是,我们也知道,MYSQL每次更新操作之后,在Server 层还会生成一条 binlog,等之后事务提交的时候,会将该事物执行过程中产生的所有 binlog 统一写 入 binlog 文件。所以,binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT 和 SHOW 操作。
为什么有了 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 用于掉电等故障恢复
同样,我们也有一些问题,如下:
问题1: binlog是直接写入磁盘的吗?
当然不是的。在事务执行过程中,先把日志写到 binlog cache(Server 层的 cache),事务提交的时候,再把 binlog cache 写到 binlog 文件中。
MySQL 给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
问题2: 什么时候 binlog cache 会写到 binlog 文件?
在事务提交的时候,执行器把 binlog cache 里的完整事务写入到 binlog 文件中,并清空 binlog cache。如下图:
虽然每个线程有自己 binlog cache,但是最终都写到同一个 binlog 文件:
图中的 write,指的就是指把日志写入到 binlog 文件,但是并没有把数据持久化到磁盘,因为数据还缓存在文件系统的 page cache 里,write 的写入速度还是比较快的,因为不涉及磁盘 I/O。
图中的 fsync,才是将数据持久化到磁盘的操作,这里就会涉及磁盘 I/O,所以频繁的 fsync 会导致磁盘的 I/O 升高。
binlog的 刷盘 策略是通过sync_binlog 参数控制:
1. sync_binlog = 0 的时候,表示每次提交事务都只 write,不 fsync,后续交由操作系统决定何时将数据持久化到磁盘。默认值
2. sync_binlog = 1 的时候,表示每次提交事务都会 write,然后马上执行 fsync
3. sync_binlog =N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync
所以,sync_binlog 参数不同值的安全性和写入性能,就不言而喻了:
数据安全性:1 > N > 0
写入性能: 0 > N > 1
问题3: 如果不小心整个数据库的数据被删除了,能使用 redo log 文件恢复数据吗?
不可以使用 redo log 文件恢复,只能使用 binlog 文件恢复。因为 redo log 文件是循环写,是会边写边擦除日志的,只记录未被刷入磁盘的数据的物理日志,已经刷入磁盘的数据都会从 redo log 文件里擦除。而binlog 文件保存的是全量的日志,也就是保存了所有数据变更的情况,理论上只要记录在 binlog 上的数据,都可以恢复,所以如果不小心整个数据库的数据被删除了,得用 binlog 文件恢复数据。
上面也提到binlog的用途之一是主从复制,如果想了解更多主从复制相关的知识,可以看MYSQL主从复制文章
错误日志记录着 mysqld 启动和停止,以及服务器在运行过程中发生的错误的相关信息。在默认情况下,系统记录错误日志的功能是关闭的,错误信息被输出到标准错误输出。可以通过下面命令查看错误日志信息:
SHOW VARIABLES LIKE '%err%';