mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-16 18:10:13 +08:00
[docs update]完善对undo log的介绍
This commit is contained in:
parent
9b8d1e3d35
commit
790f10dc1e
@ -9,27 +9,27 @@ tag:
|
||||
|
||||
## 前言
|
||||
|
||||
`MySQL` 日志 主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中,比较重要的还要属二进制日志 `binlog`(归档日志)和事务日志 `redo log`(重做日志)和 `undo log`(回滚日志)。
|
||||
MySQL 日志 主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中,比较重要的还要属二进制日志 binlog(归档日志)和事务日志 redo log(重做日志)和 undo log(回滚日志)。
|
||||
|
||||

|
||||
|
||||
今天就来聊聊 `redo log`(重做日志)、`binlog`(归档日志)、两阶段提交、`undo log` (回滚日志)。
|
||||
今天就来聊聊 redo log(重做日志)、binlog(归档日志)、两阶段提交、undo log(回滚日志)。
|
||||
|
||||
## redo log
|
||||
|
||||
`redo log`(重做日志)是`InnoDB`存储引擎独有的,它让`MySQL`拥有了崩溃恢复能力。
|
||||
redo log(重做日志)是 InnoDB 存储引擎独有的,它让 MySQL 拥有了崩溃恢复能力。
|
||||
|
||||
比如 `MySQL` 实例挂了或宕机了,重启时,`InnoDB`存储引擎会使用`redo log`恢复数据,保证数据的持久性与完整性。
|
||||
比如 MySQL 实例挂了或宕机了,重启时,InnoDB 存储引擎会使用 redo log 恢复数据,保证数据的持久性与完整性。
|
||||
|
||||

|
||||
|
||||
`MySQL` 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 `Buffer Pool` 中。
|
||||
MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 `Buffer Pool` 中。
|
||||
|
||||
后续的查询都是先从 `Buffer Pool` 中找,没有命中再去硬盘加载,减少硬盘 `IO` 开销,提升性能。
|
||||
后续的查询都是先从 `Buffer Pool` 中找,没有命中再去硬盘加载,减少硬盘 IO 开销,提升性能。
|
||||
|
||||
更新表数据的时候,也是如此,发现 `Buffer Pool` 里存在要更新的数据,就直接在 `Buffer Pool` 里更新。
|
||||
|
||||
然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(`redo log buffer`)里,接着刷盘到 `redo log` 文件里。
|
||||
然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(`redo log buffer`)里,接着刷盘到 redo log 文件里。
|
||||
|
||||

|
||||
|
||||
@ -64,15 +64,15 @@ InnoDB 将 redo log 刷到磁盘上有几种情况:
|
||||
|
||||
刷盘策略`innodb_flush_log_at_trx_commit` 的默认值为 1,设置为 1 的时候才不会丢失任何数据。为了保证事务的持久性,我们必须将其设置为 1。
|
||||
|
||||
另外,`InnoDB` 存储引擎有一个后台线程,每隔`1` 秒,就会把 `redo log buffer` 中的内容写到文件系统缓存(`page cache`),然后调用 `fsync` 刷盘。
|
||||
另外,InnoDB 存储引擎有一个后台线程,每隔`1` 秒,就会把 `redo log buffer` 中的内容写到文件系统缓存(`page cache`),然后调用 `fsync` 刷盘。
|
||||
|
||||

|
||||
|
||||
也就是说,一个没有提交事务的 `redo log` 记录,也可能会刷盘。
|
||||
也就是说,一个没有提交事务的 redo log 记录,也可能会刷盘。
|
||||
|
||||
**为什么呢?**
|
||||
|
||||
因为在事务执行过程 `redo log` 记录是会写入`redo log buffer` 中,这些 `redo log` 记录会被后台线程刷盘。
|
||||
因为在事务执行过程 redo log 记录是会写入`redo log buffer` 中,这些 redo log 记录会被后台线程刷盘。
|
||||
|
||||

|
||||
|
||||
@ -84,15 +84,15 @@ InnoDB 将 redo log 刷到磁盘上有几种情况:
|
||||
|
||||

|
||||
|
||||
为`0`时,如果`MySQL`挂了或宕机可能会有`1`秒数据的丢失。
|
||||
为`0`时,如果 MySQL 挂了或宕机可能会有`1`秒数据的丢失。
|
||||
|
||||
#### innodb_flush_log_at_trx_commit=1
|
||||
|
||||

|
||||
|
||||
为`1`时, 只要事务提交成功,`redo log`记录就一定在硬盘里,不会有任何数据丢失。
|
||||
为`1`时, 只要事务提交成功,redo log 记录就一定在硬盘里,不会有任何数据丢失。
|
||||
|
||||
如果事务执行期间`MySQL`挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。
|
||||
如果事务执行期间 MySQL 挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。
|
||||
|
||||
#### innodb_flush_log_at_trx_commit=2
|
||||
|
||||
@ -100,13 +100,13 @@ InnoDB 将 redo log 刷到磁盘上有几种情况:
|
||||
|
||||
为`2`时, 只要事务提交成功,`redo log buffer`中的内容只写入文件系统缓存(`page cache`)。
|
||||
|
||||
如果仅仅只是`MySQL`挂了不会有任何数据丢失,但是宕机可能会有`1`秒数据的丢失。
|
||||
如果仅仅只是 MySQL 挂了不会有任何数据丢失,但是宕机可能会有`1`秒数据的丢失。
|
||||
|
||||
### 日志文件组
|
||||
|
||||
硬盘上存储的 `redo log` 日志文件不只一个,而是以一个**日志文件组**的形式出现的,每个的`redo`日志文件大小都是一样的。
|
||||
硬盘上存储的 redo log 日志文件不只一个,而是以一个**日志文件组**的形式出现的,每个的`redo`日志文件大小都是一样的。
|
||||
|
||||
比如可以配置为一组`4`个文件,每个文件的大小是 `1GB`,整个 `redo log` 日志文件组可以记录`4G`的内容。
|
||||
比如可以配置为一组`4`个文件,每个文件的大小是 `1GB`,整个 redo log 日志文件组可以记录`4G`的内容。
|
||||
|
||||
它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示。
|
||||
|
||||
@ -117,15 +117,15 @@ InnoDB 将 redo log 刷到磁盘上有几种情况:
|
||||
- **write pos** 是当前记录的位置,一边写一边后移
|
||||
- **checkpoint** 是当前要擦除的位置,也是往后推移
|
||||
|
||||
每次刷盘 `redo log` 记录到**日志文件组**中,`write pos` 位置就会后移更新。
|
||||
每次刷盘 redo log 记录到**日志文件组**中,`write pos` 位置就会后移更新。
|
||||
|
||||
每次 `MySQL` 加载**日志文件组**恢复数据时,会清空加载过的 `redo log` 记录,并把 `checkpoint` 后移更新。
|
||||
每次 MySQL 加载**日志文件组**恢复数据时,会清空加载过的 redo log 记录,并把 `checkpoint` 后移更新。
|
||||
|
||||
`write pos` 和 `checkpoint` 之间的还空着的部分可以用来写入新的 `redo log` 记录。
|
||||
`write pos` 和 `checkpoint` 之间的还空着的部分可以用来写入新的 redo log 记录。
|
||||
|
||||

|
||||
|
||||
如果 `write pos` 追上 `checkpoint` ,表示**日志文件组**满了,这时候不能再写入新的 `redo log` 记录,`MySQL` 得停下来,清空一些记录,把 `checkpoint` 推进一下。
|
||||
如果 `write pos` 追上 `checkpoint` ,表示**日志文件组**满了,这时候不能再写入新的 redo log 记录,MySQL 得停下来,清空一些记录,把 `checkpoint` 推进一下。
|
||||
|
||||

|
||||
|
||||
@ -172,9 +172,9 @@ MySQL830 mysql:8.0.32
|
||||
|
||||
### redo log 小结
|
||||
|
||||
相信大家都知道 `redo log` 的作用和它的刷盘时机、存储形式。
|
||||
相信大家都知道 redo log 的作用和它的刷盘时机、存储形式。
|
||||
|
||||
现在我们来思考一个问题:**只要每次把修改后的数据页直接刷盘不就好了,还有 `redo log` 什么事?**
|
||||
现在我们来思考一个问题:**只要每次把修改后的数据页直接刷盘不就好了,还有 redo log 什么事?**
|
||||
|
||||
它们不都是刷盘么?差别在哪里?
|
||||
|
||||
@ -190,32 +190,32 @@ MySQL830 mysql:8.0.32
|
||||
|
||||
而且数据页刷盘是随机写,因为一个数据页对应的位置可能在硬盘文件的随机位置,所以性能是很差。
|
||||
|
||||
如果是写 `redo log`,一行记录可能就占几十 `Byte`,只包含表空间号、数据页号、磁盘文件偏移
|
||||
如果是写 redo log,一行记录可能就占几十 `Byte`,只包含表空间号、数据页号、磁盘文件偏移
|
||||
量、更新值,再加上是顺序写,所以刷盘速度很快。
|
||||
|
||||
所以用 `redo log` 形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。
|
||||
所以用 redo log 形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。
|
||||
|
||||
> 其实内存的数据页在一定时机也会刷盘,我们把这称为页合并,讲 `Buffer Pool`的时候会对这块细说
|
||||
|
||||
## binlog
|
||||
|
||||
`redo log` 它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 `InnoDB` 存储引擎。
|
||||
redo log 它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎。
|
||||
|
||||
而 `binlog` 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于`MySQL Server` 层。
|
||||
而 binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于`MySQL Server` 层。
|
||||
|
||||
不管用什么存储引擎,只要发生了表数据更新,都会产生 `binlog` 日志。
|
||||
不管用什么存储引擎,只要发生了表数据更新,都会产生 binlog 日志。
|
||||
|
||||
那 `binlog` 到底是用来干嘛的?
|
||||
那 binlog 到底是用来干嘛的?
|
||||
|
||||
可以说`MySQL`数据库的**数据备份、主备、主主、主从**都离不开`binlog`,需要依靠`binlog`来同步数据,保证数据一致性。
|
||||
可以说 MySQL 数据库的**数据备份、主备、主主、主从**都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性。
|
||||
|
||||

|
||||
|
||||
`binlog`会记录所有涉及更新数据的逻辑操作,并且是顺序写。
|
||||
binlog 会记录所有涉及更新数据的逻辑操作,并且是顺序写。
|
||||
|
||||
### 记录格式
|
||||
|
||||
`binlog` 日志有三种格式,可以通过`binlog_format`参数指定。
|
||||
binlog 日志有三种格式,可以通过`binlog_format`参数指定。
|
||||
|
||||
- **statement**
|
||||
- **row**
|
||||
@ -237,21 +237,21 @@ MySQL830 mysql:8.0.32
|
||||
|
||||
这样就能保证同步数据的一致性,通常情况下都是指定为`row`,这样可以为数据库的恢复与同步带来更好的可靠性。
|
||||
|
||||
但是这种格式,需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗`IO`资源,影响执行速度。
|
||||
但是这种格式,需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗 IO 资源,影响执行速度。
|
||||
|
||||
所以就有了一种折中的方案,指定为`mixed`,记录的内容是前两者的混合。
|
||||
|
||||
`MySQL`会判断这条`SQL`语句是否可能引起数据不一致,如果是,就用`row`格式,否则就用`statement`格式。
|
||||
MySQL 会判断这条`SQL`语句是否可能引起数据不一致,如果是,就用`row`格式,否则就用`statement`格式。
|
||||
|
||||
### 写入机制
|
||||
|
||||
`binlog`的写入时机也非常简单,事务执行过程中,先把日志写到`binlog cache`,事务提交的时候,再把`binlog cache`写到`binlog`文件中。
|
||||
binlog 的写入时机也非常简单,事务执行过程中,先把日志写到`binlog cache`,事务提交的时候,再把`binlog cache`写到 binlog 文件中。
|
||||
|
||||
因为一个事务的`binlog`不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为`binlog cache`。
|
||||
因为一个事务的 binlog 不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为`binlog cache`。
|
||||
|
||||
我们可以通过`binlog_cache_size`参数控制单个线程 binlog cache 大小,如果存储内容超过了这个参数,就要暂存到磁盘(`Swap`)。
|
||||
|
||||
`binlog`日志刷盘流程如下
|
||||
binlog 日志刷盘流程如下
|
||||
|
||||

|
||||
|
||||
@ -272,57 +272,63 @@ MySQL830 mysql:8.0.32
|
||||
|
||||

|
||||
|
||||
在出现`IO`瓶颈的场景里,将`sync_binlog`设置成一个比较大的值,可以提升性能。
|
||||
在出现 IO 瓶颈的场景里,将`sync_binlog`设置成一个比较大的值,可以提升性能。
|
||||
|
||||
同样的,如果机器宕机,会丢失最近`N`个事务的`binlog`日志。
|
||||
同样的,如果机器宕机,会丢失最近`N`个事务的 binlog 日志。
|
||||
|
||||
## 两阶段提交
|
||||
|
||||
`redo log`(重做日志)让`InnoDB`存储引擎拥有了崩溃恢复能力。
|
||||
redo log(重做日志)让 InnoDB 存储引擎拥有了崩溃恢复能力。
|
||||
|
||||
`binlog`(归档日志)保证了`MySQL`集群架构的数据一致性。
|
||||
binlog(归档日志)保证了 MySQL 集群架构的数据一致性。
|
||||
|
||||
虽然它们都属于持久化的保证,但是侧重点不同。
|
||||
|
||||
在执行更新语句过程,会记录`redo log`与`binlog`两块日志,以基本的事务为单位,`redo log`在事务执行过程中可以不断写入,而`binlog`只有在提交事务时才写入,所以`redo log`与`binlog`的写入时机不一样。
|
||||
在执行更新语句过程,会记录 redo log 与 binlog 两块日志,以基本的事务为单位,redo log 在事务执行过程中可以不断写入,而 binlog 只有在提交事务时才写入,所以 redo log 与 binlog 的写入时机不一样。
|
||||
|
||||

|
||||
|
||||
回到正题,`redo log`与`binlog`两份日志之间的逻辑不一致,会出现什么问题?
|
||||
回到正题,redo log 与 binlog 两份日志之间的逻辑不一致,会出现什么问题?
|
||||
|
||||
我们以`update`语句为例,假设`id=2`的记录,字段`c`值是`0`,把字段`c`值更新成`1`,`SQL`语句为`update T set c=1 where id=2`。
|
||||
|
||||
假设执行过程中写完`redo log`日志后,`binlog`日志写期间发生了异常,会出现什么情况呢?
|
||||
假设执行过程中写完 redo log 日志后,binlog 日志写期间发生了异常,会出现什么情况呢?
|
||||
|
||||

|
||||
|
||||
由于`binlog`没写完就异常,这时候`binlog`里面没有对应的修改记录。因此,之后用`binlog`日志恢复数据时,就会少这一次更新,恢复出来的这一行`c`值是`0`,而原库因为`redo log`日志恢复,这一行`c`值是`1`,最终数据不一致。
|
||||
由于 binlog 没写完就异常,这时候 binlog 里面没有对应的修改记录。因此,之后用 binlog 日志恢复数据时,就会少这一次更新,恢复出来的这一行`c`值是`0`,而原库因为 redo log 日志恢复,这一行`c`值是`1`,最终数据不一致。
|
||||
|
||||

|
||||
|
||||
为了解决两份日志之间的逻辑一致问题,`InnoDB`存储引擎使用**两阶段提交**方案。
|
||||
为了解决两份日志之间的逻辑一致问题,InnoDB 存储引擎使用**两阶段提交**方案。
|
||||
|
||||
原理很简单,将`redo log`的写入拆成了两个步骤`prepare`和`commit`,这就是**两阶段提交**。
|
||||
原理很简单,将 redo log 的写入拆成了两个步骤`prepare`和`commit`,这就是**两阶段提交**。
|
||||
|
||||

|
||||
|
||||
使用**两阶段提交**后,写入`binlog`时发生异常也不会有影响,因为`MySQL`根据`redo log`日志恢复数据时,发现`redo log`还处于`prepare`阶段,并且没有对应`binlog`日志,就会回滚该事务。
|
||||
使用**两阶段提交**后,写入 binlog 时发生异常也不会有影响,因为 MySQL 根据 redo log 日志恢复数据时,发现 redo log 还处于`prepare`阶段,并且没有对应 binlog 日志,就会回滚该事务。
|
||||
|
||||

|
||||
|
||||
再看一个场景,`redo log`设置`commit`阶段发生异常,那会不会回滚事务呢?
|
||||
再看一个场景,redo log 设置`commit`阶段发生异常,那会不会回滚事务呢?
|
||||
|
||||

|
||||
|
||||
并不会回滚事务,它会执行上图框住的逻辑,虽然`redo log`是处于`prepare`阶段,但是能通过事务`id`找到对应的`binlog`日志,所以`MySQL`认为是完整的,就会提交事务恢复数据。
|
||||
并不会回滚事务,它会执行上图框住的逻辑,虽然 redo log 是处于`prepare`阶段,但是能通过事务`id`找到对应的 binlog 日志,所以 MySQL 认为是完整的,就会提交事务恢复数据。
|
||||
|
||||
## undo log
|
||||
|
||||
> 这部分内容为 JavaGuide 的补充:
|
||||
|
||||
我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的操作进行**回滚**,在 MySQL 中,恢复机制是通过 **回滚日志(undo log)** 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用 **回滚日志** 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。
|
||||
每一个事务对数据的修改都会被记录到 undo log ,当执行事务过程中出现错误或者需要执行回滚操作的话,MySQL 可以利用 undo log 将数据恢复到事务开始之前的状态。
|
||||
|
||||
另外,`MVCC` 的实现依赖于:**隐藏字段、Read View、undo log**。在内部实现中,`InnoDB` 通过数据行的 `DB_TRX_ID` 和 `Read View` 来判断数据的可见性,如不可见,则通过数据行的 `DB_ROLL_PTR` 找到 `undo log` 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 `Read View` 之前已经提交的修改和该事务本身做的修改
|
||||
undo log 属于逻辑日志,记录的是 SQL 语句,比如说事务执行一条 DELETE 语句,那 undo log 就会记录一条相对应的 INSERT 语句。同时,undo log 的信息也会被记录到 redo log 中,因为 undo log 也要实现持久性保护。并且,undo-log 本身是会被删除清理的,例如 INSERT 操作,在事务提交之后就可以清除掉了;UPDATE/DELETE 操作在事务提交不会立即删除,会加入 history list,由后台线程 purge 进行清理。
|
||||
|
||||
undo log 是采用 segment(段)的方式来记录的,每个 undo 操作在记录的时候占用一个 **undo log segment**(undo 日志段),undo log segment 包含在 **rollback segment**(回滚段)中。事务开始时,需要为其分配一个 rollback segment。每个 rollback segment 有 1024 个 undo log segment,这有助于管理多个并发事务的回滚需求。
|
||||
|
||||
通常情况下, **rollback segment header**(通常在回滚段的第一个页)负责管理 rollback segment。rollback segment header 是 rollback segment 的一部分,通常在回滚段的第一个页。**history list** 是 rollback segment header 的一部分,它的主要作用是记录所有已经提交但还没有被清理(purge)的事务的 undo log。这个列表使得 purge 线程能够找到并清理那些不再需要的 undo log 记录。
|
||||
|
||||
另外,`MVCC` 的实现依赖于:**隐藏字段、Read View、undo log**。在内部实现中,InnoDB 通过数据行的 `DB_TRX_ID` 和 `Read View` 来判断数据的可见性,如不可见,则通过数据行的 `DB_ROLL_PTR` 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 `Read View` 之前已经提交的修改和该事务本身做的修改
|
||||
|
||||
## 总结
|
||||
|
||||
@ -330,7 +336,7 @@ MySQL830 mysql:8.0.32
|
||||
|
||||
MySQL InnoDB 引擎使用 **redo log(重做日志)** 保证事务的**持久性**,使用 **undo log(回滚日志)** 来保证事务的**原子性**。
|
||||
|
||||
`MySQL`数据库的**数据备份、主备、主主、主从**都离不开`binlog`,需要依靠`binlog`来同步数据,保证数据一致性。
|
||||
MySQL 数据库的**数据备份、主备、主主、主从**都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性。
|
||||
|
||||
## 参考
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user