23、MySQL 教程 - MySQL 事务日志~redo log保证事务的持久性

redo log保证事务的持久性

01. 事务的四大特性

事务有4种特性:原子性、一致性、隔离性和持久性。那么事务的四种特性到底是基于什么机制实现呢?

事务的隔离性由锁机制实现

事务的原子性、一致性和持久性由事务的 redo 日志和 undo 日志来保证

redo log称为重做日志 ,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性。

undo log称为回滚日志 ,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。

redo log是存储引擎层生成的日志,记录的是物理级别上的页修改操作,比如页号xxx,偏移量yyy,写入了zzz数据,主要是为了保证数据的可靠性。

undo log是存储引擎层生成的日志,记录的是逻辑操作的日志,比如对某一行数据进行了insert语句操作,那么undo log就记录一条与之相反的delete操作。主要用于事务的回滚和一致性非锁定读(mvcc)。

02. 什么是 Buffer Pool

InnoDB存储引擎在处理客户端的请求时,如果需要访问某个页的数据,就会把完整的页中的数据全部加载到内存中,即使只访问页中的一条记录,也需要先把整个页的数据加载到内存中。将整个页加载到内存后就可以进行读写访问了,而且在读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,这样将来有请求再次访问该页面时,就可以剩下磁盘IO的开销了。

为了缓存磁盘中的页,Innodb在MySQL服务器启动时就像操作系统申请了一片连续的内存,即Buffer Pool(缓冲池)。默认情况下,Buffer Pool的大小为128M。

Buffer Pool对应的一片连续的内存被划分为若干个页面,页面大小与Innodb表空间用的页面大小一致,默认都是16kb,为了与磁盘中的页面区分开来,我们把这些Buffer Pool中的页面称为缓冲页。

当我们修改了Buffer Pool中某个缓冲页的数据,它就与磁盘上的页不一致了,这样的缓冲页称为脏页。当然,我们可以每当修改完某个数据页时,就立即将其刷新到磁盘中对应的页上,但是频繁的往磁盘中写数据会严重的影响程序的性能,所以每次修改缓冲页后,我们并不着急立即将修改刷新到磁盘上,而是在某个时间点进行刷新。

后台有专门的线程负责每隔一段时间就把脏页刷新到磁盘,这样就可以不影响用户线程处理正常的请求。

总结:InnoDB存储引擎是以页为单位来管理存储空间的,在真正访问页面之前,需要先把磁盘中的页加载到内存中的Buffer Pool中,之后才可以访问,所有的变更都必须先更新缓冲池中的数据,然后缓冲池中的脏页以一定的频率刷新到磁盘(checkpoint机制),通过缓冲池来优化CPU和磁盘之间的鸿沟,这样就能保证整体的性能不会下降的太快。

03. 什么是 redo log

根据事务的持久性,对于一个已经提交的事务,在事务提交之后即使系统发生了崩溃,这个事务对数据库所在的更改也不能丢失。

在真正访问页面之前,需要先把磁盘中的页加载到内存中的Buffer Pool中,之后才可以访问。如果我们只在内存中的Buffer Pool中修改了页面,假设在事务提交之后突然发生了某个故障,导致内存中的数据都失效了,就无法保证事务的持久性了。

那么如何保证事务的持久性呢?

一个简单的做法就是在事务提交完成之前,把该事务修改的所有页面都刷新到磁盘,但是这个粗暴的做法存在下面这些问题:

(1) 刷新一个完整的数据页太浪费时间了,有时我们仅仅修改了某个页面中的一个字节,但是由于InnoDB是以页为单位进行磁盘IO的,也就是事务提交时不得不将一个完整的页面刷新到磁盘,太浪费了。
(2) 随机IO刷新的比较慢,一个事务可能包含很多语句,即使是一条语句也可能修改许多页面,而且这些页面可能并不相邻。这就意味着在将某个事务修改的Buffer Pool中的页面刷新到磁盘时,需要进行很多的随机IO。

我们只是想让已经提交了的事务对数据库中的数据所在的修改能永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复过来。所以,其实没有必要在每次提交事务时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把修改的内容记录下来即可。

比如,某个事务将系统表空间中第10号 页面中偏移量为 100 处的那个字节的值 1 改成 2 。我们只需要记录一下:

将第0号表 空间的10号页面的偏移量为100处的值更新为 2

这样在事务提交时,就会把上述的内容刷新到磁盘中,即使之后系统崩溃了,重启之后只要按照上述内容所记录的步骤重新更新一下数据页,那么该事务对数据库中所做的修改就可以被恢复过来,这样就能满足持久性的要求了。

因为在系统因崩溃而重启时需要按照上述内容所记录的步骤重新更新数据页,所以上述内容也称为重做日志

04. redo log 的好处

相较于在事务提交时将所有修改过的页刷新到磁盘中,只将该事务执行过程中产生的redo日志刷次年到磁盘,有下面的好处:

(1) redo日志降低了刷盘频率

(2) redo日志占用的空间非常小

(3) redo日志是顺序写入磁盘的。在执行事务的过程中,每执行一条语句,就可能产生若干条redo日志,这些日志是按照产生的顺序写入磁盘的,也就是顺序IO。

InnoDB存储引擎的事务采用了WAL技术(Write-Ahead Logging),这种技术就是先写日志,再写磁盘,只有日志写入成功,才算事务提交成功,这里的日志就是redo log。当发生宕机且数据未刷新到磁盘的时候,可以通过redo log恢复过来,保证事务的持久性。

redo log跟bin log的区别:redo log是存储引擎层产生的,而bin log是数据库层产生的。假设一个事务,对表做了10万行的记录插入,在这个过程中,一直不断的往redo log顺序记录,而bin log不会记录,直到这个事务提交,才会一次写入bin log文件中。

05. redo log 的组成

  • 重做日志缓冲区(redo log buffer):保存在内存中,是易丢失的。InnoDB为了解决磁盘速度过慢的问题而引入了Buffer Pool,同理,写入redo log日志时,也不能直接写到磁盘中,实际上在服务器启动时就向操作系统申请了一片称为redo log buffer的连续存储空间。
  • 重做日志文件 (redo log file) :保存在硬盘中,是持久的。redo日志总存放在内存redo log buffer中待着也不是个办法,在一些情况下他们会被刷新到磁盘中。mysql的数据目录下默认有名为ib_logfile0和ib_logfile1两个文件,log buffer中的日志在默认情况下就是刷新到这两个磁盘文件中,即redo log file。

06. redo log 刷盘时机

  • log buffer空间不足时:log buffer的大小是有限的,如果不停的向这个有限大小的log buffer中塞入日志,很快就会将它填满,设计InnoDB的大叔认为,如果当前写入log buffer的redo日志量已经占满了log buffer总容量的50%左右,就需要将这些日志刷新到磁盘中。
  • 事务提交时:之所以提出redo log的概念,主要是因为它占用的空间少,而且可以将其顺序写入磁盘,引入redo日志之后,虽然在事务提交时可以不把修改过的buffer pool页面立即刷新到磁盘,但是为了保持持久性,必须要把页面修改时所对应的redo日志刷新到磁盘,否则系统崩溃后,无法将该事务对页面所做的修改恢复过来。
  • 将某个脏页刷新到磁盘前,会先保证该脏页对应的redo日志刷新到磁盘中:redo日志是顺序写入的,因此在将某个脏页对应的redo日志从redo log buffer刷新到磁盘中时,也会保证将在其之前产生的redo日志也刷新到磁盘中。
  • 后台有一个线程,大约以每秒一次的频率将redo log buffer中的redo日志刷新到磁盘中
  • 做checkpoint时

07. redo log 的整体流程

以一个更新事务为例,redo log 流转过程,如下图所示:
 

第1步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝 。

第2步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值 。

第3步:当事务commit时,将redo log buffer中的内容刷新到 redo log file,对 redo log file采用追加写的方式 。

第4步:定期将内存中修改的数据刷新到磁盘中。

Write-Ahead Log(预先日志持久化):在持久化一个数据页之前,先将内存中相应的日志页持久化。

08. redo log 的刷盘策略

redo log的写入并不是直接写入磁盘的,InnoDB引擎会在写redo log的时候先写redo log buffer,之后以 一 定的频率刷入到真正的redo log file 中。这里的一定频率怎么看待呢?这就是我们要说的刷盘策略。

redo log buffer刷盘到redo log file的过程并不是真正的刷到磁盘中去,只是刷入到文件系统缓存 (page cache)中去,真正的写入会交给系统自己来决定(比如page cache足够大了)。那么对于InnoDB来说就存在一个问题,如果交给系统来同 步,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小的)。

针对这种情况,InnoDB给出innodb_flush_log_at_trx_commit参数,该参数控制 commit提交事务时,如何将 redo log buffer中的日志刷新到 redo log file 中。它支持三种策略:

(1)设置为0 :表示每次事务提交时不进行刷盘操作。默认master thread每隔1s进行一次重做日志的同步。

(2)设置为1 :表示每次事务提交时都将进行同步,刷盘操作( 默认值 )

(3)设置为2 :表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步。由os自己决定什么时候同步到磁盘文件。

InnoDB存储引擎有一个后台线程,每隔1秒,就会把redo log buffer中的内容写到文件系统缓存(page cache),然后调用刷盘操作。
 

一个没有提交事务的redo log日志,也可能会刷盘,因为在事务执行过程redo log记录是会写入redo log buffer中,这些redo log记录会被后台线程刷盘。

show variables like 'innodb_flush_log_at_trx_commit';

 

可以看到刷盘策略默认为1。

09. redo log 的不同刷盘策略演示

(1)innodb_flush_log_at_trx_commit = 1
 
innodb_flush_log_at_trx_commit = 1时,redo log记录就一定在硬盘里,不会有任何数据丢失,如果在事务执行期间MySQL挂了或者宕机了,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失,可以保证ACID的D,数据绝对不会丢失,但是效率是最差的。

建议使用默认值,虽然操作系统宕机的概率理论小于数据库宕机的概率,但是一般既然使用了事务,那么树的安全性相对来说就更重要些。

(2)innodb_flush_log_at_trx_commit = 2
 
innodb_flush_log_at_trx_commit = 2时,只要事务提交成功,redo log buffer中的内容只写入文件系统缓存,如果只是MySQL挂了不会有任何数据丢失,但是操作系统宕机可能会有1秒数据的丢失,这种情况下无法满足ACID中的D,但是数值2是效率最高的。

(3)innodb_flush_log_at_trx_commit = 0
 
innodb_flush_log_at_trx_commit = 0时,后台线程每隔1秒进行一次重做日志的fsync操作,因此示例crash最多丢失1秒钟内的事务。后台线程是负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性。

10. redo log 写入redo log buffer的过程(了解)

MySQL把对底层页面进行一次原子访问的过程称为一个Mini-Trasaction (mtr),比如向B+树中插入一条记录的过程就算是一个Mini-Transaction。一个mtr可以包含一组redo 日志,在进行恢复时,需要将一组redo日志作为一个不可分割的整体来处理。

一个事务可以包含若干条语句,每一条语句又包含若干个mtr,每一个mtr又可以包含若干条redo log日志。每当一个mtr执行完成时,伴随该mtr生成的一组redo日志就需要被复制到redo log buffer中。
 
为了更好的管理redo日志,MySQL将通过mtr生成的redo日志都放在了大小为512字节的页中,这个用来存储redo日志的页称为redo log block。真正的redo log日志都存放在log block body中。
 
redo log写入redo log buffer :

向log buffer中写入redo log的过程是顺序的,也就是先往前面的block中写,当该block的空闲空间用完后再往下一个block中写。当我们向往log buffer中写入redo log时,第一个遇到的问题就是应该写在哪个block的哪个偏移量处,所以Innodb的设计者特意提供了一个称之为buffer_free的全局变量,该变量指明后续写入的redo日志应该写入到log buffer中的哪个位置,如图所示:
 
mtr运行过程中产生的一组redo日志在mtr结束时会被复制到redo log buffer中,可是这些日志总存放在内存中待着也不是个办法,在一些情况下他们会被刷新到磁盘中。

11. redo log file(了解)

1、相关参数设置:

  • innodb_log_group_home_dir :指定 redo log 文件组所在的路径,默认值为 ./ ,表示在数据库 的数据目录下。MySQL的默认数据目录( var/lib/mysql )下默认有两个名为 ib_logfile0和 ib_logfile1 的文件,log buffer中的日志默认情况下就是刷新到这两个磁盘文件中。此redo日志 文件位置还可以修改。
  • innodb_log_files_in_group:指明redo log file的个数,命名方式如:ib_logfile0,iblogfile1... iblogfilen。默认2个,最大100个。
     
  • innodb_flush_log_at_trx_commit:控制 redo log 刷新到磁盘的策略,默认为1。
     
  • innodb_log_file_size:单个 redo log 文件设置大小,默认值为 48M 。最大值为512G,注意最大值 指的是整个 redo log 系列文件之和,即(innodb_log_files_in_group * innodb_log_file_size )不能大 于最大值512G。
     

2、日志文件组:

磁盘中的redo日志文件不止一个,而是以一个日志文件组的形式出现的。这些文件以ib_logfile[数字](数字可以是0、1、2)的形式进行命名,每个redo日志文件大小都是一样的。

在将redo日志写入日志文件组时,是从ib_logfile0开始写,如果ib_logfile0写满了,就接着写ib_logfile1写。同理,ib_logfile1写满了就去写ib_logfile2,依次类推。如果写到最后一个文件咋办呢?那就重新转到ib_logfile0继续写,整个过程如下图所示:

总共的redo日志文件大小其实就是:innodb_log_file_size × innodb_log_files_in_group

采用循环使用的方式向redo日志文件组里写数据的话,会导致后写入的redo日志覆盖掉前边写的redo日 志?当然!所以InnoDB的设计者提出了checkpoint的概念。

3、checkpoint:

在整个在日志文件组中还有两个重要的属性,分别是write pos、checkpoint

  • write pos是当前记录的位置,一边写一边后移。
  • checkpoint是当前要擦除的位置,也是往后推移。
     

每次刷盘redo log记录到日志文件组中,write pos位置就会后移更新。每次MySQL加载日志文件组恢复数据时,会清空加载过的redo log记录,并把checkpoint后移更新。write pos和checkpoint之间的还空着的部分可以用来写入新的redo log记录。
 
如果write pos 追上 checkpoint ,表示日志文件组满了,这时候不能再写入新的 redo log记录,MySQL得停下来,清空一些记录,把 checkpoint 推进一下。
 
Innodb的更新操作采用的是write ahead log(预先日志持久化)策略,即先写日志,再写入磁盘。