【重难点】【MySQL 06】事务隔离级别、事务隔离级别的实现原理、深入理解 MySQL ACID

【重难点】【MySQL 06】事务隔离级别、事务隔离级别的实现原理、为什么 MySQL 事务可以保证失败回滚

一、事务隔离级别

1.并发读写问题

在并发情况下,MySQL 的同时读写可能会导致三类问题:脏读、不可重复读和幻读

脏读

当前事务中读到其他事务未提交的数据,也就是脏数据

在这里插入图片描述

以上图为例,事务 A 在读取文章的阅读量时,读取到了事务 B 未提交的数据。如果事务 B 最后没有顺利提交,导致事务回滚,那么实际上阅读量并没有修改成功,事务 A 就读到了脏数据

不可重复读

在事务 A 中先后两次读取同一个数据,但是两次读取的结果不一样。脏读与不可重复读的区别在于:前者读到的是其它事务未提交的数据,后者读到的是其它事务已经提交的数据

在这里插入图片描述

以上图为例,事务 A 在先后读取文章阅读量的数据时,结果却不一样。说明事务 A 在执行的过程中,阅读量的值被其它事务给修改了。这样使得数据的查询结果不在可靠,同样也不合实际

幻读

在事务 A 中按照某个条件先后两次查询数据库,两次查询结果的行数不同,这种现象称为幻读。不可重复读和幻读的区别可以通俗地理解为:前者是数据变了,后者是数据的行数变了
在这里插入图片描述

以上图为例,当 0 < 阅读量 < 100 的文章进行查询时,先查到了一个结果,后来查询到了两个结果。这表明同一个事务查询结果的行数不一致。这样的问题使得在根据某些条件对数据进行筛选的时候,前后筛选结果不具有可靠性

2.隔离级别

根据上面三种并发读写问题,产生了四种隔离级别,表明数据库不同程度的隔离性质,表明数据库不同程度的隔离性质

在这里插入图片描述

在实际的数据库设计中,隔离级别越高,导致数据库的并发效率越低;而隔离级别太低,又会导致数据库在读写过程中会遇到各种乱七八糟的问题。因此在大多数数据库系统中,默认的隔离级别是读已提交(Oracle)或可重复读(InnoDB 引擎,通过间隙锁解决幻读问题,通过 MVCC 解决不可重复读问题)

二、事务隔离级别的实现原理

1.标准 SQL 事务隔离级别实现原理

我们上面遇到的问题其实就是并发事务下的控制问题,解决并发事务的最常见方式就是悲观并发控制。标准 SQL 事务隔离级别的实现方式是依赖锁的,我们来看下具体是怎么实现的:

读未提交(RU)

事务对当前被获取的数据不加锁

事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级共享锁,直到事务结束才释放

读已提交(RC)

事务对当前被读取的数据加行级共享锁(当读到时才加锁),一旦读完该行,立即释放该行级共享锁

事务在更新某些数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放

可重复读(RR)

事务在读取某数据的瞬间(就是开始读取的瞬间),必须先对其加行级共享锁,直到事务结束才释放

事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放

可串行化(S)

事务在读取数据时,必须先对其加表级共享锁,直到事务结束才释放

事务在更新数据时,必须先对其加表级排他锁,直到事务结束才释放


可以看到,在只使用锁来实现隔离级别的控制的时候,需要频繁地加锁解锁,而且很容易发生读写的冲突(例如在 RC 级别下,事务 A 更新了数据行 1,则事务 B 需要等待事务 A 提交并释放锁才能读取数据行 1)

为了不加锁解决读写冲突的问题,MySQL 引入了多版本并发控制(MVCC)机制

2.多版本并发控制

本质

多版本并发控制(Multiversion concurrency control,MVCC)是数据库管理系统常用的一种并发控制,也用于程序设计语言实现事务内存。MVCC 的特点是在同一时刻,不同事务可以读取到不同版本的数据,从而可以解决脏读和不可重复读的问题

乐观并发控制和悲观并发控制都是通过延迟或者终止相应的事务来解决事物之间的竞争条件来保证事务的可串行化。虽然这两种并发控制机制确实能够从根本上解决并发事务的可串行化问题,但是其实都是在解决写冲突的问题,两者区别在于对写冲突的乐观程度不同。而在实际使用过程中,数据库读请求是写请求的很多倍,我们如果能解决读写并发的问题的话,就能更大地提高数据库的读性能,而这就是多版本并发控制所能做到的事情

与悲观并发控制和乐观并发控制不同的是,MVCC 是为了解决读写锁造成的多个、长时间的读操作饿死写操作问题,也就是解决读写冲突的问题。MVCC 可以与前两者中的任意一种机制结合使用,以提高数据库的读性能

数据库的悲观锁基于提升并发性能的考虑,一般都同时实现了多版本并发控制。不仅是 MySQL,包括 Oracle、PostgreSQL 等其它数据库系统也都实现了 MVCC,但各自的实现机制不完全相同,因为 MVCC 并没有一个统一的实现标准

总的来说,MVCC 的出现是因为使用悲观并发控制或乐观并发控制不能完美解决读写冲突问题

实现方式

MVCC 的实现,是通过保存数据在某个时间点的快照来实现的。每个事务读到的数据项都是一个历史快照,被称为快照读。不同于当前读,快照读读到的数据可能不是最新的,但是快照隔离能使得在整个事务看到的数据都是它启动时的数据状态。而写操作不覆盖已有数据项,而是创建一个新的版本,直至所在事务提交时才变为可见

  • 当前读
    像 select lock in share mode(共享锁),select for update;update,insert,delete(排他锁) 这些操作都是一种当前读。为什么叫当前读?就是因为它读取的是记录的最新版本,读取时还要保证其它并发事务不能修改当前记录,会对读取的记录进行加锁
  • 快照读
    像不加锁的 select 操作就是快照读,即不加锁的非阻塞读。快照读的前提是隔离级别不是读未提交和可串行化,因为读未提交从事读取最新的数据行,而不是符合当前事务版本的数据行。而可串行化则会对所有的行都加锁

在 MySQL 中实现 MVCC 时,每一行的数据中会额外保存几个隐藏的列,比如当前行创建时的版本号、删除时间和指向 undo log 的回滚指针。这里的版本号并不是实际的时间值,而是系统版本号。每开始新的事务,系统版本号都会自动递增。系统版本号会作为事务的版本号,用来和查询每行记录的版本号进行比较

优缺点

MVCC 使大多数读操作都可以不用加锁,这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是每行记录都需要额外的存储空间,需要更多的行检查工作,以及一些额外的维护工作

3.一些概念

锁定读

在一个事务中,主动给读加锁,例如 SELECT … LOCK IN SHARE MODE 和 SELECT … FOR UPDATE,分别加上了行共享锁和行排他锁

一致性非锁定读

InnoDB 使用 MVCC 向事务的查询提供某个时间点的数据库快照。查询会看到在该时间点之前提交的事务所做的更改,而不会看到稍后或未提交的事务所做的更改(本事务除外)。也就是说,在开始了事务之后,事务看到的数据都是事务开启那一刻的数据了,其它事务的后续修改不会在本次事务中可见

Consistent read 是 InnoDB 在 RC 和 RR 隔离级别处理 SELECT 语句的默认模式。一致性非锁定读不会对其访问的表设置任何锁,因此,在对表执行一致性非锁定读的同时,其它事务可以同时并发地读取或者修改它们

隐式锁定

InnoDB 在事务执行过程中,使用两阶段锁协议(不主动进行显式锁定的情况):

  • 随时都可以执行锁定,InnoDB 会根据隔离级别在需要的时候自动加锁
  • 锁只有在执行 commit 或者 rollback 地时候才会释放,并且所有的锁都是在同一时刻被释放

显式锁定

InnoDB 支持通过特定的语句进行显式锁定(存储引擎层)

select ... lock in share mode	//共享锁
select ... for update			//排他锁

MySQL Server 层的显式锁定

lock talbe
unlock table

Record、Gap、Next-Key

  • 行锁(Record Lock):锁直接加在索引记录上,锁住的是 Key
  • 间隙锁(Gap Lock):锁定索引记录间隙,确保索引记录的间隙不变
  • Next-Key Lock:行锁和间隙锁组合起来就叫 Next-Key Lock

默认情况下,InnoDB 工作在可重复读隔离级别下,并且会以 Next-Key Lock 的方式对数据行进行加锁,这样可以有效防止幻读的发生。Next-Key Lock 是行锁和间隙锁的组合,当 InnoDB 扫描索引记录的时候,会首先对索引记录加上行锁,再对索引记录两边的间隙加上间隙锁。加上间隙锁之后,其它事务就不能在这个间隙修改或者插入记录

举个生活中的例子,小明、小红和小花三个人依次站成一排,此时,如何让新来的小刚不能站在小红旁边呢?这时候只要将小红和她前面的小明之间的空隙封锁,将小红和她后面的小花之间的空隙封锁,那么小刚就不能站到小红的旁边,这里的小明、小红和小花就是数据库中的一条条记录。他们之间的空隙也就是间隙,而封锁空隙的锁就是间隙锁

间隙锁的唯一作用就是防止其他事务的插入操作,以此防止幻读的发生。比如上面幻读的例子,开始查询 0 < 阅读量 < 100 的文章时,只查到了一个结果。Next-Key 会将查询出的这一行进行锁定,同时还会对 0 < 阅读量 < 100 这个范围进行加锁,这样一来,就能保证在 0 < 阅读量 < 100 这个间隙中,只存在原来的一行数据,从而防止了幻读的发生

4.InnoDB 事务隔离级别实现原理

在理解上面的概念后,我们来看以下 InnoDB 的事务隔离级别具体是怎么实现的(下面的读都是指非主动加锁的 SELECT):

读未提交(RU)

事务对当前读取的数据不加锁,是当前读

事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级共享锁,直到事务结束才释放

读已提交(RC)

事务对当前被读取的数据不加锁,是快照读

事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁(Record),直到事务结束才释放

可重复读(RR)

事务对当前被读取的数据不加锁,是快照读

事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁(Record,GAP,Next-Key),直到事务结束才释放

通过间隙锁,在这个级别 MySQL 就解决了幻读的问题

通过快照,在这个级别 MySQL 就解决了不可重复读的问题

可串行化

事务在读取数据时,必须先对其加表级共享锁,直到事务结束才释放,都是当前读

事务在更新数据时,必须先对其加表级排他锁,直到事务结束才释放


可以看到,InnoDB 通过 MVCC 很好地解决了读写冲突的问题,而且提前一个级别就解决了标准级别下会出现的幻读问题,大大提升了数据库的并发能力

虽然 InnoDB 使用 Next-Key Lock 能够避免幻读问题,但是并不是真正的可串行化,再来看一个例子

在这里插入图片描述
在 T6 时间,事务 A 提交事务之后,我们发现文章 B 的阅读量也被修改成了 10000。这意味着事务 B 的提交实际上对事务 A 的执行产生了影响,也就是说,这两个事务并没有完全隔离。虽然能够避免幻读的现象,但是没有达到可串行化的级别

这还说明,避免脏读、不可重复读和幻读,是达到可串行化隔离级别的必要不充分条件

三、深入理解 MySQL ACID

转载自『浅入深出』MySQL 中事务的实现 – Draveness
在这里插入图片描述
事务其实就是并发控制的基本单位。我们都知道,事务是一个序列操作,其中的操作要么都执行,要么都不执行,它是一个不可分割的工作单位。数据库事务的 ACID 四大特性是事务的基础,了解了 ACID 是如何实现的,我们也就清楚了事务的实现,接下来我们将依次介绍数据库是如何实现这四个特性的

1.原子性(Atomicity)

在学习事务时,经常有人会告诉你,事务就是一系列的操作,要么全部都执行,要么都不执行,这其实就是对事务原子性的刻画。虽然事务具有原子性,但是原子性并不是只与事务有关,它的身影在很多地方都会出现

在这里插入图片描述
由于操作并不具有原子性,并且可以再分为多个操作,当这些操作出现错误或抛出异常时,整个操作就可能不会继续执行下去,而已经进行的操作造成的副作用就可能造成数据更新的丢失或者错误

事务其实和一个操作没有什么太大的区别,它是一些列的数据库操作(可以理解为 SQL)的集合,如果事务不具备原子性,那么就没办法保证同一事务中的所有操作都被执行或者不被执行,整个数据库系统既不可用也不可信

回滚日志

要想保证事务的原子性,就需要在异常发生时,对已经执行的操作进行回滚,而在 MySQL 中,恢复机制是通过回滚日志(undo log)实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后在对数据库中的对应行进行写入

在这里插入图片描述

这个过程其实非常好理解,为了能够在发生错误时撤销之前的全部操作,肯定是需要将之前的操作都记录下来的,这样在发生错误时才可以回滚

回滚日志除了能够在发生错误或者用户执行 ROLLBACK 时提供回滚相关的信息,它还能够在整个系统发生崩溃、数据库进程直接被杀死后,当用户再次启动数据库进程时,还能够立刻通过查询回滚日志将之前未完成的事务进行回滚,这也就需要回滚日志必须先于数据持久化到磁盘上,是我们需要先写日志后写数据库的主要原因

回滚日志并不能将数据库物理地恢复到执行语句或者事务之前的样子。它是逻辑日志,当回滚日志被使用时,它只会按照日志逻辑地将数据库中的修改撤销掉。比如:我们在事务中使用的每一条 INSERT 都对应了一条 DELETE,每一条 UPDATE 也都对应一条相反的 UPDATE 语句

在这里插入图片描述

事务的状态

因为事务具有原子性,所以从远处看的话,事务就是密不可分的一个整体,事务的状态也只有三种:Active、Commited 和 Failed,事务要么就在执行中,要么就是成功或者失败的状态

在这里插入图片描述

但是如果放大来看,我们会发现事务不再是原子的,其中包含了很多中间状态,比如部分提交,事务的状态图也变得越来越复杂

在这里插入图片描述

  • Active:事务的初始状态,表示事务正在执行
  • Partially Commited:在最后一条语句执行之后
  • Failed:发现事务无法正常执行之后
  • Aborted:事务被回滚并且数据库恢复到了事务进行之前的状态之后
  • Commited:成功执行了整个事务

虽然在发生错误时,整个数据库的状态可以恢复,但是如果我们在事务中执行了诸如:向标准输出打印日志、向外界发出邮件、没有通过数据库修改了磁盘上的内容,甚至在事务执行期间发生了转账汇款,那么这些操作作为可见的外部输出都是没有办法回滚的

并行事务的原子性

到目前为止,所有的事务都是串行执行的,一直都没有有考虑过并行执行的权问题。然而,在实际工作中,并行执行的事务才是常态,在并行任务下, 可能出现非常复杂的问题

在这里插入图片描述
当 Transaction1 在执行的过程中对 id = 1 的用户进行了读写,但是没有将修改的内容进行提交或者回滚,在这时 Transaction2 对同样的数据进行了读操作并提交了事务。也就是说 Transaction2 是依赖于 Transaction1 的,当 Transaction1 由于一些错误需要回滚时,因为要保证事务的原子性,需要对 Transaction2 进行回滚,但是由于我们已经提交了 Transaction2,所以我们已经没有办法进行回滚操作,在这种问题下我们就发生了问题,Database System Concepts 一书中将这种现象称为不可恢复安排(Nonrecoverable Schedule),那什么情况下是可以回复的呢?

简单理解一下,如果 Transaction2 依赖于 Transaction1,那么事务 Transaction1 必须在 Transaction2 提交之前完成提交的操作

在这里插入图片描述

然而这样还不算完,当事务的数量逐渐增多时,整个恢复流程也会变得越来越复杂,如果我们想要从事务发生的错误中恢复,也不是一件那么容易的事情

在这里插入图片描述

在上图所示的一次事件中,Transaction2 依赖于 Transaction1,而 Transaction3 又依赖于 Transaction1,当 Transaction1 由于执行出现问题发生回滚时,为了保证事务的原子性,就会将 Transaction2 和 Transaction3 中的工作全部回滚,这种情况也叫做级联回滚(Cascading Rollback)。级联回滚的发生会导致大量的工作需要撤回,这是我们难以接受的,不过如果想要达到绝对的原子性,这件事情又是不得不去处理的

2.持久性(Durablity)

既然是数据库,那么一定对数据的持久存储有着非常强烈的需求,如果数据被写入到数据库中,那么数据一定能够被安全存储在磁盘上。而事务的持久性就体现在:一旦事务被提交,那么数据一定会被写入到数据库中并持久存储起来

在这里插入图片描述

当事务已经被提交之后,就无法再次回滚了,唯一能够撤回已经提交的事务的方式就是创建一个相反的事务对原操作进行 “补偿”,这也是事务持久性的体现之一

重做日志

与原子性一样,事务的持久性也是通过日志来实现的,MySQL 使用重做日志(redo log)实现事务的持久性,重做日志由两部分组成,一是内存中的重做日志缓冲区,因为重做日志缓冲区在内存中,所以它是易失的,另一个就是在磁盘上的重做日志文件,它是持久的

在这里插入图片描述

当我们在一个事务中尝试对数据进行修改时,它会先将数据从磁盘读入内存,并更新内存中缓存的数据,然后生成一条重做日志并写入重做日志缓存,当事务真正提交时,MySQL 会将重做日志缓存中的内容刷新到重做日志文件,再将内存中的数据更新到磁盘上,图中第 4、5 步就是在事务提交时执行的

在 InnoDB 中,重做日志都是以 512 字节的块的形式进行存储的,同时因为块的大小与磁盘扇区大小相同,所以重做日志的写入可以保证原子性,不会由于机器断电导致重做日志仅写入一半并留下脏数据

除了所有对数据库的修改会产生重做日志,因为回滚日志也是需要持久存储的,它们也会创建对应的重做日志,在发生错误后,数据库重启时会从重做日志中找出未被更新到数据库磁盘中的日志重新执行以满足事务的持久性

回滚日志和重做日志

到现在为止,我们了解了 MySQL 中的两种日志,回滚日志(undo log)和重做日志(redo log)。在数据库系统中,事务的原子性和持久性是由事务日志(transaction log)保证的,在实现时也就是上面提到的两种日志,前者用于对事务的影响进行撤销,后者在错误处理时已经提交的事务进行重做,它们能保证两点:

  1. 发生错误或者需要回滚的事务能够成功回滚(原子性)
  2. 在事务提交后,数据没来得及写入磁盘就宕机时,在下次重新启动后能够成功恢复数据(持久性)

在数据库中,这两种日志经常都是一起工作的,我们可以将它们整体看作一条事务日志,其中包含了事务的 ID、修改的行元素以及修改前后的值

在这里插入图片描述
一条事务日志同时包含了修改前后的值,能够非常简单地进行回滚和重做两种操作

3.隔离性(Isolation)

事务的隔离性是数据库处理数据的几大基础之一,如果数据库的事务之间没有隔离性,就会发生级联回滚问题,造成性能上的巨大损失。当多个事务同时并发执行时,事务的隔离性可能就会被违反,虽然单个事务的执行可能没有任何错误,但是从总体来看就会破坏数据库的一致性。而串行虽然允许开发者忽略并行造成的影响,能够很好地维护数据库的一致性,但是却会影响事务执行的性能

4.一致性(Consistency)

作者认为数据库的一致性是一个非常让人迷惑的概念,原因是数据库领域其实包含两个一致性,一个是 ACID 中的一致性,另一个是 CAP 定义中的一致性

在这里插入图片描述
这两个数据库的一致性说的完全不是一个事情,很多人都对这两者的概念有非常深的误解,当我们在讨论数据库的一致性时,一定要清楚上下文的语义是什么

ACID

数据库对于 ACID 中的一致性的定义是这样的:如果一个事务原子地在一个一致的数据库中独立运行,那么在它执行之后,数据库的状态一定是一致的。对于这个概念,它的第一层意思就是对于数据完整性的约束,包括主键约束、引用约束以及一些约束检查等等,在事务的执行的前后以及过程中不会违背对数据完整性的约束,所有对数据库写入的操作都应该是合法的,并不能产生不合法的数据状态。也就是说,事务执行前与事务执行后,数据内在的逻辑始终是成立的

我们可以将事务理解成一个函数,它接受一个外界的 SQL 输入和一个一致的数据库,它一定会返回一个一致的数据库

在这里插入图片描述

而第二层意思其实是指逻辑上的对于开发者的要求,我们要在代码中写出正确的事务逻辑,比如银行转账,事务中的逻辑不可能只扣钱或者只加钱,这是应用层面上对于数据库一致性的要求

数据库 ACID 中的一致性对事务的要求不止包含对数据完整性以及合法性的检查,还包含应用层面逻辑的正确

CAP 定理中的数据一致性,其实是说分布式系统中的各个节点中对于同一数据的拷贝有着相同的值。而 ACID 中的一致性是指数据库的规则,如果 schema 中规定了一个值必须是唯一的,那么一致的系统必须确保在所有的操作中,该值都是唯一的,由此来看 CAP 和 ACID 对于一致性的定义有着根本性的区别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

313YPHU3

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值