博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
谈谈MySQL InnoDB存储引擎事务的ACID特性
阅读量:6592 次
发布时间:2019-06-24

本文共 6599 字,大约阅读时间需要 21 分钟。

hot3.png

1、前言

狼哥一直对数据库Mysql这块没有进行过系统的深入学习,今天看到一篇文章写的还不错,特意分享一下,我不能保证文章中所讲的都是正确,大家都应该带着探索的思维去阅读别人的文章。

数据库的4大事务特性ACID,想必大家都应该知道。

ACID表示原子性、一致性、隔离性和持久性。一个很好的事务处理系统,必须具备这些标准特性:

  • 原子性(Atomicity):一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚。

  • 一致性(consistency):数据库总是从一个一致性的状态转换到另一个一致性的状态。(其实原子性和隔离性间接的保证了一致性)

  • 隔离性(isolation):通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。

  • 持久性(durability):一旦事务提交,则其所做的修改就会永久保存到数据库中。

而我们最常说的隔离性其实有对应的隔离级别,MySQL规定的隔离级别有4种,分别是:

  • READ UNCOMMITTED(读未提交):在此级别里,事务的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,也就是会产生脏读,在实际应用中一般很少使用。

  • READ COMMITTED(读已提交):大多数数据库系统的默认隔离级别都是它,但是MySQL不是。它能够避免脏读问题,但是在一个事务里对同一条数据的多次查询可能会得到不同的结果,也就是会产生不可重复读问题。

  • REPEATABLE READ(可重复读):该隔离级别是MySQL默认的隔离级别,看名字就知道它能够防止不可重复读问题,但是在一个事务里对一段数据的多次读取可能会导致不同的结果,也就是会有幻读的问题(注:这里说的无法解决是MySQL定义层面,对于InnoDB引擎则完美的解决了幻读的问题,如果你正在使用InnoDB引擎,可忽略)

  • SERIALIZABLE(可串行化):该隔离级别是级别最高的,它通过锁来强制事务串行执行,避免了前面说的所有问题。在高并发下,可能导致大量的超时和锁争用问题。实际应用中也很少用到这个隔离级别,因为RR级别解决了所有问题。

可以看到隔离级别里最重要的只有两个隔离级别:RC和RR。那么问题来了,我们知道上面说的ACID以及隔离级别的实现原理吗?无论是平时工作还是面试,这部分的问题都重中之重,接下来,我会抛出几个问题,大家可以带着问题来看此文:

ACID问题:

  • InnoDB能够保证原子性?用的什么方式?
  • 为什么InnoDB能够保证一致性?用的什么方式?
  • 为什么InnoDB能够保证持久性?用的什么方式?

隔离性里隔离级别的问题:

为什么RU级别会发生脏读,而其他的隔离级别能够避免? 为什么RC级别不能重复读,而RR级别能够避免? 为什么InnoDB的RR级别能够防止幻读? 解决这些问题之前,我们要首先知道Redo log、Undo log以及MVCC都是什么。

2、Redo log

redo log(重做日志)用来实现事务的持久性,即事务ACID中的D。其由两部分组成,一是内存中的重做日志缓冲(redo log buffer),其实易失的。二是重做日志文件(redo log file),其是持久的。

在一个事务中的每一次SQL操作之后都会写入一个redo log到buffer中,在最后COMMIT的时候,必须先将该事务的所有日志写入到redo log file进行持久化(这里的写入是顺序写的),待事务的COMMIT操作完成才算完成。

由于重做日志文件打开没有使用O_DIRECT选项,因此重做日志缓冲先写入文件系统缓存。为了确保重做日志写入磁盘,必须进行一次fsync操作。由于fsync的效率取决于磁盘的性能,因此磁盘的性能决定了事务提交的性能,也就是数据库的性能。由此我们可以得出在进行批量操作的时候,不要for循环里面嵌套事务。

参数 innodb_flush_log_at_trx_commit 用来控制重做日志刷新到磁盘的策略,该参数有3个值:0、1和2。

  • 0:表示事务提交时不进行写redo log file的操作,这个操作仅在master thread中完成(master thread每隔1秒进行一次fsync操作)。

  • 1:默认值,表示每次事务提交时进行写redo log file的操作。

  • 2:表示事务提交时将redo log写入文件,不过仅写入文件系统的缓存中,不进行fsync操作。

我们可以看到0和2的设置都比1的效率要高,但是破坏了数据库的ACID特性,不建议使用!

对比binlog

在MySQL数据库中还有一种二进制日志(binlog),从表面上来看它和redo log很相似,都是记录了对数据库操作的日志,但是,它们有着非常大的不同。

首先,redo log是在MySQL的InnoDB引擎层产生,而binlog则是在MySQL的上层产生,它不仅针对InnoDB引擎,其他任何引擎对于数据库的更改都会产生binlog。

其次,两种日志记录的内容形式不同,binlog是一种逻辑日志,其记录的是对应的SQL语句。而redo log则是记录的物理格式日志,其记录的是对于每个页的修改。

此外,两种日志记录写入磁盘的时间点不同,binlog只在事务提交完成后一次性写入,而redo log在上面也说了是在事务进行中不断被写入,这表现为日志并不是随事务提交的顺序进行写入的。

redo log block

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

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

下面我来解释一下组成Log Block header的4个部分各自的含义: LOGBLOCKHDR_NO:它主要用来标记所处Redo Log Buffer中Log Block的位置。 LOGBLOCKHDRDATALEN:它表示Log Block所占用的大小。当Log Block被写满时,该值为0x200,表示使用全部Log Block空间,即占用512字节。 LOGBLOCKFIRSTRECGROUP:表示Log Block中第一个日志所在的偏移量,如果该值大小和LOGBLOCKHDRDATALEN相同,则表示当前Log Block不包含新的日志,如果事务的日志大小超过一个Log Block的大小,剩余的将会接着保存到一个新的Log Block中。 LOGBLOCKCHECKPOINT_NO:表示该Log Block最后被写入时的检查点第4字节的值。 Log Block tailer只包含一个LOGBLOCKTRLNO,它的值和LOGBLOCKHDRNO相同,并在函数logblockinit中被初始化。

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进行恢复就可以了。 我们可以通过命令show engine innodb status来观察LSN的情况:


LOG

Log sequence number 33646077360 Log flushed up to

33646077360 Last checkpoint at
33646077360 0 pending log writes ,

0 pending chkp writes 49687445 log i / o 's done, 1.25 log i/o' s / second

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进行恢复即可。

3、Undo log

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

Undo log是InnoDB MVCC事务特性的重要组成部分。当我们对记录做了变更操作时就会产生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(TRXRSEGN_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语句 deletefromtwherea=1,如果列a有聚集索引,则不会进行真正的删除,而只是在主键列等于1的记录delete flag设置为1,即记录还是存在在B+树中。而对于update操作,不是直接对记录进行更新,而是标识旧记录为删除状态,然后新产生一条记录。那这些旧版本标识位删除的记录何时真正的删除?怎么删除?

其实InnoDB是通过undo日志来进行旧版本的删除操作的,在InnoDB内部,这个操作被称之为purge操作,原来在srvmasterthread主线程中完成,后来进行优化,开辟了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的效率。

4、多版本控制MVCC MVCC 多版本并发控制技术,用于多事务环境下,对数据读写在不加读写锁的情况下实现互不干扰,从而实现数据库的隔离性,在事务隔离级别为Read Commit 和 Repeatable Read中使用到,今天我们就用最简单的方式,来分析下MVCC具体的原理,先解释几个概念。

InnoDB存储引擎的行结构

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

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

DBROWID:InnoDB引擎中一个表只能有一个主键,用于聚簇索引,如果表没有定义主键会选择第一个非Null的唯一索引作为主键,如果还没有,生成一个隐藏的DBROWID作为主键构造聚簇索引。 DBTRXID:最近更改该行数据的事务ID。 DBROLLPTR:undo log的指针,用于记录之前历史数据在undo log中的位置。 DELETE BIT:索引删除标志,如果DB删除了一条数据,是优先通知索引将该标志位设置为1,然后通过(purge)清除线程去异步删除真实的数据。

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

事务链表

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

转载于:https://my.oschina.net/hensemlee/blog/1803367

你可能感兴趣的文章
oracle kill所有plsql developer进程
查看>>
12c rac 实例无法启动之磁盘组空间耗尽
查看>>
keepalived双机热备原理及实例部署LVS+keepalived
查看>>
曲线学习PyQt5方案一
查看>>
企业私有云之rabbitmq高可用
查看>>
OpenCV学习】矩阵运算和操作2
查看>>
nginx+ffmpeg搭建rtmp转播rtsp流的flash服务器
查看>>
Win10 IoT C#开发 1 - Raspberry安装IoT系统及搭建开发环境
查看>>
关于在arm裸板编程时使用printf问题的解决方法
查看>>
开源人工智能技术将改变一切
查看>>
2015 上半年 JavaScript 使用统计数据
查看>>
《Python算法教程》——1.6 如果您感兴趣
查看>>
深度解析Java8 – AbstractQueuedSynchronizer的实现分析(下)
查看>>
SSH原理与运用(一):远程登录
查看>>
Spring Framework 4.2 中的新功能和增强功能
查看>>
动态代理解决网站字符集编码
查看>>
我所想的GIX4的权限
查看>>
Hbuilder--让手爽,飞一般的编码(二)
查看>>
后台统计
查看>>
React组件: 提取图片颜色
查看>>