MySQL中的事务隔离级别如何实现

33次阅读
没有评论

共计 4613 个字符,预计需要花费 12 分钟才能阅读完成。

丸趣 TV 小编给大家分享一下 MySQL 中的事务隔离级别如何实现,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!

说到数据库事务,大家脑子里一定很容易蹦出一堆事务的相关知识,如事务的 ACID 特性,隔离级别,解决的问题(脏读,不可重复读,幻读)等等,但是可能很少有人真正的清楚事务的这些特性又是怎么实现的,为什么要有四个隔离级别。

今天我们就先来聊聊 MySQL 中事务的隔离性的实现原理,后续还会继续出文章分析其他特性的实现原理。

当然 MySQL 博大精深,文章疏漏之处在所难免,欢迎批评指正。

说明

MySQL 的事务实现逻辑是位于引擎层的,并且不是所有的引擎都支持事务的,下面的说明都是以 InnoDB 引擎为基准。

定义

隔离性(isolation)指的是不同事务先后提交并执行后,最终呈现出来的效果是串行的,也就是说,对于事务来说,它在执行过程中,感知到的数据变化应该只有自己操作引起的,不存在其他事务引发的数据变化。

隔离性解决的是并发事务出现的问题。

标准 SQL 隔离级别

隔离性最简单的实现方式就是各个事务都串行执行了,如果前面的事务还没有执行完毕,后面的事务就都等待。但是这样的实现方式很明显并发效率不高,并不适合在实际环境中使用。

为了解决上述问题,实现不同程度的并发控制,SQL 的标准制定者提出了不同的隔离级别:未提交读(read uncommitted)、提交读(read committed)、可重复读(repeatable read)、序列化读(serializable)。其中最高级隔离级别就是序列化读,而在其他隔离级别中,由于事务是并发执行的,所以或多或少允许出现一些问题。见以下的矩阵表:

隔离级别(+: 允许出现,-: 不允许出现)脏读不可重复读幻读未提交读 +++ 提交读 -++ 可重复读 –+ 序列化读 —

注意,MySQL 的 InnoDB 引擎在可重复读级别通过间隙锁解决了幻读问题,通过 MVCC 解决了不可重复读的问题,具体见下面的分析。

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

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

事务隔离级别实现方式未提交读(RU)事务对当前被读取的数据不加锁;

事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级共享锁,直到事务结束才释放。提交读(RC)事务对当前被读取的数据加行级共享锁(当读到时才加锁),一旦读完该行,立即释放该行级共享锁;

事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放。可重复读(RR)事务在读取某数据的瞬间(就是开始读取的瞬间),必须先对其加行级共享锁,直到事务结束才释放;

事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁,直到事务结束才释放。序列化读(S)事务在读取数据时,必须先对其加表级共享锁,直到事务结束才释放;

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

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

为了不加锁解决读写冲突的问题,MySQL 引入了 MVCC 机制,详细可见我以前的分析文章:一文读懂数据库中的乐观锁和悲观锁和 MVCC。

InnoDB 事务隔离级别实现原理

在往下分析之前,我们有几个概念需要先了解下:

1、锁定读和一致性非锁定读

锁定读:在一个事务中,主动给读加锁,如 SELECT … LOCK IN SHARE MODE 和 SELECT … FOR UPDATE。分别加上了行共享锁和行排他锁。锁的分类可见我以前的分析文章:你应该了解的 MySQL 锁分类 )。

https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html

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

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

https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html

2、当前读和快照读

当前读

读取的是最新版本,像 UPDATE、DELETE、INSERT、SELECT …  LOCK IN SHARE MODE、SELECT … FOR UPDATE 这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

快照读

读取的是快照版本,也就是历史版本,像不加锁的 SELECT 操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是未提交读和序列化读级别,因为未提交读总是读取最新的数据行,而不是符合当前事务版本的数据行,而序列化读则会对表加锁。

3、隐式锁定和显式锁定

隐式锁定

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

随时都可以执行锁定,InnoDB 会根据隔离级别在需要的时候自动加锁;

锁只有在执行 commit 或者 rollback 的时候才会释放,并且所有的锁都是在同一时刻被释放。

显式锁定

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

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

MySQL Server 层的显示锁定:

lock table
unlock table

了解完上面的概念后,我们来看下 InnoDB 的事务具体是怎么实现的(下面的读都指的是非主动加锁的 select)

事务隔离级别实现方式未提交读(RU)事务对当前被读取的数据不加锁,都是当前读;

事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级共享锁,直到事务结束才释放。提交读(RC)事务对当前被读取的数据不加锁,且是快照读;

事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加行级排他锁(Record),直到事务结束才释放。可重复读(RR)事务对当前被读取的数据不加锁,且是快照读;

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

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

通过快照,在这个级别 MySQL 就解决了不可重复读的问题序列化读(S)事务在读取数据时,必须先对其加表级共享锁,直到事务结束才释放,都是当前读;

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

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

一些常见误区幻读到底包不包括了 delete 的情况?

不可重复读:前后多次读取一行,数据内容不一致,针对其他事务的 update 和 delete 操作。为了解决这个问题,使用行共享锁,锁定到事务结束(也就是 RR 级别,当然 MySQL 使用 MVCC 在 RC 级别就解决了这个问题)

幻读:当同一个查询在不同时间生成不同的行集合时就是出现了幻读,针对的是其他事务的 insert 操作,为了解决这个问题,锁定整个表到事务结束(也就是 S 级别,当然 MySQL 使用间隙锁在 RR 级别就解决了这个问题)

网上很多文章提到幻读和提交读的时候,有的说幻读包括了 delete 的情况,有的说 delete 应该属于提交读的问题,那到底真相如何呢?我们实际来看下 MySQL 的官方文档(如下)

The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT) is executed twice, but returns a row the second time that was not returned the first time, the row is a“phantom”row.

https://dev.mysql.com/doc/refman/5.7/en/innodb-next-key-locking.html

可以看到,幻读针对的是结果集前后发生变化,所以看起来 delete 的情况应该归为幻读,但是我们实际分析下上面列出的标准 SQL 在 RR 级别的实现原理就知道,标准 SQL 的 RR 级别是会对查到的数据行加行共享锁,所以这时候其他事务想删除这些数据行其实是做不到的,所以在 RR 下,不会出现因 delete 而出现幻读现象,也就是幻读不包含 delete 的情况。

MVCC 能解决了幻读问题?

网上很多文章会说 MVCC 或者 MVCC+ 间隙锁解决了幻读问题,实际上 MVCC 并不能解决幻读问题。如以下的例子:

begin;
#假设 users 表为空,下面查出来的数据为空
select * from users; # 没有加锁
#此时另一个事务提交了,且插入了一条 id= 1 的数据
select * from users; # 读快照,查出来的数据为空
update users set name= mysql  where id=1;#update 是当前读,所以更新成功,并生成一个更新的快照
select * from users; # 读快照,查出来 id 为 1 的一条记录,因为 MVCC 可以查到当前事务生成的快照
commit;

可以看到前后查出来的数据行不一致,发生了幻读。所以说只有 MVCC 是不能解决幻读问题的,解决幻读问题靠的是间隙锁。如下:

begin;
#假设 users 表为空,下面查出来的数据为空
select * from users lock in share mode; # 加上共享锁
#此时另一个事务 B 想提交且插入了一条 id= 1 的数据,由于有间隙锁,所以要等待
select * from users; # 读快照,查出来的数据为空
update users set name= mysql  where id=1;#update 是当前读,由于不存在数据,不进行更新
select * from users; # 读快照,查出来的数据为空
commit;
#事务 B 提交成功并插入数据 

注意,RR 级别下想解决幻读问题,需要我们显式加锁,不然查询的时候还是不会加锁的。

以上是“MySQL 中的事务隔离级别如何实现”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注丸趣 TV 行业资讯频道!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-07-15发表,共计4613字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)