深入理解MySQL的事务隔离

最近准备面试,由于笔者没有上过数据库,这里看了不少大佬博客的整理,这里根据自己的理解重新整理。并作复习之用
参考链接:https://github.com/wolverinn/Waking-Up;cyc2018.github.io;
https://github.com/wolverinn/Waking-Up/blob/master/Database.md

以及丁奇的MySQL实战45讲

首先是一个最基本的数据库的几个基本性质(即ACID)

原子性(Atomicity)

事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。在数据库中,一个个的最小操作被称之为crud,但一个事务通常并不是只有一个crud组成的,而是多个(所以,事务应当被理解为一系列的操作)

CRUD:

增加(Create)、读取查询(Retrieve)、更新(Update)和删除(Delete)几个单词的首字母简写

回滚可以用回滚日志(Undo Log)来实现,回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。

一致性(Consistency)

数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对同一个数据的读取结果都是相同的。

隔离性(Isolation)

一个事务所做的修改在最终提交以前,对其它事务是不可见的。

持久性(Durability)

一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。

系统发生崩溃可以用重做日志(Redo Log)进行恢复,从而实现持久性。与回滚日志记录数据的逻辑修改不同,重做日志记录的是数据页的物理修改。

再之后是四个并发一致性的问题

丢失修改

T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。

脏读

T1读到了T2未提交的修改,然后T2又把修改给撤销了,那么T1读到的就是脏数据

不可重复读

如果T1读到的数据,被T2修改了,那么T1再次读取的话,读到的就是不一样的数据了

幻影读

T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。

为了解决上述问题,就有了锁

封锁粒度

MySQL 中提供了两种封锁粒度:行级锁以及表级锁。

应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。

但是加锁需要消耗资源,锁的各种操作(包括获取锁、释放锁、以及检查锁状态)都会增加系统开销。因此封锁粒度越小,系统开销就越大。

在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡。

读写锁

互斥锁(Exclusive),简写为 X 锁,又称写锁。
共享锁(Shared),简写为 S 锁,又称读锁。
有以下两个规定:
一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。
一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。
简单来说,X锁与其他锁互斥,所以能加两个的只有S锁

意向锁

为了实现多粒度的封锁,就有了意向锁的存在,比如说,我们需要对整张表进行某些修改,那么我们是要遍历这张表,有没有X锁呢,这无疑是非常麻烦的事。
所以就有了意向锁。

意向锁在原来的 X/S 锁之上引入了 IX/IS,IX/IS 都是表锁,用来表示一个事务想要在表中的某个数据行上加 X 锁或 S 锁。有以下两个规定:

一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁;
一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。
通过引入意向锁,事务 T 想要对表 A 加 X 锁,只需要先检测是否有其它事务对表 A 加了 X/IX/S/IS 锁,如果加了就表示有其它事务正在使用这个表或者表中某一行的锁,因此事务 T 加 X 锁失败。
深入理解MySQL的事务隔离_第1张图片
这里直接借用cyc博客里面的图片

有了锁,就衍生出了封锁强度,称之为封锁协议

一级封锁协议

事务 T 要修改数据 A 时必须加 X 锁,直到 T 结束才释放锁。

可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么事务的修改就不会被覆盖。

显然这个可以解决丢失修改的问题

二级封锁协议

在一级的基础上,要求读取数据 A 时必须加 S 锁,读取完马上释放 S 锁。

可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。

三级封锁协议

在二级的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束了才能释放 S 锁。

可以解决不可重复读的问题,因为读 A 时,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。

为了解决四个问题,对应有四种隔离等级

未提交读

最低隔离级别,允许一个事务能够读到另一个事务未提交的信息,只对修改数据的并发操作做限制**(对于数据而言,写之前加X锁,直到事务结束释放X锁,对应一级封锁协议)**,这样解决了第一类丢失更新的问题,虽然一个事务不能修改其他事务正在修改的数据,但是可以读到其他事务还未提交的修改,如果这些修改未提交,那么就会成为脏数据,所以还未解决脏读的问题,自然,就算是已经提交的数据,多次读取结果也不一定一样,所以还未解决不可重复读和幻读的问题(存在的问题:脏读,不可重复读,幻读)

提交读

只能读取已经提交的数据,换句话说,就是一个事务读取其他事务中正在修改的数据是不被允许的**(一级封锁协议+读之前加S锁,读完数据释放S锁,对应二级封锁协议)**,由于读完之后就释放S锁,所以不能保证不可重复读与幻读

可重复读

在读已提交下,同一事务内,允许多次相同的查询能够得到不同的结果,可以使用二级封锁协议+MVCC使得当前事务只能读取不高于其事务版本的数据,也可以使用三级封锁协议**(一级封锁协议+读之前加S锁,直到事务结束释放S锁)**能解决可重复读

可串行化

可重复读与幻读的区别是:可重复读是更改表中行级数据,而幻读是增加表中行级数据,可串行化使得所有的事务必须串行化执行,解决了一切并发问题,但会造成大量的等待、阻塞甚至死锁,使系统性能降低
深入理解MySQL的事务隔离_第2张图片
上图是一个阶段性的总结,大家可以用作整理参考

多版本并发控制(Multi-Version Concurrency Control, MVCC)

概览

上面提到了mvcc,那么mvcc是什么呢。他是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,要求很低,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。
MVCC在每行记录后面都保存有两个隐藏的列,用来存储创建版本号和删除版本号。

  • 创建版本号:创建一个数据行时的事务版本号(事务版本号:事务开始时的系统版本号;系统版本号:每开始一个新的事务,系统版本号就会自动递增);

  • 删除版本号:删除操作时的事务版本号;

  • 各种操作:

    • 插入操作时,记录创建版本号;
    • 删除操作时,记录删除版本号;
    • 更新操作时,先记录删除版本号,再新增一行记录创建版本号;
    • 查询操作时,要符合以下条件才能被查询出来:删除版本号未定义或大于当前事务版本号(删除操作是在当前事务启动之后做的);创建版本号小于或等于当前事务版本号(创建操作是事务完成或者在事务启动之前完成)
    • 通过版本号减少了锁的争用,提高了系统性能;可以实现提交读和可重复读两种隔离级别,未提交读无需使用MVCC

所以,我们更多的是用MVCC来实现事务之间的隔离,下面我们来细究一下MVCC实现的过程。

回滚日志以及视图

回滚日志(undo log)

我们知道MySQL有 binlog ,如果是innodb的话还有redo log,但还有一个undo log(回滚日志)。就是专门提供在不同事务中版本切换的功能。

大家可能会觉得它和redolog很相似,以下是这个两个的基本区别

  • redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。
  • undo用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。

详细对比可以参考这一篇文章

MySQL中的重做日志(redo log),回滚日志(undo log),以及二进制日志(binlog)的简单总结

视图(consistent read view)

MySQL的视图有两个 :

一个是view ,是我们执行查询时,语句定义的虚拟表

另一个则是我们这次讨论的是InnoDB在实现MVCC时用到的一致性读视图,即consistent read view

这个视图没有实际的物理结构,是通过事件ID与undolog来维护的,即需要哪个哪个版本的数据就通过undolog来回滚

具体的实现

下面来分析一下在提交读和可重复读这个模式下MVCC如何运作的。

可重复读

如果是可重复读,那么引擎会在一个事务开始时创建一个视图,即分配一个版本号(事务的ID),此时我们就只能看到当前事务以及当前事务之前的数据了,如果我当前行的数据已经被别的事务修改,那么就会通过回滚日志,找到我或我之前的版本(取决于我此次有没有进行修改),来保证可重复读。

但这里要注意一点,并不是版本号越大,修改一定在后面,例如我一个事务A先启动,被分配到100的ID,后面事务B后启动,被分配到101的ID,然后事务B先进行修改,所以事务B的版本反而会在前面。当然官方可能为了尽量防止这种情况的发生有以下规定:

begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB表 的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用start transaction with consistent snapshot 这个命令。

提交读

提交读就是在可重复读的基础上,每一个语句执行前都会重新算出一个新的视图,这样自然就可以实现提交读了

tip

有一点需要注意的是,可重复读时针对查询来说的,如果一个update语句,那么他就会找到最新的记录进行更新,不然就会出现丢失修改的情况了

你可能感兴趣的:(数据库)