MySQL持久化和回滚该怎么理解

61次阅读
没有评论

共计 7810 个字符,预计需要花费 20 分钟才能阅读完成。

这篇文章跟大家分析一下“MySQL 持久化和回滚该怎么理解”。内容详细易懂,对“MySQL 持久化和回滚该怎么理解”感兴趣的朋友可以跟着丸趣 TV 小编的思路慢慢深入来阅读一下,希望阅读后能够对大家有所帮助。下面跟着丸趣 TV 小编一起深入学习“MySQL 持久化和回滚该怎么理解”的知识吧。

redo log

事务的支持是数据库区分文件系统的重要特征之一,事务的四大特性:

原子性:所有的操作要么都做,要么都不做,不可分割。

一致性:数据库从一种状态变成另一种状态的的结果最终是一致的,比如 A 给 B 转账 500,A 最终少了 500,B 最终多了 500,但是 A + B 的值始终没变。

隔离性:事务和事务之前相互隔离,互不干扰。

持久性:事务一旦提交,它对数据的变更是永久性的。

本篇文章主要说说持久性相关的知识。

当我们在事务中更新一条记录的时候,比如:

update user set age=11 where user_id=1;

它的流程大概是这样的:

先判断 user_id 这条数据所在的页是否在内存里,如果不在的话,先从数据库读取到,然后加载到内存中

修改内存中的 age 为 11

写入 redo log,并且 redo log 处于 prepare 状态

写入 binlog

提交事务,redo log 变成 commit 状态

这里面有几个关键的点:redo log 是什么?为什么需要 redo log?prepare 状态的 redo log 是什么?redo log 和 binlog 是否可以只选其一 …? 带着这一系列的问题,我们来揭开 redo log 的面纱。

为什么要先更新内存数据,不直接更新磁盘数据?

我们为什么不每次更新数据的时候,直接更新对应的磁盘数据?首先我们知道磁盘 IO 是缓慢的,内存是快速的,两者的速度不是一个量级的,那么针对缓慢的磁盘 IO,出现了索引,通过索引哪怕数据成百上千万我们依然可以在磁盘上很快速的找我们的数据,这就是索引的作用。但是索引也需要维护,并不是一成不变的,当我们插入一条新数据 A 的时候,由于这条数据要插入在已存在的数据 B 之后,那么就要移动 B 数据,让出一个位置给 A,这个有一定的开销。

更糟糕的是,本来要插入的页已经满了,那么就要申请一个新的页,然后挪一部分数据过去,这叫做页的分裂,这个开销更大。如果我们的 sql 变更是直接修改磁盘的数据,恰巧正好出现上面的问题,那么此时的效率就会很低,严重的话会造成超时,这也是上面更新的过程为什么先要加载对应的数据页到内存中,然后先更新内存中的数据的原因。对于 mysql 来说,所有的变更都必须先更新缓冲池中的数据,然后缓冲池中的脏页会以一定的频率被刷入磁盘(checkPoint 机制),通过缓冲池来优化 CPU 和磁盘之间的鸿沟,这样就可以保证整体的性能不会下降太快。

为什么需要 redo log?

缓冲池可以帮助我们消除 CPU 和磁盘之间的鸿沟,checkpoint 机制可以保证数据的最终落盘,然而由于 checkpoint 并不是每次变更的时候就触发的,而是 master 线程隔一段时间去处理的。所以最坏的情况就是刚写完缓冲池,数据库宕机了,那么这段数据就是丢失的,无法恢复。这样的话就不满足 ACID 中的 D,为了解决这种情况下的持久化问题,InnoDB 引擎的事务采用了 WAL 技术(Write-Ahead Logging),这种技术的思想就是先写日志,再写磁盘,只有日志写入成功,才算事务提交成功,这里的日志就是 redo log。当发生宕机且数据未刷到磁盘的时候,可以通过 redo log 来恢复,保证 ACID 中的 D,这就是 redo log 的作用。

redo log 是如何实现的?

redo log 的写入并不是直接写入磁盘的,redo log 也有缓冲区的,叫做 redo log buffer(重做日志缓冲),InnoDB 引擎会在写 redo log 的时候先写 redo log buffer,然后也是以一定的频率刷入到真正的 redo log 中,redo log buffer 一般不需要特别大,它只是一个临时的容器,master 线程会每秒将 redo log buffer 刷到 redo log 文件中,因此我们只要保证 redo log buffer 能够存下 1s 内的事务变更的数据量即可,以 mysql5.7.23 为例,这个默认是 16M。

mysql  show variables like  %innodb_log_buffer_size% 
+------------------------+----------+
| Variable_name | Value |
+------------------------+----------+
| innodb_log_buffer_size | 16777216 |
+------------------------+----------+

16M 的 buffer 足够应对大部分应用了,buffer 同步到 redo log 的策略主要有如下几个:

master 线程每秒将 buffer 刷到到 redo log 中

每个事务提交的时候会将 buffer 刷到 redo log 中

当 buffer 剩余空间小于 1 / 2 时,会被刷到 redo log 中

需要注意的是 redo log buffer 刷到 redo log 的过程并不是真正的刷到磁盘中去了,只是刷入到 os cache 中去,这是现代操作系统为了提高文件写入的效率做的一个优化,真正的写入会交给系统自己来决定(比如 os cache 足够大了)。那么对于 InnoDB 来说就存在一个问题,如果交给系统来 fsync,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小的)。针对这种情况,InnoDB 给出 innodb_flush_log_at_trx_commit 策略,让用户自己决定使用哪个。

mysql  show variables like  innodb_flush_log_at_trx_commit 
+--------------------------------+-------+
| Variable_name | Value |
+--------------------------------+-------+
| innodb_flush_log_at_trx_commit | 1 |
+--------------------------------+-------+

0:表示事务提交后,不进行 fsync,而是由 master 每隔 1s 进行一次重做日志的 fysnc

1:默认值,每次事务提交的时候同步进行 fsync

2:写入 os cache 后,交给操作系统自己决定什么时候 fsync

从 3 种刷入策略来说:

2 肯定是效率最高的,但是只要操作系统发生宕机,那么就会丢失 os cache 中的数据,这种情况下无法满足 ACID 中的 D

0 的话,是一种折中的做法,它的 IO 效率理论是高于 1 的,低于 2 的,它的数据安全性理论是要低于 1 的,高于 2 的,这种策略也有丢失数据的风险,也无法保证 D。

1 是默认值,可以保证 D,数据绝对不会丢失,但是效率最差的。个人建议使用默认值,虽然操作系统宕机的概率理论小于数据库宕机的概率,但是一般既然使用了事务,那么数据的安全应该是相对来说更重要些。

redo log 是对页的物理修改,第 x 页的第 x 位置修改成 xx,比如:

page(2,4),offset 64,value 2

在 InnoDB 引擎中,redo log 都是以 512 字节为单位进行存储的,每个存储的单位我们称之为 redo log block(重做日志块),若一个页中存储的日志量大于 512 字节,那么就需要逻辑上切割成多个 block 进行存储。

一个 redo log block 是由日志头、日志体、日志尾组成。日志头占用 12 字节,日志尾占用 8 字节,所以一个 block 真正能存储的数据就是 512-12-8=492 字节。

多个 redo log block 组成了我们的 redo log。

每个 redo log 默认大小为 48M:

mysql  show variables like  innodb_log_file_size 
+----------------------+----------+
| Variable_name | Value |
+----------------------+----------+
| innodb_log_file_size | 50331648 |
+----------------------+----------+

InnoDB 默认 2 个 redo log 组成一个 log 组,真正工作的就是这个 log 组。

mysql  show variables like  innodb_log_files_in_group 
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| innodb_log_files_in_group | 2 |
+---------------------------+-------+
#ib_logfile0
#ib_logfile1

当 ib_logfile0 写完之后,会写 ib_logfile1,当 ib_logfile1 写完之后,会重新写 ib_logfile0…,就这样一直不停的循环写。

为什么一个 block 设计成 512 字节?

这个和磁盘的扇区有关,机械磁盘默认的扇区就是 512 字节,如果你要写入的数据大于 512 字节,那么要写入的扇区肯定不止一个,这时就要涉及到盘片的转动,找到下一个扇区,假设现在需要写入两个扇区 A 和 B,如果扇区 A 写入成功,而扇区 B 写入失败,那么就会出现非原子性的写入,而如果每次只写入和扇区的大小一样的 512 字节,那么每次的写入都是原子性的。

为什么要两段式提交?

从上文我们知道,事务的提交要先写 redo log(prepare),再写 binlog,最后再提交 (commit)。这里为什么要有个 prepare 的动作?redo log 直接 commit 状态不行吗?假设 redo log 直接提交,在写 binlog 的时候,发生了 crash,这时 binlog 就没有对应的数据,那么所有依靠 binlog 来恢复数据的 slave,就没有对应的数据,导致主从不一致。

所以需要通过两段式(2pc)提交来保证 redo log 和 binlog 的一致性是非常有必要的。具体的步骤是:处于 prepare 状态的 redo log,会记录 2PC 的 XID,binlog 写入后也会记录 2PC 的 XID,同时会在 redo log 上打上 commit 标识。

redo log 和 bin log 是否可以只需要其中一个?

不可以。redo log 本身大小是固定的,在写满之后,会重头开始写,会覆盖老数据,因为 redo log 无法保存所有数据,所以在主从模式下,想要通过 redo log 来同步数据给从库是行不通的。那么 binlog 是一定需要的,binlog 是 mysql 的 server 层产生的,和存储引擎无关,binglog 又叫归档日志,当一个 binlog file 写满之后,会写入到一个新的 binlog file 中。

所以我们是不是只需要 binlog 就行了?redo log 可以不需要?当然也不行,redo log 的作用是提供 crash-safe 的能力,首先对于一个数据的修改,是先修改缓冲池中的数据页的,这时修改的数据并没有真正的落盘,这主要是因为磁盘的离散读写能力效率低,真正落盘的工作交给 master 线程定期来处理,好处就是 master 可以一次性把多个修改一起写入磁盘。

那么此时就有一个问题,当事务 commit 之后,数据在缓冲区的脏页中,还没来的及刷入磁盘,此时数据库发生了崩溃,那么这条 commit 的数据即使在数据库恢复后,也无法还原,并不能满足 ACID 中的 D,然后就有了 redo log,从流程来看,一个事务的提交必须保证 redo log 的写入成功,只有 redo log 写入成功才算事务提交成功,redo log 大部分情况是顺序写的磁盘,所以它的效率要高很多。当 commit 后发生 crash 的情况下,我们可以通过 redo log 来恢复数据,这也是为什么需要 redo log 的原因。

但是事务的提交也需要 binlog 的写入成功,那为什么不可以通过 binlog 来恢复未落盘的数据?这是因为 binlog 不知道哪些数据落盘了,所以不知道哪些数据需要恢复。对于 redo log 而言,在数据落盘后对应的 redo log 中的数据会被删除,那么在数据库重启后,只要把 redo log 中剩下的数据都恢复就行了。

crash 后是如何恢复的?

通过两段式提交我们知道 redo log 和 binlog 在各个阶段会被打上 prepare 或者 commit 的标识,同时还会记录事务的 XID,有了这些数据,在数据库重启的时候,会先去 redo log 里检查所有的事务,如果 redo log 的事务处于 commit 状态,那么说明在 commit 后发生了 crash,此时直接把 redo log 的数据恢复就行了,如果 redo log 是 prepare 状态,那么说明 commit 之前发生了 crash,此时 binlog 的状态决定了当前事务的状态,如果 binlog 中有对应的 XID,说明 binlog 已经写入成功,只是没来的及提交,此时再次执行 commit 就行了,如果 binlog 中找不到对应的 XID,说明 binlog 没写入成功就 crash 了,那么此时应该执行回滚。

undo log

redo log 是事务持久性的保证,undo log 是事务原子性的保证。在事务中更新数据的前置操作其实是要先写入一个 undo log 中的,所以它的流程大致如下:

什么情况下会生成 undo log?

undo log 的作用就是 mvcc(多版本控制)和回滚,我们这里主要说回滚,当我们在事务里 insert、update、delete 某些数据的时候,就会产生对应的 undo log,当我们执行回滚时,通过 undo log 就可以回到事务开始的样子。需要注意的是回滚并不是修改的物理页,而是逻辑的恢复到最初的样子,比如一个数据 A,在事务里被你修改成 B,但是此时有另一个事务已经把它修改成了 C,如果回滚直接修改数据页把数据改成 A,那么 C 就被覆盖了。

对于 InnoDB 引擎来说,每个行记录除了记录本身的数据之外,还有几个隐藏的列:

DB_ROW_ID:如果没有为表显式的定义主键,并且表中也没有定义唯一索引,那么 InnoDB 会自动为表添加一个 row_id 的隐藏列作为主键。

DB_TRX_ID:每个事务都会分配一个事务 ID,当对某条记录发生变更时,就会将这个事务的事务 ID 写入 trx_id 中。

DB_ROLL_PTR:回滚指针,本质上就是指向 undo log 的指针。

当我们执行 INSERT 时:

begin;
INSERT INTO user (name) VALUES (tom)

插入的数据都会生一条 insert undo log,并且数据的回滚指针会指向它。undo log 会记录 undo log 的序号、插入主键的列和值 …,那么在进行 rollback 的时候,通过主键直接把对应的数据删除即可。

对于更新的操作会产生 update undo log,并且会分更新主键的和不更新的主键的,假设现在执行:

UPDATE user SET name= Sun  WHERE id=1;

这时会把老的记录写入新的 undo log,让回滚指针指向新的 undo log,它的 undo no 是 1,并且新的 undo log 会指向老的 undo log(undo no=0)。

假设现在执行:

UPDATE user SET id=2 WHERE id=1;

对于更新主键的操作,会先把原来的数据 deletemark 标识打开,这时并没有真正的删除数据,真正的删除会交给清理线程去判断,然后在后面插入一条新的数据,新的数据也会产生 undo log,并且 undo log 的序号会递增。

可以发现每次对数据的变更都会产生一个 undo log,当一条记录被变更多次时,那么就会产生多条 undo log,undo log 记录的是变更前的日志,并且每个 undo log 的序号是递增的,那么当要回滚的时候,按照序号依次向前推,就可以找到我们的原始数据了。

undo log 是如何回滚的?

以上面的例子来说,假设执行 rollback,那么对应的流程应该是这样:

通过 undo no= 3 的日志把 id= 2 的数据删除

通过 undo no= 2 的日志把 id= 1 的数据的 deletemark 还原成 0

通过 undo no= 1 的日志把 id= 1 的数据的 name 还原成 Tom

通过 undo no= 0 的日志把 id= 1 的数据删除

undo log 存在什么地方?

InnoDB 对 undo log 的管理采用段的方式,也就是回滚段,每个回滚段记录了 1024 个 undo log segment,InnoDB 引擎默认支持 128 个回滚段

mysql  show variables like  innodb_undo_logs 
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_undo_logs | 128 |
+------------------+-------+

那么能支持的最大并发事务就是 128*1024。每个 undo log segment 就像维护一个有 1024 个元素的数组。

当我们开启个事务需要写 undo log 的时候,就得先去 undo log segment 中去找到一个空闲的位置,当有空位的时候,就会去申请 undo 页,最后会在这个申请到的 undo 页中进行 undo log 的写入。我们知道 mysql 默认一页的大小是 16k。

mysql  show variables like  %innodb_page_size% 
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+

那么为一个事务就分配一个页,其实是非常浪费的(除非你的事物非常长),假设你的应用的 TPS 为 1000,那么 1s 就需要 1000 个页,大概需要 16M 的存储,1 分钟大概需要 1G 的存储 …,如果照这样下去除非 mysql 清理的非常勤快,否则随着时间的推移,磁盘空间会增长的非常快,而且很多空间都是浪费的。

于是 undo 页就被设计的可以重用了,当事务提交时,并不会立刻删除 undo 页,因为重用,这个 undo 页它可能不干净了,所以这个 undo 页可能混杂着其他事务的 undo log。undo log 在 commit 后,会被放到一个链表中,然后判断 undo 页的使用空间是否小于 3 /4,如果小于 3 / 4 的话,则表示当前的 undo 页可以被重用,那么它就不会被回收,其他事务的 undo log 可以记录在当前 undo 页的后面。由于 undo log 是离散的,所以清理对应的磁盘空间时,效率不是那么高。

关于 MySQL 持久化和回滚该怎么理解就分享到这里啦,希望上述内容能够让大家有所提升。如果想要学习更多知识,请大家多多留意丸趣 TV 小编的更新。谢谢大家关注一下丸趣 TV 网站!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-15发表,共计7810字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)