MySQL 三大日志
前言
在上一篇文章,我们讲了MySQL的架构以及一条查询语句的执行过程。在执行查询语句的时候,需要经过连接器、分析器、优化器等,最后从存储引擎中提取数据。
那么接下来呢,我们就来看看一条更新语句是如何执行的。
首先,先来建个表:
mysql> create table T(ID int primary key, c int);
如果说,我要将 ID = 2 的这一行 +1,我应该这么做:
mysql> update T set c=c+1 where ID=2;
然后,我们再来看看查询语句那张图。其实,查询语句所走的流程,更新语句也会重新走一遍。
但是,和查询流程不同的是,更新流程涉及到了三种重要的日志模块:undo log 、redo log 和 bin log。
在学习 MySQL 的过程中,这三个日志是比较重要的知识了。今天,我们进来学习一下这三个日志。
undo log
现在来想一个问题,假如我们在执行事务的过程中,MySQL 发生了崩溃,那应该怎么回到事务开始前的数据状态呢?
这时候就要用到 undo log 了。
undo log 是一种逻辑日志,记录了事务操作之前的数据状态,用于支持事务回滚。它可以在事务开始时保存数据的“旧值”,以便在需要时可以撤销事务的操作。
我们还是通过具体的例子来进行分析:
现在有一张表:
id | value |
---|---|
1 | 100 |
2 | 200 |
我们开启这样一个事务:
START TRANSACTION;
UPDATE t SET value = 150 WHERE id = 1;
DELETE FROM t WHERE id = 2;
当事务开始的时候,undo log 中就会记录 UPDATE 和 DELETE 操作的逆操作:
- UPDATE 的逆操作是 value=100 。
- DELETE 的逆操作是 INSERT INTO t (id, value) VALUES (2, 200) 。
如果事务中途发生崩溃或执行 ROLLBACK 时,undo log 会根据记录的逆操作将数据恢复到初始状态 。
对于不同类型的操作,undo log 会有不同的记录:
- 当插入一行数据时,undo log 记录该行的主键和其他列数据。 回滚时,执行 DELETE 操作,删除插入的行。
- 当删除一行数据时,undo log 会完整记录被删除行的所有列值。回滚时,执行 INSERT 操作,将被删除的行重新插入。
- 当更新一行数据时,undo log 会记录被修改列的原始值。回滚时,执行逆向的 UPDATE 操作,将被修改的值恢复为旧值。
通过这个例子,我们能够看出,undo log 可以用于事务回滚。当事务中断或崩溃时,undo log 会提供数据修改前的快照,允许数据库恢复到事务开始之前的状态。所以,undo log 保证了事务四大特性中的原子性(确保事务中的所有操作要么全部完成,要么全部撤销)和一致性( 确保数据库状态可以恢复到事务开始前的一致性状态)。
另外,undo log 还和 ReadView 一起实现了 MVCC,即多版本并发控制。关于这一部分的内容,我会用一篇单独的文章来讲。
Buffer Pool
在讲 redo log 之前,我们先来了解一个新的概念:Buffer Pool ,我们在讲 redo log 的时候会用到。
先来看一个场景,在一个电商平台中,某个用户想要查询自己的订单信息。数据库中有一个名为 orders 的表,存储了所有用户的订单数据。
当这个查询被发出时,MySQL 是如何快速返回数据的呢?
MySQL 首先会检查 Buffer Pool 是否已经缓存了 orders 表的数据页。如果数据页在 Buffer Pool 中,直接从内存读取,速度是非常快的。如果数据页不在 Buffer Pool 中,则触发磁盘 I/O,读取相关数据页到 Buffer Pool中,这种情况下速度就会慢一些了。
我们假设这个用户查询的订单信息所在的数据页不在 Buffer Pool 中,而是在磁盘中,那么 MySQL 就需要把这个数据页从磁盘读取到 Buffer Pool 中,然后缓存起来,如果后续有其他查询再次想访问这个数据页,直接从 Buffer Pool 中读取就可以了。
想要修改数据的时候,如果这条数据所在的数据页处于 Buffer Pool 中,那么就会直接在 Buffer Pool 中进行修改,然后把这个数据页设置为脏页,然后在合适的时机由后台线程写入到磁盘。
Buffer Pool 中缓存了一些信息:
综上,我们知道,Buffer Pool 是 MySQL 用于减少磁盘 I/O 的内存缓存。
redo log
好,通过上一章节的学习,我们已经了解了 Buffer Pool 。下面我们就一起来看看 Buffer Pool 是如何与 redo log 进行协作的。
假如我们开启了一个事务来执行写操作,那么相关的数据页就会被加载到 Buffer Pool 并在内存中修改。这时,数据并不会立即写回磁盘,而是留在 Buffer Pool 中。 在修改 Buffer Pool 的同时,InnoDB 也会将修改操作记录到 redo log 中。 事务提交时,仅需确保 redo log 的记录被刷盘到磁盘(称为 WAL(Write-Ahead Logging)机制),此时即使内存数据未落盘,事务也被认为持久化。至于留在 Buffer Pool 中的脏页,会由后台线程在一个合适的时机异步刷回磁盘。
WAL技术是指,MySQL 的写操作不是先写到磁盘上,而是先写日志,然后再在合适的时机写到磁盘上。
如果事务提交之后,数据库发生断电或者宕机,后台线程还没来得及刷盘,Buffer Pool 中的数据就丢失了,咋办?不慌,有 redo log 呢。
数据库重启时,InnoDB 从 redo Log 中读取未完成的日志记录。 根据 redo log 的内容,重新应用对数据页的修改,将数据恢复到断电前的状态。恢复的数据就会被重新加载到 Buffer Pool 中。redo log 这个恢复数据的能力,就是 crash-safe(崩溃恢复) 能力。
所以现在你明白为什么需要 redo log 了吧。
它存在的意义,就是即使发生宕机,已提交事务的修改仍能通过 redo log 恢复。 事务提交只需要确保 redo log 写入磁盘即可,无需等待脏页刷新到磁盘,这样就降低了事务延迟。
现在我们再来想一个问题:redo log 也要写入磁盘,被修改的数据也要写入磁盘,不是多此一举吗?
其实, redo log 是顺序写入磁盘的,而写数据的时候,则是随机写入磁盘的。 而顺序写入的速度大约比随机写入快数倍。顺序写有点类似于磁盘顺序写入时的数据连续存储,而随机写会造成大量的寻址和磁头移动,效率低下。
好,综上所述,我们可以得出 redo log 的两个作用:
- redo log 实现了事务的持久性。它可以保证数据库即使崩溃,也可以凭借 WAL 技术实现 crash-safe,来恢复数据。
- redo log 使用顺序写的方式加快了 MySQL 写入磁盘的速度。
说到这,我们再来聊一个细节。
在 InnoDB 中, 当事务执行 修改操作(如 INSERT
、UPDATE
、DELETE
)时,生成的 redo log 并不是直接每次写入磁盘,而是会将这些操作的 日志记录 先写入 redo log buffer, redo log buffer 是在内存中的一个区域,速度较快,可以通过顺序写入快速记录事务的操作。redo log buffer 中的记录并不会立即写入磁盘,而是在事务提交时,InnoDB 会将 redo log buffer 中的日志刷新到磁盘中的 redo log 文件,确保事务的持久性。
除了在事务提交时刷新日志外,InnoDB 还会定期将 redo log buffer 中的内容刷盘到磁盘,以保证内存中的日志不会丢失。
生成的 redo log 先写入 redo log buffer 而不是磁盘,有以下两个优点:
- 提高速度:写入内存中的 Redo Log Buffer 速度比直接写入磁盘要快得多,避免了磁盘的频繁随机写操作。
- 减少磁盘 I/O:如果每次事务修改都直接写入磁盘,会产生大量的磁盘 I/O 操作,降低性能。通过先写入内存,再根据需要批量刷新到磁盘,减少了对磁盘的直接访问。
那既然有了日志就要往 redo log 中写,redo log 文件也总有被写满的时候。那么 redo log 文件要是被写满了怎么办呢?
InnoDB 中有一个日志文件组,这个文件组由两个日志文件构成,文件名分别是ib_logfile0
和 ib_logfile1
。这两个文件有一个特点,那就是它们都是环形的,在写入的时候是循环写的,也就是说,当一个文件被写满的时候,就会从头再开始写。
另外,在写入日志的时候,如果第一个日志文件(ib_logfile0
)写满了,InnoDB 会自动切换到下一个日志文件(ib_logfile1
),继续写入日志。一旦第二个日志文件也写满,InnoDB 会将写指针重新定位到第一个日志文件(ib_logfile0
)开始覆盖已经写过的内容,这时会先确保已经提交的事务的日志不会丢失。这个循环会一直进行,确保日志的不断写入。
下面我们就来看看具体是怎么循环写的。
在 redo log 文件中有两个重要的位置,它们分别是 write pos 和 check point。write pos 表示当前的记录写到哪了,check point 表示当前要被擦除的位置。
如图所示:
write pos 和 check point 都是按照顺时针的方向移动的。
write pos – check point 之间的部分,用来记录新的更新操作。
check point – write pos 之间的部分,表示需要被刷盘的脏数据页记录。
如果在某一时刻,write pos 和 check point 重合了,就表示 redo log 文件被写满了。这时候,MySQL 就需要把 Buffer Pool 当中的脏数据页进行刷盘,然后把 redo log 文件当中对应的记录擦除,来腾出新的空间记录新的操作。有空间了,check point 就可以继续按照顺时针移动了。
binlog
好,我们已经学习了 undo log 和 redo log ,下面我们再来学习三大日志中的最后一个日志:binlog。
MySQL 在执行更新操作的时候,也会产生相应的 binlog,之后在事务提交时,MySQL 就会将事务中所产生的所有 binlog 统一写入 binlog 文件中。
binlog 记录了所有数据库表结构变更和表数据修改,但是不会记录查询类的操作。
基于 binlog 上述的特点,binlog 的作用也就随之产生了:
- binlog 可以用于主从复制。在主服务器上,所有的写操作(如
INSERT
、UPDATE
、DELETE
等)都会被记录到 binlog 中,从服务器通过读取主服务器的 binlog 来同步数据。 - binlog 也用于 增量备份和 数据恢复。通过将 binlog 备份与数据文件一起使用,用户可以恢复到某一时刻的数据库状态。
- binlog 还可以用于审计与监控。通过分析 binlog,可以进行数据库操作的审计,查看每一条 SQL 操作。它可以帮助监控和追踪数据库的变化。
redo log 记录了更新操作,binlog 也记录了更新操作。那这么一看,他俩还是不是挺像的,下面我们就来看看它们的区别,千万别混淆了。
在适用对象上:
- binlog 是在 server 层实现的日志,所有的存储引擎都可以使用。
- redo log 是 InnoDB 存储引擎特有的。
在文件格式上:
- redo log 是循环写入的,记录了对数据页的 物理修改。记录的是 数据页的修改,每次对数据库表中数据的更改都会写入该日志。它并不包含 SQL 语句,而是记录修改后的物理数据页内容。
- binlog 记录的是 逻辑操作,包括 SQL 语句(如
INSERT
、UPDATE
、DELETE
)或者对行数据的操作。binlog 可以选择不同的格式:Statement-based 格式记录 SQL 语句;Row-based 格式记录具体数据行的变化。Mixed 格式根据操作自动选择最合适的格式。
在用途上:
- redo log 实现了事务的持久性,用于崩溃恢复。
- binlog 主要用于备份恢复和主从复制。
binlog 主要用于主从复制,主从复制是怎么实现的呢?
主从复制可以分为三个关健阶段:
- 主服务器生成 Binlog:主服务器记录所有的数据库修改操作到 Binlog。每个写入操作都会生成一个 Binlog 事件,并写入磁盘。
- 从服务器的 I/O 线程拉取 Binlog:从服务器启动
I/O线程
,连接到主服务器,向主服务器请求 Binlog 文件。主服务器返回 Binlog 文件,并将其写入从服务器的 Relay Log。 - 从服务器的 SQL 线程执行中继日志:从服务器的 SQL 线程 从中继日志中读取事件,执行相应的 SQL 操作,将修改同步到从服务器的数据库中。这样就确保了从服务器的数据库与主服务器一致。
总结
好,至此,三大日志我们就讲完了。通过这篇文章,我们知道了 undo log 用于事务的回滚,保证了事务的原子性;redo log 具有 crash-safe 能力,保证了事务的持久性;而 binlog 主要用于主从复制,以及主从复制的三个阶段是怎样的。