一文了解InnoDB事务实现原理

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

转载声明:转载请注明出处,本技术博客是本人原创文章

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> 一文了解InnoDB事务实现原理

本文准备通俗的讲解MySQL的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 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字节。这两部分是固定的。故每个重做日志块实际可以存储的大小为492字节(512-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语句: `INSERT INTO t SELECT 1,2;` 其记录的重做日志大致类似这个样子: `page(2,3),offset 32,value 1,2`

二、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:

  • 6字节的事务ID(DB_TRX_ID ,每处理一个事务,其值自动+1。
  • 7字节的回滚指针(DB_ROLL_PTR),指向写到rollback segment(回滚段)的一条undo log记录。
  • 隐藏的ID

1. 初始数据行

F1~F6是某行列的名字,1~6是其对应的数据。后面三个隐含字段分别对应该行的事务号和回滚指针,假如这条数据是刚INSERT的,可以认为ID为1,其他两个字段为空。

2.事务1更改该行的各字段的值

当事务1更改该行的值时,会进行如下操作:

  • 用排他锁锁定该行
  • 记录redo log
  • 把该行修改前的值Copy(可以理解成Copy,不要纠结前面说反向更新这里说复制,原理是一样的)到undo log,即上图中下面的行
  • 修改当前行的值,填写事务编号,使回滚指针指向undo log中的修改前的行。
  • 3.事务2修改该行的值

    与事务1相同,此时undo log,中有有两行记录,并且通过回滚指针连在一起。

    这些通过回滚指针联系起来的行相当于是数据的多个快照,从而实现MVCC的一致性非锁定读了。

    具体规则如下:

    InnoDB的MVCC,是通过上面我们说的每行纪录后面隐藏的列来实现的。他们保存了行的创建时间和行的过期时间(或删除时间),当然存储的并不是实际的时间值,而是系统版本号。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行纪录的版本号进行比较。在REPEATABLE READ隔离级别下,MVCC具体的操作如下:

    SELECT
    InnoDB会根据以下两个条件检查每行纪录:

  • InnoDB只查找版本早于当前事务版本的数据行,即,行的系统版本号小于或等于事务的系统版本号,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
  • 行的删除版本,要么未定义,要么大于当前事务版本号。这样可以确保事务读取到的行,在事务开始之前未被删除。 只有符合上述两个条件的纪录,才能作为查询结果返回。

  • INSERT

  • InnoDB为插入的每一行保存当前系统版本号作为行版本号。
  • DELETE

  • InnoDB为删除的每一行保存当前系统版本号作为行删除标识。
  • UPDATE

  • InnoDB为插入一行新纪录,保存当前系统版本号作为行版本号,同时,保存当前系统版本号到原来的行作为行删除标识。
  • 读到这里,也许会有一个疑问,考虑如下执行序列:

    按照之前的Select规则,会话B 的事务是在 会话A的后面开启的,那么B的事务版本号大于A的事务版本号。这样在A中插入的数据在未提交的情况下,B可以读到A修改的数据,这不就自相矛盾了么?

    其实不是,InnoDB通过read view来确定一致性读时的数据库snapshot,InnoDB的read view确定一条记录能否看到,有两条法则 :
    1 看不到read view创建时刻以后启动的事务
    2 看不到read view创建时活跃的事务

    对于Session A,start transaction时并没有创建read view,而是在update语句才创建。所以Session A 的read view创建时间要比Session B的晚。所以B是不会看到A的操作的。因此防止了不可重复读。

    两条法则原文描述如下: **Rule 1:** When the read view object is created it notes down the smallest transaction identifier that is not yet used as a transaction identifier (trx_sys_t::max_trx_id).   The read view calls it the low limit. So the transaction using the read view must not see any transaction with identifier greater than or equal to this low limit.

    **Rule 2:** The transaction using the read view must not see a transaction that was active when the read view was created. **补充:**如果undo log一直不删除,则会通过当前记录的回滚指针回溯到该行创建时的初始内容,所幸的时在Innodb中存在purge线程,它会查询那些比现在最老的活动事务还早的undo log,并删除它们,从而保证undo log文件不至于无限增长。

    Rule 2: The transaction using the read view must not see a transaction that was active when the read view was created.

    本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    转载声明:转载请注明出处,本技术博客是本人原创文章

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> 一文了解InnoDB事务实现原理


     上一篇
    深入理解MySql的Explain 深入理解MySql的Explain
    本文系投稿,作者:UncleMoveBrick个人主页:https://blog.csdn.net/UncleMoveBrick explain关键字可以模拟优化器执行SQL语句,从而知道MySQL是 如何处理你的SQL语句的。分析你的
    下一篇 
    一文了解Mysql的InnoDB存储引擎中的锁 一文了解Mysql的InnoDB存储引擎中的锁
    本文主要内容:介绍InnoDB中的锁的类型(X、S、IX、IS)。解释为什么引入意向锁行锁的三种算法:Record Lock,Gap Lock,Next-key Lock 一、InnoDB存储引擎中的锁锁,在现实生活中是为我们想要隐藏于外界