【mysql】事务的隔离级别有哪些?各个级别造成的问题是什么?Innodb分别是如何解决他们的?

上篇文章总结了有关事务的原子性,持久性和一致性【mysql】Innodb存储引擎是如何保证事务的ACID四个原则的,接下来是事务的隔离性。

事务隔离级别

  1. READ UNCOMMITTED : RU 称为浏览访问,可以读取事务未提交的数据。
  2. READ COMMITTED : RC 称为游标稳定,只能读取已经提交事务的数据。
  3. REPEATABLE READ: RR 可重复读。
  4. SERIALIZABLE : SR 序列化。所有事务按照次序依次执行。当一个事务开始未提交时,其他事务均不能执行。

各个事务级别所造成的问题以及原因

  1. RU:

    问题:读取脏数据,不可重复读,幻像
    原因:在RU隔离级别下,读未提交的数据,所以,当事务还没有提交时,数据并不是最终的数据,另一个事务就有可能读取到脏数据。

  2. RC:

    问题:不可重复读,幻读
    原因:
    不可重复读:在对数据进行修改时,会产生undo log,在undo log中,数据会有多个版本的快照数据这种技术成为行多版本技术(请阅读下面 undo log 存储版本链)。在RC级别下,innodb存储引擎每次读取的历史版本是最新的版本数据,因此,当有新的事务提交产生新的版本数据时,RC级别读取到的数据会发生不一致这种现象称为不可重复读(每次select会产生一个新的readView导致)。
    幻读:在事务执行select操作加锁时,仍然可以进行insert操作对数据进行更改,再次执行select操作得到的结果集不一致的问题。这是因为在RC级别下不支持间隙锁,当select时加锁只能对记录本身的索引值加锁,不能对范围进行加锁,以致于可以继续插入范围内的数据造成幻读(下面解决方案中会举例说明)。

  3. RR

    问题:在RR级别下,innodb存储引擎解决了RC下的问题。可以达到隔离性的要求。

  4. SR
    此级别下由于是一个事务执行完成,另一个事务才能开始执行,事务级别最高,因此可以达到隔离性。

undo log 存储的版本链

版本链类其实是采用了乐观锁的机制。
在InnoDB引擎表中,它的聚簇索引记录中有两个必要的隐藏列:

  • trx_id
    这个id用来存储的每次对某条聚簇索引记录进行修改的时候的事务id。
  • roll_pointer
    每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)

对于不同的操作,存储的数据如下:
SELECT
  innodb会根据以下两个条件检查每行记录:
    a.innodb只查找版本号早于当前事务版本的数据行,<=当前事务版本号,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的

b.行的删除版本要么未定义,要么大于当前的事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。

INSERT
  INNODB为新插入的每一行保存当前系统版本号作为行版本号
DELETE
  innodb为删除的每一行保存当前系统版本号作为行删除标识
UPDATE
  innodb为插入一行新纪录,保存当前系统版本号为行版本号,同时保存当前系统版本号到原来的行作为行删除标识

  1. RR :在innodb存储引擎中,已基本解决以上的问题,可以达到和SR相同效果的事务隔离级别
  2. SR:有事务按照次序依次执行。当一个事务开始未提交时,其他事务均不能执行。不会出现以上问题。

问题描述
1、脏读:读到未提交事务数据时,称为脏读。即读到脏数据,并不是事务最终的数据。
2、不可重复读:是幻像问题中的一种。在事务进行中,重复读取统一条记录,显示的数据不同,称为不可重复读。
3、幻读:是幻像问题的一种。在事务进行中,重复读取记录,得到的记录数与之前不一致。如:在同一个事务中第一次读取有5条记录,第二次读取为6条记录,出现了幻读现象。

不可重复读和幻读都是幻像问题的一种表现形式。

解决方案

1、脏读:将数据库隔离级别修改为READ COMMITTED。只能读取提交事务的数据即可。在mysql数据库中读取的是事务版本号比自己小的数据。

2、不可重复读:在RC 级别由于上面所说的读取的历史数据版本总是为最新的版本,这个跟ReadView相关,每次select会生成一个新的ReadView,所以造成多次查询结果数据不一致。

解决方案:将隔离级别修改为REPEATABLE READ,因为在这个级别下,每次select时总是查询事务开始时的数据版本,即每次事务开始时都会生成一份独立的ReadView,直到事务结束都用这一份ReadView,也就读取的都是事务开始时的数据。

3、幻读

在innodb存储引擎的RR级别,由于运用了间隙锁,所以可以解决幻读的问题。
间隙锁锁定的不是单个的值而是一个范围。具体解释看下面的例子。
举例:

CREATE TABLE z( a INT, b INT ,PRIMARY KEY(a),KEY(b));

INSERT INTO z SELECT 1,1;
INSERT INTO z SELECT 3,1;
INSERT INTO z SELECT 5,3;
INSERT INTO z SELECT 7,6;
INSERT INTO z SELECT 10,8;

===========语句1=================
SELECT * FROM z WHERE b=3  FOR UPDATE;  

==========语句2==================
SELEFCT * FROM z WHERE a=5 LOCK IN SHARE MODE;

==========语句3==================
INSERT INTO z SELECT 4,2;
INSERT INTO z SELECT 6,5;

==========语句4 ================
INSERT INTO z SELECT 8,6;
INSERT INTO z SELECT 2,0;
INSERT INTO z SELECT 6,7;

事实上在加锁时,不仅会对主键唯一索引加锁,同时也会对行记录中的辅助索引加锁。

当我们在执行语句1时,对辅助索引b=3这个值加X锁,此时已经对a=5也加了X锁,因此在此时不能执行语句2。

当此时数据库隔离级别为RR时,由于间隙锁的原因,此时会对b值为(1,3)和(3,6)这个范围也加上锁,因此在执行语句3时也会被阻塞。但是当执行语句4时不会被阻塞。因为语句4中b值得范围没有被加间隙锁。

那为什么主键索引没有被间隙锁限制呢?
这是因为间隙锁在查询的列为唯一索引时,会自动降级为记录锁。提高数据库的并发性。所以主键索引不会受到影响。你想想,当条件是唯一索引时,怎么查数据量也不会再增多了吧,就那一条记录。

下面来继续介绍一下上面的知识中涉及到的一些基本的概念。ReadView和数据库锁。

ReadView

ReadView中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。
假设当前列表里的事务id为[80,100]。

  • 如果你要访问的记录版本的事务id为50,比当前列表最小的id80小,那说明这个事务在之前就提交了,所以对当前活动的事务来说是可访问的。
  • 如果你要访问的记录版本的事务id为90,发现此事务在列表id最大值和最小值之间,那就再判断一下是否在列表内,如果在那就说明此事务还未提交,所以版本不能被访问。如果不在那说明事务已经提交,所以版本可以被访问。
  • 如果你要访问的记录版本的事务id为110,那比事务列表最大id100都大,那说明这个版本是在ReadView生成之后才发生的,所以不能被访问。

那么通过对ReadView的了解,我们就知道READ UNCOMITTED为什么能读到脏数据了。而READ COMMITTED则可根据这个链表来读取必须是已经提交了的数据。

举个例子。此例子来源于其他博客:https://baijiahao.baidu.com/s?id=1629409989970483292&wfr=spider&for=pc 来帮助理解。

在RR级别下:
比如此时有一个事务id为100的事务,修改了name,使得的name等于小明2,但是事务还没提交。则此时的版本链是
【mysql】事务的隔离级别有哪些?各个级别造成的问题是什么?Innodb分别是如何解决他们的?_第1张图片
那此时另一个事务发起了select 语句要查询id为1的记录,那此时生成的ReadView 列表只有[100]。那就去版本链去找了,首先肯定找最近的一条,发现trx_id是100,也就是name为小明2的那条记录,发现在列表内,所以不能访问。

这时候就通过指针继续找下一条,name为小明1的记录,发现trx_id是60,小于列表中的最小id,所以可以访问,直接访问结果为小明1。

那这时候我们把事务id为100的事务提交了,并且新建了一个事务id为110也修改id为1的记录,并且不提交事务。

这时候版本链就是
【mysql】事务的隔离级别有哪些?各个级别造成的问题是什么?Innodb分别是如何解决他们的?_第2张图片
这时候之前那个select事务又执行了一次查询,要查询id为1的记录。

由于在RR级别下,此时读取到的ReadView还是最开始生成的独立的ReadView,因此读取到的还是60版本的小明1.
但是如果此时在RC级别下,再次select会重新生成一个ReadView此时,链表中为110,100已经不在链表中,说明该事务已经提交,所以读取到的值为事务id为100的值小明2.

根据时间轴如下表更清晰:
【mysql】事务的隔离级别有哪些?各个级别造成的问题是什么?Innodb分别是如何解决他们的?_第3张图片

数据库中的锁

锁时数据库区别于文件系统的一个重要特性。用于管理对共享资源的并发访问。

锁的类型:

  • 共享锁 S:允许事务读一行数据。
  • 排他锁 X:允许事务删除或更新一条数据。
    兼容性:
    【mysql】事务的隔离级别有哪些?各个级别造成的问题是什么?Innodb分别是如何解决他们的?_第4张图片

一致性非锁定读:

指的是INNODB存储引擎通过行多版本控制的方式来读取当前执行的时间数据库中行的数据。如果读取的行正在执行DETELE或UPDATE操作,这是读取操作不会因此等待行上的锁的释放。相反地,会去读取行的一个快照数据。

正如上面小明的例子中,事务101第二次select时,其实事务110已经执行了update操作但是并没有提交这时采用的就是一致性非锁定读。是为了提高并发性。

一致性锁定读:

虽然数据库为了提高并发现支持一致性非锁定度,但是某些情况下用户需要显式地对数据库读取操作进行加锁以保证数据逻辑一致性。这时需要数据库支持加锁语句。即使是select语句。

  • SELECT … FOR UPDATE -------X锁
  • SELECT … LOCK IN SHARE MODE; -------S锁

锁算法

  • Record Lock: 单个行记录上的锁
  • Gap Lock: 间隙锁,锁定一个范围,但不包含记录本身
  • Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。对应的还有previous-key locking

以上是关于mysql数据库 innodb存储引擎的事务隔离级别相关的知识。

你可能感兴趣的:(mysql,innodb)