mysql的一个update需要经历什么最终持久化到磁盘?
我们执行一个普通的update语句时,mysql底层会做些什么最终将数据持久化到磁盘呢?
疑问?
mysql中执行更新操作时,必然涉及到读、写内存、写磁盘的操作流程。mysql是通过什么样的操作去完成更新流程的优化?
1. update语句执行流程
update T set c = c+1 where id = 2;
- 执行器先调用存储引擎的接口获取“id=2”的数据行。如果这一行所在的数据页在内存中,则存储引擎直接返回给执行器;否则需要存储引擎先去磁盘中获取数据,读取到内存中,然后再返回。
- 执行器拿到存储引擎返回的这行数据,对其进行更新操作,将c的值加+1,得到新的数据,在调用存储引擎接口,写入这行数据。
- 存储引擎收到执行器写入的这行数据的新结果,先将这条更新记录保存在内存(Buffer Pool)中,并将这条更新记录写入
redo log Buffer
,更新redo log的状态为prepare,随后向执行器返回结果。 - 执行器知道存储引擎已经将这条更新记录成功写入redo log Buffer(内存)后。
- 当SQL事务提交时,redo log 调用fsync写入磁盘(默认策略是事务提交写入磁盘)。
- 当SQL事务提交时,binlog调用fsync写入磁盘(默认策略是事务提交写入磁盘)。
- 在执行器写入binlog成功后,存储引擎将redo log的状态更新为commit,此时才算SQL事务正在提交成功;
2. redo log详解
2.1 为什么要引入redo log
为了减少与磁盘的IO交互,在对数据库增改删操作时,实际主要都是针对内存里的Buffer Pool中的数据进行的。
最详细的MySQL事务特性及原理讲解!(一)
理解Mysql中的Buffer pool
InnoDB作为mysql的存储引擎,数据是存放在磁盘中的,但是每次读写数据需要磁盘IO,效率就很低。为此,InnoDB提供了缓存(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:
数据库执行流程.png- 读取数据时,首先从Buffer Pool中读取,如果Buffer Pool中没有,则加载磁盘中的数据到Buffer Pool中;
- 写入数据的时候,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘(这一过程被称为刷脏)。
Buffer Pool的使用大大提高了读写数据的效率,但是也带来了新的问题:如果mysql宕机,如何保证数据不丢失?
2.2 redo log如何保证数据不丢失?
在修改数据时,除了修改Buffer Pool中的数据,还会在redo log Buffer(内存)记录这次操作。当事务提交时,会调用fsync对redo log(磁盘)进行刷盘。重启时可以读取磁盘上的redo log中的数据,对数据库进行恢复。redo log采用的是WAL技术(Write-ahead logging,预写式日志)。
2.3 WAL技术?
2.3.1 简介
目的:传统磁盘的顺序访问性能远好于随机访问,利用顺序写Log来记录对数据库的操作,并在故障恢复后通过Log恢复数据库到正确的状态;
算法:采用的是ARIES算法来实现,Log同时记录的是Redo和Undo的信息。原因就是BufferPool可以采用steal/no force的方式进行刷盘。
2.3.2 简述
由于传统磁盘顺序访问性能远好于随机访问,采用Logging的故障恢复机制意图利用顺序写的Log来记录对数据库的操作,并在故障恢复后通过Log内容将数据库恢复到正确的状态。简单来说,每次修改内容前先顺序写对应的log,同时为了保证恢复时可以从Log中看到最新的数据库状态,要求Log先于数据内容落盘,也就是常说的Write Ahead Log,WAL。
除此之外,事务完成Commit前还需要在Log中记录对应的Commit标记,以便恢复是了解当时的事务状态,因此还需要关注Commit标记和事务中数据落盘顺序。根据Log中记录的内容可以分为三类:Undo-Only、Redo-Only、Redo-Undo。
1、Undo Only Logging
Redo Log中记录Log记录可以表示为<T,X,v>:事务T修改了X的值,X的旧值是v。
事务提交时,需要强制Flush(将BufferPool的数据落盘)。以此保证Commit标记落盘前,对应事务的所有数据落盘,
落盘顺序:Log记录->Data->Commit标记。恢复时可以根据Commit标记判断事务的状态,并通过Undo Log中记录的旧值将未提交事务的修改回滚。
- Durability of Updates(持久性保证):Data强制刷盘,已经Commit的事务由于Data都已经在Commit标记前落盘,因此会一直存在;
- Failure Atomic(原子性保证):Undo log内容保证,失败事务的已刷盘的修改会在恢复阶段通过Undo log日志回滚,不在可见。
Undo-Only不能解决Page内并发的事务,例如两个事务的修改落到一个Page中,一个事务提交前需要强制Flush操作,会导致同Page所有事务的Data落盘,可能会早于对应的Log项从而损害WAL,同时也会导致关键路径上过于频繁的磁盘随机访问。
注:BufferPool的刷盘策略采用的是force;
2. Redo-Only Logging
不同于Undo-Only,采用Redo-Only的Log记录的是修改后的新值。对应的,Commit需要保证:Log中的Commit标记在事务的任何数据之前落盘,即落盘顺序:Log记录->Commit标记->Data。恢复时同样根据Commit标记判断事务状态,并通过Redo log中新值将已经Commit但是没有落盘的事务修改重放。
- Durability of Updates(持久性保证):Redo log内容保证,已提交事务但未刷盘的修改,利用Redo log中的内容重放,之后可见;
- Failure Atomic(原子性保证):阻止Commit前Data落盘保证,失败事务的修改不会出现在磁盘上,自然不可见。
Redo-Only同样不能有Page内并发问题,Page中多个不同事务,只要有一个未提交就不能刷盘,这些数据全部都要维护在内存中,造成较大的内存压力。
3. Redo-Undo Logging
可以看出只有Undo或者Redo问题,主要来自对Commit标记以及Data落盘顺序的限制,而这种限制来源于:Log信息中对新值或旧值的缺失。因此Redo-Undo采用同时记录新值和旧值方式,来消除Commit和Data之间刷盘顺序的限制。
- Durability of Updates(持久性保证):Redo log内容保证,已提交事务但未刷盘的修改,利用Redo log中的内容重放,之后可见;
- Failure Atomic(原子性保证):Undo log内容保证,失败事务的已刷盘的修改会在恢复阶段通过Undo log日志回滚,不在可见。
如此一来:同Page的不同事务提交会变得非常简单。同时可以将连续的数据攒着进行批量刷盘已利用磁盘较高的顺序读写能力。
2.3.3 BufferPool的Force和Steal
从上面看出:Redo和Undo内容分别可以保证Durability和Atomic两个特性,其中一种信息的缺失需要用严格的刷盘顺序来弥补。这里关注刷盘顺序包含两个维度:
- Force or No-Force:Commit时是否需要强制刷盘,采用Force的方式由于所有已提交事务数据一定已经存在于磁盘,自然而然地保证了Durability;
- No-Steal or Steal:Commit前数据能否提前刷盘,采用No-Steal的方式由于保证事务提交前修改不会出现在磁盘上,自然而然保证了Atomic。
总结一下,实现Durability可以通过记录Redo信息或要求Force刷盘顺序,实现Atomic需要记录Undo信息或要求No-Steal刷盘顺序,组合得到如下四种模式,如下图所示:
image.png2.3.4 ARIES算法
InnoDB采用的ARIES算法,ARIES本质是一种Redo-Undo的WAL实现。
Normal过程:
- 修改数据之前先追加Log记录,Log内容同时包括Redo和Undo信息,每个日志记录产生LSN(Log Sequence Number),来标记日志中的位置;
- 数据Page记录最后修改的日志项LSN,以此判断Page中内容的新旧程度,实现幂等。
- 故障恢复阶段需要通过Log中的内容恢复数据库状态,为了减少恢复时需要处理的日志量,ARIES会在正常运行期间周期性的生成Checkpoint,Checkpoint中除了当前日志LSN外,还需要记录当前活跃事务的最新LSN,以及所有脏页,供恢复时决定重放Redo的开始位置。需要注意的是:由于生成checkpoint时数据库还在正常提供服务(Fuzzy checkpoint),其中记录的活跃事务以及Dirty Page信息不一定准确,因此需要Recovery阶段通过Log内容进行修正。
为什么需要checkpoint?
WAL有一个显著的问题,随着系统运行时间越长,log会变得越来越长,导致每次crash之后,dbms需要对整个log进行恢复操作,所以dbms定期会做checkpoint,将当前在内存中所有数据全部刷到磁盘,则下次恢复只需要从最新的checkpoint开始即可。
Recover过程:
故障恢复包括三个阶段:Analysis,Redo和Undo。
- Analysis:主要是利用Checkpoint及Log中的信息确认后续Redo和Undo阶段的操作范围,通过Log修正Checkpoint中记录的Dirty Page集合信息,并用其中涉及最小的LSN位置作为下一步Redo的开始位置RedoLSN。同时修正Checkpoint中记录的活跃事务集合(未提交事务),作为Undo过程的回滚对象;
- Redo阶段:从Analysis获得的RedoLSN出发,重放所有的Log中的Redo内容,注意这里也包含了未Commit事务;
- Undo阶段对所有未提交事务利用Undo信息进行回滚,通过Log的PrevLSN可以顺序找到事务所有需要回滚的修改。
2.4 redo log也是写磁盘,比BufferPool写入磁盘优点是什么?
redo log也需要在事务提交时(默认策略)将日志写入磁盘,但是它要比Buffer Pool中修改的数据写入磁盘(即刷脏)要快:
- 刷脏是随机IO,每次修改数据位置都是随机,写redo log是追加操作,属于顺序IO;
- 刷脏是以数据页(Page)为单位,Mysql默认页大小为16KB,一个Page上一个小修改都要整页写入,而redo log中是精简的日志数据,无效IO大大减少。
2.5 redo log刷盘规则
当事务提交时,需要先将事务日志写入redo log Buffer
中,这些写入redo log Buffer
的日志并不是随着事务的提交立刻刷新到redo log 文件
中,而是有一定的规则,从而保证了 Redo Log 文件中数据的持久性。这种刷盘规则可以由innodb_flush_log_at_trx_commit
变量控制,它的取值:
- 0:每次提交事务时,不会将 Log Buffer 中的日志写入 OS buffer, 而是通过一个单独的线程,每秒写入 OS buffer 并调用系统的 fsync() 函数写入磁盘的 Redo Log File, 这种方式不是实时写磁盘的, 而是每隔 1s 写一次日志,如果系统崩溃,可能会丢失 1s 的数据。
- 1(默认):每次提交事务都会将 Log Buffer 中的日志写入 OS buffer 中,并且会调用 fsync() 函数将日志写入 Redo Log File 中,这种方式虽然不会再崩溃时丢失数据,但是性能比较差。也是这个变量的默认值。
- 2:每次提交事务时,都只是将数据写入 os buffer 中,之后每隔 1s ,通过 fsync() 函数将 os buffer 中的数据写入 Redo Log 文件中。
2.6 Redo 的整体流程
mysql_redo.png- 先将原始数据从磁盘中读入到内存(Buffer Pool)中,修改内存拷贝;
- 生成redo log并写入
redo log buffer(内存)
,记录的是数据被修改后的值; - 当事务commit时,将redo log中的内容刷新到
redo log file(磁盘)
,对redo log file采用追加写的方式; - 定期将内存(Buffer Pool)中修改的值刷新到磁盘;
2.7 redo log的两阶段提交
redo log 采用是两阶段提交的方式最终commit,那么为什么采用两阶段提交的方式?
看上面的流程图,mysql在写redo log
两阶段变更时会写bin log日志表。而记录binlog日志的目的:既可以用于数据恢复、binlog数据监听、主从库同步。那么redo log
表采用两阶段提交的目的在于:保证binlog 和redo log文件的一致性。
若不采用两阶段提交:
- 先写redo log在写binlog
如果引擎写完redo log后,bin log还没有写。异常重启。主库使用redo log 日志将数据恢复。但binlog没有记录这个语句,那么从库根据binlog同步数据时依旧没有这条语句,造成了主从库的数据不一致性;
- 先写binlog在写redo log
写完binlog后异常重启,因为redo log没有些,主库恢复后没有这条事务。但是由于binlog中有这条记录,从库根据binlog日志同步数据时,也会有这条事务。依旧导致主从不一致。
3. redo log 和binlog
3.1 redo log和binlog的区别
- redo log是InnoDB存储引擎层面,而binlog是mysql server层面,所有存储引擎均可使用;
- redo log是InnoDB为了解决
crash safe
(系统崩溃后恢复),而binlog是定期存档,重要的作用是支持主从同步。 - redo log是循环写,空间满时就会发生写覆盖;binlog是追加写,不会覆盖。
- bin log属于逻辑日志,因为没有涉及在具体哪一个page上进行修改;redo log属于物理逻辑日志(Physiological Logging),虽然有很多人认为其属于物理日志。
3.2 为什么redo log具有crash-safe能力,而binlog没有
redo log:是一个固定大小,“循环写”的日志文件,记录物理逻辑日志;
binlog:是一个无限大小,“追加写”的日志文件,记录的是逻辑日志;
redo log只会记录未刷盘的日志,已经刷入磁盘的数据都会从redo log这个固定大小的日志文件里删除;binlog是追加日志,保存的是全量的日志。
当数据库crash崩溃后,想要恢复:未刷盘但已经写入redo log和binlog的数据到内存时,binlog是无法实现的。虽然binlog拥有全量日志,但是没有标志让InnoDB判断哪些数据已经刷盘。
但redo log不一样,只要写入磁盘的数据,都会从redo log中抹除,数据库重启后,直接将redo log的数据恢复到内存。
3.3 未提交的事务日志也在Redo log中
因为不同事务的日志在Redo log是交叉存在的,所以没有办法把未提交的事务与已提交的事务分开。ARIES的做法是:不管事务有没有提交,它的日志都会被记录在Redo log上并刷到磁盘中。当崩溃的时候,会把Redo log全部回放一遍,然后再把未提交的事务给找出来,做回滚处理。
4. 写磁盘flush操作
InnoDB执行update更新操作是采用的“先写日志,在写磁盘”的策略。更新后的行数据本身先缓存在内存中,直将缩略的关键信息写入到redo log磁盘。但缓存在内存中的数据最终总是要写入到磁盘,这个操作叫做flush。
当内存数据页和磁盘数据页不一致的时候,称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。flush操作也就是“刷脏页”。
4.1 触发数据库执行flush操作的四种情况:
4.1.1 当InnoDB的redo log写满时
此时系统会停止所有的更新操作,将环形的redo log中的“读指针”向前推,对应的所有脏页此时都会flush到磁盘上。
4.1.2 当系统的内存不足时
当需要新的内存页,但是内存不够用时,就需要淘汰一些内存页(一般是空出最长时间没有被访问的内存页),此时如果淘汰的是脏页,就需要先将脏页写到磁盘。
4.1.3 当mysql认为系统“空闲”时
mysql会在运行期间“见缝插针”的找机会刷一点脏页,以避免当读写业务繁忙时过快的占满系统内存或redo log空间。
4.1.4 当mysql正常关闭时
此时mysql会把内存中所有脏页都flush到磁盘上,这样mysql下次启动的时候就直接从磁盘上读取数据,启动速度更快(相比即从磁盘读取数据,又从磁盘读取redo log日志)。