认识InnoDB事务实现
首先,我们知道事务具有ACID四个特性。也即:原子性,一致性,隔离性,持久性。
这四个性质我们只需要知道事务保证了一系列的操作要么全部执行,要么一个也不执行,同时一旦事务提交,则其所做的修改会永久保存到数据库即可。
接下来我们一起看看InnoDB怎么实现的事务。
ACD三个特性是通过Redo log(重做日志)和Undo log 实现的。 而隔离性是通过锁来实现的。由于隔离性和锁在之前的文章讲过了。所以本文重点关注Redo log 和Undo log。
一、Redo log
重做日志用来实现事务的持久性,即D特性。它由两部分组成:
①内存中的重做日志缓冲
②重做日志文件
一看有内存和磁盘上的两个对应文件,我们就知道这样做一定是为了效率考虑,因为内存的读写效率要比磁盘读写效率高太多。
innodb是支持事务的存储引擎,在事务提交时,必须先将该事务的所有日志写入到redo日志文件中,待事务的commit操作完成才算整个事务操作完成。在每次将redo log buffer写入redo log file后,都需要调用一次fsync操作,因为重做日志缓冲只是把内容先写入操作系统的缓冲系统中,并没有确保直接写入到磁盘上,所以必须进行一次fsync操作。因此,磁盘的性能在一定程度上也决定了事务提交的性能。
关于fsync这个操作用户是可以干预的,因为每次提交事务都执行一次fsync,确实影响数据库性能。通过innodb_flush_log_at_trx_commit来控制redo log刷新到磁盘的策略。该参数的默认值为1,表示每次提交事务时都执行一次fsync操作。0则表示事务提交时不进行写入重做日志文件,这个写入操作由master thread进程来完成,master thread每一秒会进行一次重做日志文件的fsync操作。2则表示事务提交时将重做日志写入重做日志文件,但仅写入文件系统的缓存中,并不进行fsync操作。用户可以通过设置0或者2啦提高事务提交的性能,也可以设置1来要求确保redo log是写入文件中的,总之三种方法各有利弊。
还有需要了解的是:
redo log buffer将内存中的log block刷新到磁盘是有一定的规则的:事务提交时(前面已经提到)、当log buffer中有一半的内存空间被使用时、log checkpoint时。
那接下来我们就需要看看redo log file存储的内容到底是什么了。
为了避免大家懵圈,不打算把存储格式一个一个细钻(我也没那实力,哈哈)。我们只需要知道他大致是怎么设计的就行了。这样,我们以后如果自己设计一个类似场景的产品,就完全可以借鉴它的设计思想啦。
好,开始:
在InnoDB存储引擎中,重做日志都是以512字节进行存储的,这意味着重做日志缓存、重做日志文件块都是以块block的方式进行保存的,称为重做日志块(redo log block)每块的大小512字节。由于重做日志快的大小和磁盘扇区大小一样,都是512字节,因此重做日志的写入可以保证原子性,不需要double write技术。
每个重做日志块的内容快除了日志记录本身之外,还由日志块头(log block header)及日志块尾(log block tailer)两部分组成。重做日志头一共占用12字节,重做日志尾占用8字节。这两部
在图中标注出来不用太过关注这几个字段的含义,因为他们对理解Redo log实现事务的机制没有太大影响,反而如果关注这些,容易让人看到这些大写字母的变量感到头晕。
ps:这些变量是维护log block状态的一些变量。比如表示log block当前占用量,当前redo block的第一个redo log开始位置等等。举个例子吧:
事务T1的重做日志1占用762字节,事务T2的重做日志占用100字节,。由于每个log block实际只能保存492字节,因此其在log buffer的情况应该如下图所示:
实现这个功能就是靠log block的头部的字段来实现的。好了,这不是我们关注的问题,讲这个只是为了满足大家的好奇心以及对这些变量的初步认识。
重做日志块中出去header和tailer的内容就是具体的redo log了。不同的数据库操作会有对应的重做日志格式。此外,由于InnoDB存储引擎的存储管理是基于页的,故其重做日志格式也是基于页的。虽然有着不同的重做日志格式,但他们有着通用的头部格式,如图:
通用的头部格式由一下3部分组成
redo_log_type重做日志类型
space: 表空间ID
page_no页的偏移量即页的位置
之后是redo log body ,根据重做日志类型的不对,会有不同的存储内容,例如,对于页上记录的插入和删除操作,分别对应的如图的格式(同样,不要细扣每一个字段的含义,这不是我们要抓的重点):
大体上的redo log结构介绍完了。在说从redo log file恢复之前,还要说一个LSN的概念,LSN是Log Sequence Number的缩写,其代表的是日志序列号,在InnoDB存储引擎中,LSN占用8个字节,并且单调递增。
LSN表示事务写入重做日志字节的总量。例如当前重做日志的LSN为1000,有一个事务T1写入了100字节的重做日志,那么LSN就变成1100,若又有事务T2写入200字节的重做日志,那么LSN就变为1300。
LSN不仅记录在重做日志中,还存在每个页中,在每个页的头部,有一个值FIL_PAGE_LSN,记录了该页的LSN,在页中,LSN表示该页最后刷新时LSN的大小。因为重做日志记录的是每个页的日志,因此页中的LSN可以判断页是否需要进行恢复操作。例如,页P1的LSN为10000,而数据库启动时,InnoDB检测到写入重做日志中的LSN为13000,并且事务已经提交,那么数据库需要进行恢复操作。将重做日志应用到P1页中,同样的,对于重做日志中LSN小于P1页的LSN,不需要进行重做,因为P1页中的LSN表示已经被刷新到该位置,在此位置之前的内容已经被成功的处理了。
接下来就是恢复操作了:
InnoDB存储引擎在启动时不管上次数据运行是否正常关闭,都会尝试进行恢复操作,因为重做日志记录的是物理日志(不要纠结这个),因此恢复的速度比逻辑日志,如二进制日志要快的多,于此同时,InnoDB存储引擎自身也对恢复进行了一定程度的优化,如顺序读取及并行应用重做日志,这样可以进一步提高数据库恢复的速度
由于checkpoint表示已经刷新到磁盘页上的LSN,因此在恢复过程中仅需恢复checkpoint开始的日志部分。对于图中的例子,当数据库在checkpoint的LSN为10 000时发生宕机,恢复操作仅恢复LSN 10000~13000范围内的日志。
物理日志
举个例子,对于Insert操作,物理日志记录的是每个页的变化:
若执行SQL语句:
其记录的重做日志大致类似这个样子:
二、Undo log
第二部分是Undo log,它可以实现如下两个功能:
1.实现回滚
2.实验MVCC
undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。
当执行回滚时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。有时候应用到行版本控制的时候,也是通过undo log来实现的:当读取的某一行被其他事务锁定时,它可以从undo log中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。后面我们举一个具体的例子。例子来自此文。
下面演示对事务对某行记录的更新过程:
在演示之前,补充
Innodb为每行记录都实现了三个隐藏字段,用来实现MVCC:
1. 初始数据行
F1~F6是某行列的名字,1~6是其对应的数据。后面三个隐含字段分别对应该行的事务号和回滚指针,假如这条数据是刚INSERT的,可以认为ID为1,其他两个字段为空。
2.事务1更改该行的各字段的值
当事务1更改该行的值时,会进行如下操作:
用排他锁锁定该行
记录redo log
把该行修改前的值Copy到undo log,即上图中下面的行
修改当前行的值,填写事务编号,使回滚指针指向undo log中的修改前的行。
3.事务2修改该行的值
与事务1相同,此时undo log,中有有两行记录,并且通过回滚指针连在一起。
这些通过回滚指针联系起来的行相当于是数据的多个快照,从而实现MVCC的一致性非锁定读了。
注意:如果undo log一直不删除,则会通过当前记录的回滚指针回溯到该行创建时的初始内容,所幸的时在Innodb中存在purge线程,它会查询那些比现在最老的活动事务还早的undo log,并删除它们,从而保证undo log文件不至于无限增长。