admin管理员组

文章数量:1534194

InnoDB Architecture

https://dev.mysql/doc/refman/5.6/en/innodb-architecture.html

我们都知道数据库有四大属性ACID,即事务的原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。

原子性(Atomicity)

原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。 

一致性(Consistency)

事务必须使数据库从一个一致性状态变换到另外一个一致性状态。

隔离性(Isolation)

事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。

持久性(Durability)

持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

在事务处理的ACID属性中,一致性是最基本的属性,其它的三个属性都为了保证一致性而存在的。

首先回顾一下一致性的定义。所谓一致性,指的是数据处于一种有意义的状态,这种状态是语义上的而不是语法上的。最常见的例子是转帐。例如从帐户A转一笔钱到帐户B上,如果帐户A上的钱减少了,而帐户B上的钱却没有增加,那么我们认为此时数据处于不一致的状态。

在数据库实现的场景中,一致性可以分为数据库外部的一致性和数据库内部的一致性。前者由外部应用的编码来保证,即某个应用在执行转帐的数据库操作时,必须在同一个事务内部调用对帐户A和帐户B的操作。如果在这个层次出现错误,这不是数据库本身能够解决的,也不属于我们需要讨论的范围。后者由数据库来保证,即在同一个事务内部的一组操作必须全部执行成功(或者全部失败)。这就是事务处理的原子性(用Undo log来保证)。

但是,原子性并不能完全保证一致性。在多个事务并行进行的情况下,即使保证了每一个事务的原子性,仍然可能导致数据不一致的结果,比如丢失更新问题。

为了保证并发情况下的一致性,引入了隔离性,即保证每一个事务能够看到的数据总是一致的,就好象其它并发事务并不存在一样。用术语来说,就是多个事务并发执行后的状态,和它们串行执行后的状态是等价的。

01

重做日志(Redo  Log):用于保证事务的持久性,即事务ACID中的D。

实际上,它可以分为以下两种类型:

  • 物理重做日志

  • 逻辑重做日志

在InnoDB存储引擎中,在大多数情况下,Redo是一个物理日志,它记录数据页面的物理变化。但是,逻辑重做日志不会记录页面的实际修改,而是记录一种对页面的修改,例如新数据页面,需要记录逻辑日志。就逻辑重做日志而言,我们只需要记住,在大多数情况下,Redo是一个物理日志,而DML对页面的修改需要记录重做。

重做的角色

Redo日志的主要功能是用于数据库崩溃恢复

重做的组成

重做日志可以简单地分为以下两部分:

  • 一个是内存中的重做日志缓冲区,它是易失性的并且在内存中。

  • 其次,重做日志文件是持久的并存储在磁盘上。

我什么时候写重做?

上图仅反映了Redo的编写过程。在这里我们详细介绍了编写Redo的时间:

  • 完成数据页面的修改后,在脏页刷出磁盘之前写入重做日志。请注意,首先修改数据,然后写入日志。

  • 在数据页之前将Reo日志写回磁盘

  • 集群索引,二级索引和撤消页面修改都需要记录重做日志。

重做的整个过程

以更新事务为例,宏观地掌握重做日志流程,如下图所示:

  • 步骤1:首先将原始数据从磁盘读入内存,修改数据的内存副本

  • 步骤2:生成重做日志并将其写入重做日志缓冲区,该日志缓冲区记录数据的修改值

  • 步骤3:当事务提交时,将重做日志缓冲区的内容刷新到重做日志文件,并以另外的方式写入重做日志文件。

  • 步骤4:定期将内存中修改后的数据刷新到磁盘

重做如何确保交易持久性?

InnoDB是用于事务的存储引擎,Force Log at Commit机制为了实现事务持久性,也就是说,当提交事务时,重做日志缓冲区被写入重做日志文件以进行持久化,并且在提交之前它不会完成交易的操作完成。这种做法也称为预写日志(pre-log persistence)在保存数据页之前,将相应的日志页保留在内存中。

为确保每个日志都写入重做日志文件,每次重做缓冲区写入重做日志文件后,默认情况下,需要在 fsync 操作后调用InnoDB存储引擎。因为没有O_DIRECT选项(O_DIRECT选项是Linux系统中的一个选项。使用此选项后,文件直接进行IO操作并写入磁盘而无需文件系统缓存)可以打开重做日志,重做日志首先写入文件系统缓存。要确保将重做日志写入磁盘,必须执行fsync操作。

Fsync是一个系统调用操作,其效率取决于磁盘的性能,因此磁盘的性能也会影响事务提交的性能,即数据库的性能。

Force Log at Commit机制它取决于InnoDB存储引擎提供的参数。innodb_flush_log_at_trx_commit参数可以控制重做日志刷新到磁盘的策略。设置此参数值还可以允许用户设置非持久情况,如下所示:

  • 当参数设置为1(默认为1)时,表示在提交时必须调用一次事务。fsync操作,最安全的配置,保证持久性

  • 当参数设置为2时,仅在事务提交写入时执行操作仅保证重做日志缓冲区在没有fsync操作的情况下写入系统的页面缓存。因此,如果MySQL数据库关闭,事务不会丢失,但如果操作系统出现故障,事务可能会丢失。

  • 当参数设置为0时,意味着在提交事务时不会写入重做日志操作。此操作仅在主线程中执行,而重做日志的fsync操作在主线程中每秒执行一次,因此实例崩溃最多会在一秒内丢失事务。(主线程负责从缓冲池到磁盘的异步刷新数据,以确保数据一致性)

fsyncwriteOperations实际上是在许多持久性场景中使用的系统调用函数,例如Redis中的AOF持久性。fsync操作将数据提交到硬盘,强制硬盘同步,并在写入硬盘后返回。fsync操作有性能瓶颈,write操作在将数据写入系统的页面缓存后立即返回数据,然后按照:

用户缓冲区 > 页面缓存 > 磁盘 Disk

的顺序,通过系统的调度机制将缓存的数据刷到磁盘上磁盘。

内存中已修改的页面通常不会立即写回磁盘。相反,更改记录在磁盘上当前重做日志的末尾 - 这是您的ib_logfile0或ib_logfile1。该页面在内存中也标记为“脏”=写回磁盘。

在三种情况下,将脏页写入表空间:

1.组织为环形缓冲区的重做日志已满。为了释放一些空间,我们将以重做日志顺序写出脏页面,以便我们可以使重做日志环缓冲区的尾随指针前进一个空间。

2.Innodb_log_wait,将在同名的状态计数器中注册。

InnoDB需要来自InnoDB缓冲池的免费页面,但找不到。通常我们可以通过放弃未标记为脏的页面来释放缓冲池中的页面。当页面没有标记为脏时,其内容可以随时从磁盘重新加载,因此我们可以安全地将其放入内存中。但是当缓冲池只保存脏页时,这是不可能的,我们实际上必须将脏页刷新到磁盘才能将它们释放出来用于其他用途。

3.Innodb_buffer_pool_wait_free,将在同名的状态计数器中注册。InnoDB试图避免这种情况:每当超过innodb_max_dirty_pages_pct%许多页面被标记为脏时,强制检查点并写入脏页。InnoDB空闲时,每秒会向磁盘写出64页的批次。

除了上面提到的Force Log at Commit机制,它保证了事务持久性,重做日志的实现,实际上依赖于“迷你事务”(mini-transaction)。

Redo如何在InnoDB中工作?与迷你交易连接?

Redo的实现与mini-transaction密切相关,mini-transaction是InnoDB通过小型事务使用的一种机制。确保并发事务操作和数据库异常下的数据页中的数据一致性,但它不是业务。

为了确保数据页中的数据一致性,迷你事务必须遵循三个协议

  • FIX规则

  • 预写日志

  • 强制日志在提交

FIX规则

在修改数据页面时,我们需要获取页面的x-latch(独占锁定,(exclusive lock) ),并且在获取数据页面时,我们需要页面的s-latch(读锁或共享锁, (read lock or shared lock) )或x-latch,持有页面锁定,直到完成修改或访问页面的操作。

预写日志

即WAL,Write-Ahead Log。在持久化数据页面之前,必须保留内存中相应的日志页面。每个页面都有一个LSN(日志序列号),表示日志序列号(LSN占用8个字节,单调递增)。在需要将数据页写入持久性设备之前,需要首先将内存中小于LSN的日志写入持久性设备。

那你为什么要先写日志呢?您可以直接将数据写入磁盘而无需编写日志吗?原则上,它是可能的,但它会引起一些问题。数据修改将产生随机IO,但是日志是在IO,append模式(顺序模式)中顺序写入的,以便充分利用磁盘性能。

WAL提供三种持久化模式。最严格的是“full-sync”,保证在返回之前将记录刷新到磁盘, 从而使数据能够在系统级别的崩溃中幸存下来。

第二个级别是“write-only”,保证记录写入操作系统的文件中, 然后返回给用户, 允许数据在进程级别的崩溃后仍然幸存。

最后是“no-sync”,即将记录保存在内存的缓冲区中, 但不保证将其立即写入文件系统。

强制日志在提交

这是如何确保上述内容的持久性,这里再次总结,并回应上述内容。Write-Ahead Log可以修改事务中的多个页面,但不能保证单个数据页面的一致性。Force-log-at-commit要求在提交事务时,必须将其生成的所有迷你事务日志刷新到磁盘。如果日志刷新完成,则应将缓冲池中的页面刷新为持久存储设备之前的数据。当数据库关闭时,可以通过在数据库重新启动时进行日志记录来保证数据的完整性。

写重做日志的过程

上图显示了重做日志的写入过程。每个迷你事务对应于每个DML操作,例如更新语句,其由小事务保证。修改数据后,生成redo1。首先,redo1被写入迷你事务私有缓冲区。更新语句完成后,redo1将从专用缓冲区复制到公共日志缓冲区。提交整个外部事务时,会将重做日志缓冲区刷入重做日志文件。

参考:https://developpaper/analysis-of-redo-and-undo-in-mysql-transaction/

Redo Log 的数据接口很像一个“贪吃蛇”,咬住自己的尾巴:

 

redo log包括两部分:

一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;

二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。

innodb通过force log at commit机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log file和undo log file中进行持久化。

为了确保每次日志都能写入到事务日志文件中,在每次将log buffer中的日志写入日志文件的过程中都会调用一次操作系统的fsync操作(即fsync()系统调用)。

因为MariaDB/MySQL是工作在用户空间的,MariaDB/MySQL的log buffer处于用户空间的内存中。要写入到磁盘上的log file中(redo:ib_logfileN文件,undo:share tablespace或.ibd文件),中间还要经过操作系统内核空间的os buffer,调用fsync()的作用就是将OS buffer中的日志刷到磁盘上的log file中。

也就是说,从redo log buffer写日志到磁盘的redo log file中,过程如下: 

在InnoDB引擎中,redo log都是以512字节进行存储的(和磁盘扇区的大小一样,因此redo log写入可以保证原子性,不需要double write),也就是重做日志缓存和文件都是以块的方式进行保存的,称为redo log block,每个block占512字节。

重做日志除了日志本身之外,还由日志块头(log block header)及日志块尾(log block tailer)两部分组成。


解释一下组成Log Block header的4个部分各自的含义:

  • LOG_BLOCK_HDR_NO:它主要用来标记所处Redo Log Buffer中Log Block的位置。

  • LOG_BLOCK_HDR_DATA_LEN:它表示Log Block所占用的大小。当Log Block被写满时,该值为0x200,表示使用全部Log Block空间,即占用512字节。

  • LOG_BLOCK_FIRST_REC_GROUP:表示Log Block中第一个日志所在的偏移量,如果该值大小和LOG_BLOCK_HDR_DATA_LEN相同,则表示当前Log Block不包含新的日志,如果事务的日志大小超过一个Log Block的大小,剩余的将会接着保存到一个新的Log Block中。

  • LOG_BLOCK_CHECKPOINT_NO:表示该Log Block最后被写入时的检查点第4字节的值。

Log Block tailer只包含一个LOG_BLOCK_TRL_NO,它的值和LOG_BLOCK_HDR_NO相同,并在函数log_block_init中被初始化。


02


撤销日志 Undo Log: 确保原子性

重做日志记录了事务的行为,可以很好的通过其对页进行“重做”操作。但是事务有时候还需要进行回滚操作,也就是ACID中的A(原子性),这时就需要Undo log了。因此在数据库进行修改时,InnoDB存储引擎不但会产生Redo,还会产生一定量的Undo。这样如果用户执行的事务或语句由于某种原因失败了,又或者用户一条ROLLBACK语句请求回滚,就可以利用这些Undo信息将数据库回滚到修改之前的样子。

Undo log是InnoDB 

事务特性的重要组成部分。当我们对记录做了变更操作时就会产生Undo记录,Undo记录默认被记录到系统表空间(ibdata)中,但从5.6开始,也可以使用独立的Undo 表空间。

Undo记录中存储的是老版本数据,当一个旧的事务需要读取数据时,为了能读取到老版本的数据,需要顺着undo链找到满足其可见性的记录。当版本链很长时,通常可以认为这是个比较耗时的操作。

基本文件结构

为了保证事务并发操作时,在写各自的undo log时不产生冲突,InnoDB采用回滚段(Rollback Segment,简称Rseg)的方式来维护undo log的并发写入和持久化。回滚段实际上是一种 Undo 文件组织方式,每个回滚段又有多个undo log slot。具体的文件组织方式如下图所示:

上图展示了基本的Undo回滚段布局结构,其中:

  • rseg0预留在系统表空间ibdata中。

  • rseg 1~rseg 32 这32个回滚段存放于临时表的系统表空间中,用于临时表的undo。

  • rseg33~rseg 128 则根据配置(InnoDB >= 1.1默认128,可通过参数 innodb_undo_logs 设置)存放到独立undo表空间中(如果没有打开独立Undo表空间,则存放于ibdata中,独立表空间可以通过参数 innodb_undo_directory设置),用于普通事务的undo。

如图所示,每个回滚段维护了一个段头页,在该page中又划分了1024个slot(TRX_RSEG_N_SLOTS),每个slot又对应到一个undo log对象,因此理论上InnoDB最多支持 96 * 1024个普通事务。

Undo log的格式

在InnoDB引擎中,undo log分为:

  • insert undo log

  • update undo log

insert undo log是指在insert操作中产生的undo log,因为insert操作的记录,只对事务本身可见,对其他事务不可见(这是事务隔离性的要求),故该undo log可以在事务提交后直接删除,不需要进行purge操作。而update undo log记录的是delete和update操作产生的undo log。该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除,提交时放入undo log链表,等待purge线程进行最后的删除。下面是两种undo log的结构图:

purge

对于一条delete语句 delete from t where a = 1,如果列a有聚集索引,则不会进行真正的删除,而只是在主键列等于1的记录delete flag设置为1,即记录还是存在在B+树中。而对于update操作,不是直接对记录进行更新,而是标识旧记录为删除状态,然后新产生一条记录。那这些旧版本标识位删除的记录何时真正的删除?怎么删除?

其实InnoDB是通过undo日志来进行旧版本的删除操作的,在InnoDB内部,这个操作被称之为purge操作,原来在srv_master_thread主线程中完成,后来进行优化,开辟了purge线程进行purge操作,并且可以设置purge线程的数量。purge操作每10s进行一次。

为了节省存储空间,InnoDB存储引擎的undo log设计是这样的:一个页上允许多个事务的undo log存在。虽然这不代表事务在全局过程中提交的顺序,但是后面的事务产生的undo log总在最后。此外,InnoDB存储引擎还有一个history列表,它根据事务提交的顺序,将undo log进行连接:

在执行purge过程中,InnoDB存储引擎首先从history list中找到第一个需要被清理的记录,这里为trx1,清理之后InnoDB存储引擎会在trx1所在的Undo page中继续寻找是否存在可以被清理的记录,这里会找到事务trx3,接着找到trx5,但是发现trx5被其他事务所引用而不能清理,故再去history list中取查找,发现最尾端的记录时trx2,接着找到trx2所在的Undo page,依次把trx6、trx4清理,由于Undo page2中所有的记录都被清理了,因此该Undo page可以进行重用。

InnoDB存储引擎这种先从history list中找undo log,然后再从Undo page中找undo log的设计模式是为了避免大量随机读操作,从而提高purge的效率。

撤消日志是与单个读写事务关联的撤消日志记录的集合。撤消日志记录包含有关如何撤消事务到聚簇索引 记录的最新更改的信息。如果另一个事务需要将原始数据视为一致读取操作的一部分,则从撤消日志记录中检索未修改的数据。撤消日志存在于 撤消日志段中,这些日志段包含在 回滚段中。默认情况下,回滚段实际上是系统表空间的一部分 ,但它们也可以驻留在撤消表空间中。

InnoDB支持128个回滚段。该 innodb_rollback_segments变量定义了所使用的回滚段的数量 InnoDB

回滚段支持的事务数取决于回滚段中的撤消槽数和每个事务所需的撤消日志数。

回滚段中的撤消槽数根据InnoDB页面大小而不同。

InnoDB页面大小回滚段中的撤消插槽数(InnoDB页面大小/ 16)
4096 (4KB)256
8192 (8KB)512
16384 (16KB)1024

事务最多分配两个撤消日志,每个日志对应于以下每种操作类型:

  1. INSERT 操作

  2. UPDATE和 DELETE操作

根据需要分配撤消日志。例如,执行一个事务INSERT, UPDATE和 DELETE操作都分配了两个撤消日志。仅执行INSERT操作的事务将 分配一个撤消日志。撤消日志从也分配给事务的回滚段分配给事务。

分配给事务的撤消日志在其持续时间内仍与事务相关联。例如,分配给INSERT 操作事务的撤消日志用于该事务INSERT 执行的所有操作。

鉴于上述因素,可以使用以下公式来估计InnoDB能够支持的并发读写事务的数量。

注意:

在达到InnoDB能够支持的并发读写事务数之前,事务可能会遇到并发事务限制错误。分配给事务的回滚段用完撤消槽时会发生这种情况。在这种情况下,请尝试重新运行该事务。

  • 如果每个事务执行一个一个或 一个 一个 操作,那么能够支持的并发读写事务的数量 是: INSERT UPDATEDELETEInnoDB

    (innodb_page_size / 16) * innodb_rollback_segments

    如果每个事务执行一个 一个或一个或 一个 操作,那么能够支持的并发读写事务的数量 是: INSERT UPDATEDELETEInnoDB

    (innodb_page_size / 16 / 2) * innodb_rollback_segments

https://dev.mysql/doc/refman/5.6/en/innodb-undo-logs.html

03

检查点:Checkpoint

崩溃回复 crash recovery

前面提到了redo log是用来实现ACID的持久性的,也就是只要事务提交成功后,事务内的所有修改都会保存到数据库,哪怕这时候数据库crash了,也要有办法来进行恢复。也就是Crash Recovery。

说到恢复,我们先来了解一个概念:什么是LSN

LSN(log sequence number) 用于记录日志序号,它是一个不断递增的 unsigned long long 类型整数,占用8字节。它代表的含义有:

  • redo log写入的总量。

  • checkpoint的位置。

  • 页的版本,用来判断是否需要进行恢复操作。

checkpoint:它是redo log中的一个检查点,这个点之前的所有数据都已经刷新回磁盘,当DB crash后,通过对checkpoint之后的redo log进行恢复就可以了。

Log sequence number表示当前的LSN,Log flushed up to表示刷新到redo log文件的LSN,Last checkpoint at表示刷新到磁盘的LSN。如果把它们三个简写为 A、B、C 的话,它们的值的大小肯定为 A>=B>=C

InnoDB引擎在启动时不管上次数据库运行时是否正常关闭,都会进行恢复操作。因为重做日志记录的是物理日志,因此恢复的速度比逻辑日志,如二进制日志要快很多。恢复的时候只需要找到redo log的checkpoint进行恢复即可。

03

MVCC :多版本并发控制

MVCC在1981年的一篇论文"Concurrency Control in Distributed Database Systems"被充分论述,并成为经典描述。一般认为原创工作始于1978年David P. Reed的博士学位论文。

算法

MVCC使用时间戳 (TS), 或“自动增量的事务ID”实现“事务一致性”。MVCC可以确保每个事务(T)通常不必“读等待”数据库对象(P)。这通过对象有多个版本,每个版本有创建时间戳 与废止时间戳 (WTS)做到的。

事务Ti读取对象(P)时,只有比事务Ti的时间戳早,但是时间上最接近事务Ti的对象版本可见,且该版本应该没有被废止。

事务Ti写入对象P时,如果还有事务Tk要写入同一对象,则(Ti)必须早于(Tk),即 (Ti) < (Tk),才能成功。

MVCC可以无锁实现。

InnoDB存储引擎的行结构

InnoDB表数据的组织方式为主键聚簇索引,二级索引中采用的是(索引键值, 主键键值)的组合来唯一确定一条记录。

InnoDB表数据为主键聚簇索引,mysql默认为每个索引行添加了4个隐藏的字段,分别是:

  • DB_ROW_ID:InnoDB引擎中一个表只能有一个主键,用于聚簇索引,如果表没有定义主键会选择第一个非Null的唯一索引作为主键,如果还没有,生成一个隐藏的DB_ROW_ID作为主键构造聚簇索引。

  • DB_TRX_ID:最近更改该行数据的事务ID。

  • DB_ROLL_PTR:undo log的指针,用于记录之前历史数据在undo log中的位置。

  • DELETE BIT:索引删除标志,如果DB删除了一条数据,是优先通知索引将该标志位设置为1,然后通过(purge)清除线程去异步删除真实的数据。

整个MVCC的机制都是通过DB_TRX_ID,DB_ROLL_PTR这2个隐藏字段来实现的。

事务链表

当一个事务开始的时候,会将当前数据库中正在活跃的所有事务(执行begin,但是还没有commit的事务)保存到一个叫trx_sys的事务链表中,事务链表中保存的都是未提交的事务,当事务提交之后会从其中删除。

ReadView

有了前面隐藏列和事务链表的基础,接下去就可以构造MySQL实现MVCC的关键——ReadView。

ReadView说白了就是一个数据结构,在事务开始的时候会根据上面的事务链表构造一个ReadView,初始化方法如下:

// readview 初始化
// m_low_limit_id = trx_sys->max_trx_id;
// m_up_limit_id = !m_ids.empty() ? m_ids.front() : m_low_limit_id;
ReadView::ReadView()
    :
    m_low_limit_id(),
    m_up_limit_id(),
    m_creator_trx_id(),
    m_ids(),
    m_low_limit_no()
{
    ut_d(::memset(&m_view_list, 0x0, sizeof(m_view_list)));
}

总共做了以下几件事:

  1. 活跃事务链表(trx_sys)中事务id最大的值被赋值给m_low_limit_id

  2. 活跃事务链表中第一个值(也就是事务id最小)被赋值给m_up_limit_id

  3. m_ids 为事务链表。

通过该ReadView,新的事务可以根据查询到的所有活跃事务记录的事务ID来匹配能够看见该记录,从而实现数据库的事务隔离,主要逻辑如下:

  1. 通过聚簇索引的行结构中DB_TRX_ID隐藏字段可以知道最近被哪个事务ID修改过。

  2. 一个新的事务开始时会根据事务链表构造一个ReadView。

  3. 当前事务根据ReadView中的数据去跟检索到的每一条数据去校验,看看当前事务是不是能看到这条数据。

判断可见性的源码:

// 判断数据对应的聚簇索引中的事务id在这个readview中是否可见	
bool changes_visible(	
        trx_id_t        id, // 记录的id	
    const table_name_t& name) const	
MY_ATTRIBUTE((warn_unused_result))	
{	
    ut_ad(id > 0);	
    // 如果当前记录id < 事务链表的最小值或者等于创建该readview的id就是它自己,那么是可见的	
    if (id < m_up_limit_id || id == m_creator_trx_id) {	
        return(true);	
    }	

	
    check_trx_id_sanity(id, name);	
    // 如果该记录的事务id大于事务链表中的最大值,那么不可见	
    if (id >= m_low_limit_id) {	
        return(false);	
        // 如果事务链表是空的,那也是可见的	
    } else if (m_ids.empty()) {	
        return(true);	
    }	

	
    const ids_t::value_type*    p = m_ids.data();	

	
    //判断是否在ReadView中,如果在说明在创建ReadView时 此条记录还处于活跃状态则不应该查询到,否则说明创建ReadView是此条记录已经是不活跃状态则可以查询到	
    return(!std::binary_search(p, p + m_ids.size(), id));	
}

可见性判断逻辑:

  1. 当检索到的数据的事务ID小于事务链表中的最小值(数据行的DB_TRX_ID < m_up_limit_id)表示这个数据在当前事务开启前就已经被其他事务修改过了,所以是可见的。

  2. 当检索到的数据的事务ID表示的是当前事务自己修改的数据(数据行的DB_TRX_ID = m_creator_trx_id) 时,数据可见。

  3. 当检索到的数据的事务ID大于事务链表中的最大值(数据行的DB_TRX_ID >= m_low_limit_id) 表示这个数据在当前事务开启后到下一次查询之间又被其他的事务修改过,那么就是不可见的。

  4. 如果事务链表为空,那么也是可见的,也就是当前事务开始的时候,没有其他任意一个事务在执行。

  5. 当检索到的数据的事务ID在事务链表中的最小值和最大值之间,从m_low_limit_id到m_up_limit_id进行遍历,取出DB_ROLL_PTR指针所指向的回滚段的事务ID,把它赋值给 trx_id_current ,然后从步骤1重新开始判断,这样总能最后找到一个可用的记录。

RC和RR隔离级别ReadView的实现方式

我们知道,RC隔离级别是能看到其他事务提交后的修改记录的,也就是不可重复读,但是RR隔离级别完美的避免了,但是它们都是使用的MVCC机制,那又为何有两种截然不同的结果呢?其实我们看一下他们创建ReadView的区别就知道了。

  • 在RC事务隔离级别下,每次语句执行都关闭ReadView,然后重新创建一份ReadView。

  • 在RR下,事务开始后第一个读操作创建ReadView,一直到事务结束关闭。

上面的总结英文版为:With REPEATABLE READ isolation level, the snapshot is based on the time when the first read operation is performed. With READ COMMITTEDisolation level, the snapshot is reset to the time of each consistent read operation.

来源自MySQL官网:MySQL Glossary-glos_consistent_read

因为RC每次查询语句都创建一个新的ReadView,所以活跃的事务列表一直在变,也就导致如果事务B update提交了后事务A才进行查询,查询的结果就是最新的行,也就是不可重复读咯。而RR则一直用的事务开始时创建的ReadView。

https://benjaminwhx/2018/04/25/%E8%B0%88%E8%B0%88MySQL-InnoDB%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E4%BA%8B%E5%8A%A1%E7%9A%84ACID%E7%89%B9%E6%80%A7/

04

缓存管理

Double Write Buffer

Double Write Buffer 是 InnoDB 所使用的一种较为独特的文件 Flush 实现技术,也就是牺牲了一点点写性能,提高系统 Crash 或者断电情况下数据的安全性,避免写入的数据不完整。

在介绍 double write 的实现之前,有必要先了解一下 partial page write 问题。

问题起因

InnoDB 中的默认页大小是 16KB,通过 innodb_page_size 变量定义,很多的操作 (主要是对数据文件操作),如数据校验、写入磁盘等,也是以页为单位进行。

而计算机硬件和操作系统的原子操作通常小于该值,一般为 512 字节,也就意味着,在极端情况下(如宕机、断电、OS Crash 等),往往并不能保证写入页的原子性。

----- MySQL变量查看,数据写入页大小为16K
mysql> SHOW GLOBAL VARIABLES LIKE 'innodb_page_size';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.06 sec)

----- 查看文件系统的块大小,一般为4K
# getconf PAGESIZE
# blockdev --getbsz /dev/sda7
# dumpe2fs /dev/sda7 | grep "Block size"
dumpe2fs 1.42.9 (28-Dec-2013)
Block size:               4096

----- 查看sector的大小
# fdisk -l | grep Sector
Sector size (logical/physical): 512 bytes / 512 bytes

例如,16K 的数据,在写入 4K 时机器宕机,此时只有一部分写是成功的,这种情况下就是 partial page write 问题。

MySQL 在崩溃恢复阶段,读取数据页时,需要检查页的 checksum,当发生 partial page write 时,页已经损坏,就导致数据无法恢复。

为了解决上述问题,采用两次写,此时需要额外添加两个部分,A) 内存中的两次写缓冲 (double write buffer),大小为 2MB;B) 磁盘上共享表空间中连续的 128 页,大小也为 2MB。

配置参数

在 InnoDB 中,可以通过如下方式查看 double write 的状态。

------ 查看是否启用了double write,以及相关参数
mysql> SHOW VARIABLES LIKE 'innodb_doublewrite%';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| innodb_doublewrite            | ON    |
| innodb_doublewrite_batch_size | 120   |
+-------------------------------+-------+
2 rows in set (0.02 sec)

----- 可以查询double write的使用情况
mysql> SHOW STATUS LIKE 'innodb_dblwr_%';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Innodb_dblwr_pages_written | 14615 |   从BP写入到dblwr的page数
| Innodb_dblwr_writes        | 636   |   写文件的次数
+----------------------------+-------+
2 rows in set (0.02 sec)

如上可以得到平均每次写操作合并页数为 Innodb_dblwr_pages_written/Innodb_dblwr_writes 。

工作过程

工作过程大致如下:

  1. 当需要将缓冲池的脏页刷新到 data file 时,并不直接写到数据文件中,而是先拷贝至内存中的 double write buffer。

  2. 接着从 double write buffer 分两次写入磁盘共享表空间中,每次写入 1MB,并马上调用 fsync 函数,同步到磁盘,避免缓冲带来的问题。

  3. 第 2 步完成后,再将两次写缓冲区写入数据文件。

如下是执行示意图。

在这个过程中,第二步的 double write 是顺序写,所以开销并不大;而第三步,在将 double write buffer 写入各表空间文件,是离散写入;而 double write 实际引入的是第二步的开销。

恢复过程

有 double write 后,恢复时就简单多了,首先检查数据页,如果损坏,则尝试从 double write 中恢复数据;然后,检查 double writer 的数据的完整性,如果不完整直接丢弃,重新执行 redo log;如果 double write 的数据是完整的,用 double buffer 的数据更新该数据页,跳过该 redo log。

https://jin-yang.github.io/post/mysql-innodb-double-write-buffer.html

相关文章阅读:

The basics of the InnoDB undo logging and history system

Jeremy Cole

Geek, electronics nerd, database nerd, aviation nerd, father of three.

InnoDB implements multi-version concurrency control (MVCC), meaning that different users will see different versions of the data they are interacting with (sometimes called snapshots, which is a bit of a misleading term). This is done in order to allow users to see a consistent view of the system without expensive and performance-constraining locking which would limit concurrency. (This is where the “concurrency control” part of the term comes from; one alternative is locking everything the user may need.) Undo logging and InnoDB’s “history” system are the mechanisms that underly its implementation of MVCC, but the way this works is generally very poorly understood.

InnoDB keeps a copy of everything that is changed

The key thing to know in InnoDB’s implementation of MVCC is that when a record is modified, the current (“old”) version of the data being modified is first stashed away as an “undo record” in an “undo log”. It’s called an undo log because it contains the information necessary to undo the change made by the user, reverting the record to its previous version.

Every record contains a reference to its most recent undo record, called a rollback pointer or ROLL_PTR, and every undo record contains a reference to its previous undo record (except for an initial record insert, which can be undone by simply deleting the record), forming a chain of all previous versions of a record. In this way, any previous version of a record can be easily constructed, as long as the the undo records (the “history”) still exist in the undo logs.

Transactions always operate on the “live” data — there are no private copies

Any transaction1, no matter how small or temporary it may be, is always operating on the database. As records are added, modified, and deleted, this is done in the very same index structure that all other transactions and users are actively using. Although the data for these in-flight transactions may not be visible to other transactions (depending on their transaction isolation level), the effects—particularly the performance costs—associated with those modifications are immediately visible.

When reading an index, a transaction uses a “read view”, which controls what version of records a transaction is allowed to see. While reading records in the index, any recently modified record (modified by a transaction whose ID is newer than the reading transaction’s read view would allow it to see) must first be reverted to an old-enough version. (And this may cause the record to not be visible at all.)

When a transaction updates a record, without yet committing, all other transactions using transaction isolation are immediately impacted by having to revert the version of that record to an older version (that they are allowed to see) every time they encounter the record in a read.

What about transaction isolation levels?

There are three transaction isolation levels of interest for undo logging, history, and multi-versioning:

  • READ UNCOMMITTED — Also known as “dirty read”, because it literally always uses the newest data in the index without regard to transaction isolation at all, potentially reading data which isn’t currently (and may never be) committed. Even within a single statement, transactional inconsistencies may be seen from one record to the next, because no record is ever reverted to a previous version during a read.

  • READ COMMITTED — A new read view is used for each statement, based on the current maximum committed transaction ID at statement start. Records read or returned within the statement will still be consistent with each other, but from statement to statement the user will see new data.

  • REPEATABLE READ — The default for MySQL/InnoDB. A read view is created at transaction start, and that read view is used for all statements within the transaction, allowing for a consistent view of the database from statement to statement. That is, reads of data are “repeatable” within the transaction.

(Additionally there is one more transaction isolation level supported by MySQL/InnoDB, called SERIALIZABLE, but it is primarily a difference in locking, not transaction visibility, compared to REPEATABLE READ.)

In the normal course of accessing an index, some small number of records will need to be reverted to a previous version in order to satisfy the transaction isolation requirements imposed by the system. This has a cost, but as long as the read view of the transaction is fairly new, most records will not require reversion, and there is very little performance cost to doing this.

Long-running transactions and queries

It is common and mostly unsubstantiated wisdom that long-running transactions are “bad” in MySQL — but why is that? There are two reasons that long-running transactions can cause problems for MySQL:

  1. Extremely old read views. A long-running transaction (especially in the default REPEATABLE READ isolation level) will have an old read view. In a write-heavy database this may require reverting the version of very many rows to very old versions. This will slow down the transaction itself and in the worst case may mean that very long-running queries in a write-heavy database can never actually complete; the longer they run the more expensive their reads get. They can spiral into performance death eventually.

  2. Delaying purge. Because a long-running transaction has an old (potentially very old) read view, purging of undo logs (history) for the entire system will be stalled until the transaction completes. This can cause the total size of the undo logs to grow (rather than re-using the same space over and over as it normally would), causing the system tablespace (ibdata1) to grow—and of course due to other limitations, it can’t be shrunk later.

If a very long-running transaction (or query) is needed, it’s well worth considering whether it could use dirty reads in READ UNCOMMITTED isolation level in order to avoid these problems.

Deleting isn’t really deleting

Whenever a record is deleted, other transactions may still need to see the record as it existed, due to transaction isolation. If, upon delete, the record was immediately removed from the index, other transactions wouldn’t be able to find it, and thus would also not be able to find its reference to the previous record version they may need. (Keep in mind that any number of transactions may see the record in any number of versions, so five different transactions may see up to five separate versions of the record.) In order to handle this, DELETE doesn’t actually delete anything: instead it delete marks the record, flipping a “deleted” flag on.

Global history and purge operations

In addition to every record having a reference to its previous version, there is also a global view of the history of the entire database, called the “history list”. As each transaction is committed, its history is linked into this global history list in transaction serialization (commit) order. The history list is used primarily for cleaning up after a transaction, once no existing read view still needs its history (all other transactions have completed).

In the background, InnoDB runs a continuous “purge” process which is responsible for two things:

  1. Actually deleting delete-marked records2, if the current version of the record in the index at the time of purge is still delete-marked and bears the same transaction ID. (That is, the record hasn’t been re-inserted.)

  2. Freeing undo log pages and unlinking them from the global history list to make them available for re-use.

InnoDB exposes the total amount of history present in the system as a “History list length”, which can be seen in SHOW ENGINE INNODB STATUS. This is the count of all database modifications present in the undo logs, in units of undo logs (which may contain a single record modification or many).

What’s next?

Next, the physical structure of the undo logs, undo records, and history will be examined.

1 Note that InnoDB does not start a transaction internally when a BEGIN or START TRANSACTION is issued; this happens only after the first read, or immediately if START TRANSACTION WITH CONSISTENT SNAPSHOT is executed.

2 Of course actually deleting the record still doesn’t actually delete it — it just links the record into a “garbage” list, where record space may be reused. However nothing is guaranteed to be really removed from a page until the page is re-organized.

https://blog.jcole.us/2014/04/16/the-basics-of-the-innodb-undo-logging-and-history-system/

本文标签: 详解重做事务图文日志