龙空技术网

MySQL事务隔离

JavaCodeGirl 290

前言:

而今大家对“mysqlisolation”大概比较关怀,姐妹们都想要剖析一些“mysqlisolation”的相关文章。那么小编也在网上搜集了一些对于“mysqlisolation””的相关内容,希望姐妹们能喜欢,姐妹们快快来学习一下吧!

事务是MySQL中一个比较重要的概念,隔离性是它比较重要的知识点,我们必须要掌握它!

我们今天从以下几个方面学习事务隔离性:

1)事务的特性有哪些?

2)多个事务时会存在什么问题?

3)事务的隔离级别有哪些?

4)事务的隔离级别是如何实现的?

事务特性

提到事务相信大家脑袋里的第一个反应就是这四个单词:ACID

事务是保证一组数据库的操作要么全部成功,要么全部失败。事务是引擎层实现的,不同的存储引擎对事务的支持不同。MyISAM不支持事务,InnoDB支持事务,文章中所说的事务都是在存储引擎为InnoDB的基础上的。

A(Atomicity)原子性

一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部成功提交,要么全部失败回滚。对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。

C(Consistency)一致性

数据库总是从一个一致性状态转换到另外一个一致性状态。举个银行转账的例子,A账户扣除100元,B账户增加100元,如果A执行之后出错,A账户也不会损失100元。因为事务没有最终提交,所以事务所做的修改是不会保存到数据库中的。

I(Isolation)隔离性

一个事务所做的修改在最终提交以前,对其他事务是不可见的。数据库允许多个事务并发对数据进行读取和修改,隔离性可以防止多个事务并发时引起的数据问题。这个特性也是我们这篇文章的重点。

D(Durability)持久性

一旦事务提交,则其所做的修改就会永久保存到数据库中。即使系统崩溃,修改的数据也不会丢失。

今天我们学习的知识就是I:隔离性。

事务存在的问题

在学习事务的隔离特性之前,我们需要了解事务存在的问题!

事务的问题是在多个事务时存在的,也即是当多个事务同时执行的时候,可能会出现脏读、不可重复读、幻读的问题,为了解决这些问题就引入了事务的隔离级别。

脏读

脏读:一个事务读到了其他事务没有提交的数据。比如A和B两个事务对同一条记录进行修改,A修改之后还没有提交,但是B事务已经看到了A修改的数据,并且可以使用这个数据。

不可重复读

不可重复读:一个事务读到了其他事务修改且已经提交的数据。A事务开启事务读取数据,此时B事务也开启并且修改了同一行数据,B提交事务之后A再次读取的时候读到了B修改之后的数据,也即是A事务的整个事务的两次读取到的数据不一致。

幻读

幻读:一个事务读取到了其他事务新增且已经提交的数据。A事务开启事务读取数据,此时B事务开启新增了一条记录,B事务提交之后A事务再次查询得到了不一致的记录。幻读区别于不可重复读的区别一个在于修改同一行记录(不可重复读),一个是新增记录(幻读)。

事务的隔离级别

隔离级别

MySQL的隔离级别有4种:读未提交、读已提交、可重复读和串行化。

读未提交(read uncommitted):一个事务读到了其他事务未提交的数据。即一个事务未提交时,它修改的数据就能被其他的事务看到。事务可以读取未提交的数据,也就是我们说的脏读。这个级别有很多问题,但是从性能上来说优于其他的隔离级别。除非有非常必要的理由,在实际应用中一般很少使用。

读已提交(read committed):一个事务读到了其他事务已经提交的数据。即一个事务提交之后,它做的变更就能被其他事务看到。这个隔离级别会导致不可重复读的问题。这是大部分数据库除了MySQL之外默认的隔离级别。

可重复读(repeatable read):一个事务执行过程中看到的数据,总是跟这个事务在启动的时候看到的数据是一致的。可重复读隔离级别还是无法解决幻读的问题。这是数据库MySQL默认的隔离级别。

串行化(serializable):对于同一行记录,写会加写锁,读会加读锁。当出现读写锁冲突的时候,后访问的事务必须等待前一个事务执行完,才能继续执行。它是最高的隔离级别,只有在非常需要确保数据一致性而且可以接受没有并发的场景下,才考虑使用该级别。

user表中存在主键id为6的记录,信息如下:

(6, 'Tommy', 19, '0', NULL, '2021-02-16');

我们使用事务A和事务B,事务A在不同的时刻查询数据,事务B修改同一行记录,可以查看不同的隔离级别下事务A不同时刻看到的数据值。

隔离级别为读未提交时,则V1值是19,B事务修改数据后虽然还没提交事务,但是事务A已经能看到修改后的数据了。因此V2、V3、V4的值为20。

隔离级别为读已提交时,则V1、V2值是19,因为事务B修改的数据在提交之后才能被事务A看到。因此V3、V4是20。

隔离级别为可重复读时,则V1、V2、V3是19,因为事务A在未提交事务之前整个事务执行期间看到数据是一致的。V4的值为20。

隔离级别为串行化时,则事务B是写操作,与事务A发生读写冲突,事务B会被锁住。直到事务A提交之后,事务B才可以继续执行。所以V1、V2、V3的值是19,V4的值是20。

查看和设置隔离级别

通过上述例子,我们看到不同的隔离级别下,数据库的行为是不同的。Oracle数据库默认的隔离级别为读提交,MySQL数据库默认的隔离级别为可重复读

MySQL数据库可以通过show variables查看配置参数transaction_isolation的值查看隔离级别。

mysql> show variables like 'transaction_isolation';+-----------------------+-----------------+| Variable_name         | Value           |+-----------------------+-----------------+| transaction_isolation | REPEATABLE-READ |+-----------------------+-----------------+

我们还可以通过set session transaction isolation level 隔离级别的方式修改事务的隔离级别。

mysql> set session transaction isolation level serializable;Query OK, 0 rows affected (0.00 sec)mysql> show variables like 'transaction_isolation';+-----------------------+--------------+| Variable_name         | Value        |+-----------------------+--------------+| transaction_isolation | SERIALIZABLE |+-----------------------+--------------+

事务的启动

显式启动事务:begin/start transaction。如果事务执行成功则执行commit语句,执行失败则执行rollback语句。这种启动事务的方式不是我们执行了begin/start transaction命令事务就立即开启了,而是在执行第一个操作InnoDB表的语句时,事务才真正的启动。

如果我们想要立即开启一个事务,可以使用start transaction with consistent snapshot命令

MySQL默认采用自动提交(AUTOCOMMIT)模式,即如果不是显式开始一个事务,每个执行语句都当作一个事务执行提交操作。可以通过设置AUTOCOMMIT的值来启动或者禁用自动提交模式。

mysql> show variables like 'autocommit';+---------------+-------+| Variable_name | Value |+---------------+-------+| autocommit    | ON    |+---------------+-------+

1或者ON表示启动,0或者OFF表示禁用。我们可以使用set autocommit = 0这个命令将线程的自动提交关掉。即当我们执行一个select语句时,这个事务就启动了,直到我们主动执行commit或者rollback语句,或者断开连接。这种设置方式如果遇到长连接,就可能导致了意外的长事务。所以我们一般使用set autocommit = 1通过显式语句的方式来启动事务。

隔离级别的实现

MVCC多版本

数据库的多版本控制(MVCC:Multiversion Concurrency Control)表示同一条记录在系统中可以存在多个版本。MVCC的实现是通过保存数据在某个时间点的快照来实现的。不管事务执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表同一时刻看到的数据可能是不一样的。

InnoDB的 MVCC ,是通过在每行记录的后面保存两个隐藏的列来实现的。这两个列, 一个保存了行的创建时间,一个保存了行的过期时间(或删除时间), 当然存储的并不是实际的时间值,而是系统版本号。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的版本号会作为事务的版本号,与查询到的每行记录的版本号对比。

INSERT语句,InnoDB会为新插入的每一行保存当前系统版本号为行版本号。

DELETE语句,InnoDB会为每一行保存当前系统版本号作为删除标识。

UPDATE语句,InnoDB为新插入的一行记录保存当前系统版本号为行版本号,同时把当前系统版本号作为原来行的删除标识。

InnoDB在实现MVCC时用到的一致性试图,是支持读提交和可重复读的实现。

我们接下来分析4个隔离级别的实现。

读未提交

读未提交隔离级别下总是读取最新的数据行,它不是基于MVCC的支持。

串行化

串行化的隔离级别下是直接用加锁的方式来避免并行访问。

可重复读

可重复读是基于MVCC的实现,它会创建一个视图,这个视图是在事务启动时创建的,整个事务期间都会用这个视图。

在可重复读的隔离级别下,事务在启动的时候就拍了快照,这个快照是基于整个数据库的。这个快照是怎么实现的呢?

InnoDB每个事务都有一个唯一的按照申请顺序严格递增的事务ID,我们称为transaction id。这个事务ID赋值给了MVCC中的事务ID列,为row trx_id。同时旧的数据也会保留,并且新的数据版本中可以直接得到它。

如果我们id=6对age字段连续更新,那么这一行记录的多个版本信息如下所示:

图中的U3、U2、U1即是undo log,V1、V2、V3、V4并不是物理存在的,而是根据当前版本与undo log计算出来的。InnoDB为每个事务构造了一个数据,用来保存这个事务启动瞬间,当前活跃的所有事务ID。即事务启动了但是还没有提交的事务ID。

数组里面事务ID的最小值记为低水位,当前系统里面已经创建过的事务ID的最大值加1为高水位。那么这个视图数组和高水位就组成了当前事务的一致性视图(read-view)。

对于当前的事务,启动瞬间,它的row trx_id有以下几种情况:

1)如果在已提交的事务部分,表示这个版本是已提交的事务或者当前事务自己生成的,这个数据是可见的。

2)如果在未开始的事务部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的。

3)如果在未提交的事务部分,有两种情况:

row trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见。row trx_id不在数组中,表示这个版本是已提交的事务生成的,可见。

也许到这里你还是觉着有点抽象,那么我们接下来举个实际的例子讲一下。

我们做如下假设:

事务A开启之前,系统里面只有一个活跃事务ID为99;事务A、B、C的事务id分别为100、101、102,且系统中目前只有这4个事务。事务开启之前,id为6的这一行的age字段值为19,这一行的row trx_id为98。

对于事务A查询时瞬间,事务的水位分配如下所示:

那么事务A查询数据的流程如下:

查询到当前版本(6,21),判断row trx_id = 101,位于未开始的事务区域,不可见。继续查询上一个历史版本,判断row trx_id = 102,位于未开始的事务区域,不可见。继续查询上一个历史版本,判断row trx_id = 98,位于已提交的事务区域,可见。

A事务查询得到(6,19),与事务启动时查询得到的数据是一致的,我们称为可重复读,又称为一致性读。

可能有的小伙伴这里会有疑问:按照这种分析,事务B是不是应该看不到事务C修改的数据,为啥它更新后的数据是21呢?

大家记住一个规则:更新数据之前都会读取数据然后再写数据,而这个读,只能读当前的最新值,我们称为当前读。按照这个规则再去理解事务B就知道为啥它能看到事务C修改后的数据了。

读已提交

读已提交是基于MVCC的实现,它会创建一个视图,访问的时候也是以这个视图的逻辑结果为准的。对于读已提交的隔离级别,这个视图是在每个SQL语句开始执行的时候创建的。

对于上述的例子,在读已提交的隔离级别下视图的创建如下图所示:

从事务A查询语句创建的视图来说,事务ID的水位分配如下:

对于事务A的查询来说,在查询之前创建了新的视图,这个时候事务C已经提交,因此是可以看到的,而对于事务B来说虽然是活跃的事务但是还没提交所以不可见。

所以事务A查询的结果为age=20,而事务B查询的结果为age=21。

从这里我们可以了解读已提交和可重复读的实现区别:

可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务的其他查询都是共用这一个视图的。读已提交隔离级别下,每一个执行语句前都会重新计算一个新的视图。总结

事务的特性:原子性、一致性、隔离性和持久性。

事务出现的问题:脏读、不可重复读和幻读。

事务的隔离级别:读未提交、读已提交、重复读和串行化。

读已提交解决了脏读的问题,但是没有解决不可重复读和幻读问题。

重复读解决了脏读和不可重复读的问题,但是没有解决幻读。

串行化通过加锁的方式,虽然解决了问题,但是性能比较差。

MySQL默认的隔离级别为可重复读。

读已提交和可重复读是数据库MVCC多版本的支持。

读已提交在每个执行语句之前生成视图。查询承认在语句执行前就已经提交完成的数据。

可重复读在事务开启时生成视图,后续的查询语句都是基于这个视图的。查询承认在事务启动前就已经提交完成的数据。

而当前读,总是读取已经提交完成的最新版本数据,update语句就是当前读。

我是勾勾,一直在努力的程序媛

在看、点赞、转发和关注是最好的支持!

我们下篇文章见!

参考资料:

《高性能MySQL》

《MySQL实战45讲-丁奇》

标签: #mysqlisolation