image-20230812200524132

前言

mysql底层是如何保证事务里的数据不丢失的?没错,就是通过redo log。

注意,绝对的数据不丢失是做不到的。我们所谈论的redo log,是针对事务而言。我们只要能保证事务中的数据不丢就行,说简单点就是:redo保证了事务的原子性和持久性

简单解释下:

原子性:要么都做了,要么都没做。不要只做一半。比如两个人互相转账,不允许出现一个人转了,而另一个人没收到钱的情况。也就是两条update要么全部执行,要么都不执行。

持久性:数据写到磁盘了。当然这个说法不严谨,比如硬盘坏了呢?为了好理解,我就这样写了!
redo是什么?

首先明确,redo是InnoDB引擎特有的。记录着事务里对数据的修改。

在mysql中,如果修改了数据,那么事务提交前,首先会被记录成redo日志写入磁盘,并不会去更新bufferpool或者数据库,只有等到事务提交时,才会根据redolog把新数据写入磁盘。这也就是经常说的WAL(Write-Ahead Logging)。

为什么要这么操作?

主要还是考虑到性能的问题,记录redolog是追加写的方式,是顺序IO。而直接更新数据库B+树,用的是随机的IO。(顺序IO就是按顺序地找,随机IO就是数据随机分布在磁盘,得来回寻址)

redo的好处

那么为什么需要redo呢?

1.保证数据的持久性。众所周知,数据是记录在磁盘上的,如果事务提交后mysql突然挂了(比如断电之类的),那么内存里的数据,是不是就丢失了?我们数据恢复时候,通过redo 就可以恢复回来。

解释:因为我们在事务提交前都已经把相关日志写入了磁盘,所以碰到意外当然可以恢复。如果写redo的时候断电了呢?大致上可以认为事务回滚了。不过没这么简单,到底是回滚还是提交,还牵扯到bin log。我们下文会讲到。

2.redo只是记录了对数据的修改,数据会比一页数据小得多。大大减少了IO频率

解释:显而易见吧,一条redo再怎么也不会有16k那么大吧!

当然,写redo并不是一条条写的。因为一条redo日志并不是写入的最小单位,一般来说至少都是几条一起写。这是因为一条修改语句对B+树的修改,绝大多数情况下都不会是只产生一个修改点,比如你要插入一条数据,叶子节点容不下了,那么就可能页分裂,同时还会更新许多内节点的信息。所以这一系列对底层B+树的操作,都会以一组的形式去写入磁盘

具体的写入过程下文会讲。现在只需要知道,一个事务会包含许多SQL,而一条SQL修改语句,会产生很多组redo就行了,而这一组(学名:Mini-Transaction)才是写入的最小单元。

好奇的你可能又会问:那这一组也没法保证写入的原子性啊,写一半断电了怎么办呢?

是的,写入永远没法保证原子性,所以只能在读取的时候保证了。在一组redo日志里,结尾会有一个标志符。解析redo日志的时候,读不到这个标志符的话,这一组就不解析了。不过这个Mini-Transaction并不是为了保证事务,我个人觉得是为了保证那颗B+树不会被搞坏了。

3.redo日志的写入是顺序IO。而修改磁盘的B+树是随机IO。

解释:redo就是往一块硬盘的某个相邻的区域写就完事了。如果要直接去修改B+树,很可能这些页并不相邻,寻址会很慢的。当然,固态硬盘的随机IO会好很多。

image-20230813160121954

redo的流程

image-20230813161311655

binlog适用于维护集群内数据的一致性,而redo log用于崩溃恢复

binlog主要用于数据备份,数据同步,redolog主要是用于保证数据的持久化特性,当数据库崩溃的时候,还可以通过redolog来恢复未完成的数据,保证数据的完整性。还可以通过合理的配置,如redolog的大小来恢复MySQL的性能

image-20230813152159490

为什么要写redo log,不写redo log的话,根本就不会出现“两阶段提交”的麻烦事啊?

先说结论:在于崩溃恢复。

MySQL为了提升性能,引入了BufferPool缓冲池。查询数据时,先从BufferPool中查询,查询不到则从磁盘加载在BufferPool。

每次对数据的更新,也不总是实时刷新到磁盘,而是先同步到BufferPool中,涉及到的数据页就会变成脏页。

同时会启动后台线程,异步地将脏页刷新到磁盘中,来完成BufferPool与磁盘的数据同步。

如果在某个时间,MySQL突然崩溃,则内存中的BufferPool就会丢失,剩余未同步的数据就会直接消失。

虽然在更新BufferPool后,也写入了binlog中,但binlog并不具备crash-safe的能力。

因为崩溃可能发生在写binlog后,刷脏前。在主从同步的情况下,从节点会拿到多出来的一条binlog。

所以server层的binlog是不支持崩溃恢复的,只是支持误删数据恢复。InnoDB考虑到这一点,自己实现了redo log。

为什么要写两次redo log,写一次不行吗?

先不谈到底写几次redo log合适,如果只写一次redo log会有什么样的问题呢?

redo log与binlog都写一次的话,也就是存在以下两种情况:

先写binlog,再写redo log

当前事务提交后,写入binlog成功,之后主节点崩溃。在主节点重启后,由于没有写入redo log,因此不会恢复该条数据。

而从节点依据binlog在本地回放后,会相对于主节点多出来一条数据,从而产生主从不一致。

先写redo log,再写binlog

当前事务提交后,写入redo log成功,之后主节点崩溃。在主节点重启后,主节点利用redo log进行恢复,就会相对于从节点多出来一条数据,造成主从数据不一致。

因此,只写一次redo log与binlog,无法保证这两种日志在事务提交后的一致性。

也就是无法保证主节点崩溃恢复与从节点本地回放数据的一致性。

在两阶段提交的情况下,是怎么实现崩溃恢复的呢?

首先比较重要的一点是,在写入redo log时,会顺便记录XID,即当前事务id。在写入binlog时,也会写入XID。

如果在写入redo log之前崩溃,那么此时redo log与binlog中都没有,是一致的情况,崩溃也无所谓。

如果在写入redo log prepare阶段后立马崩溃,之后会在崩恢复时,由于redo log没有被标记为commit。于是拿着redo log中的XID去binlog中查找,此时肯定是找不到的,那么执行回滚操作。

如果在写入binlog后立马崩溃,在恢复时,由redo log中的XID可以找到对应的binlog,这个时候直接提交即可。

总的来说,在崩溃恢复后,只要redo log不是处于commit阶段,那么就拿着redo log中的XID去binlog中寻找,找得到就提交,否则就回滚。

在这样的机制下,两阶段提交能在崩溃恢复时,能够对提交中断的事务进行补偿,来确保redo log与binlog的数据一致性。