上篇文章总结了有关事务的原子性,持久性和一致性【mysql】Innodb存储引擎是如何保证事务的ACID四个原则的,接下来是事务的隔离性。
RU:
问题:读取脏数据,不可重复读,幻像
原因:在RU隔离级别下,读未提交的数据,所以,当事务还没有提交时,数据并不是最终的数据,另一个事务就有可能读取到脏数据。
RC:
问题:不可重复读,幻读
原因:
不可重复读:在对数据进行修改时,会产生undo log,在undo log中,数据会有多个版本的快照数据这种技术成为行多版本技术(请阅读下面 undo log 存储版本链)。在RC级别下,innodb存储引擎每次读取的历史版本是最新的版本数据,因此,当有新的事务提交产生新的版本数据时,RC级别读取到的数据会发生不一致这种现象称为不可重复读(每次select会产生一个新的readView导致)。
幻读:在事务执行select操作加锁时,仍然可以进行insert操作对数据进行更改,再次执行select操作得到的结果集不一致的问题。这是因为在RC级别下不支持间隙锁,当select时加锁只能对记录本身的索引值加锁,不能对范围进行加锁,以致于可以继续插入范围内的数据造成幻读(下面解决方案中会举例说明)。
RR
问题:在RR级别下,innodb存储引擎解决了RC下的问题。可以达到隔离性的要求。
SR
此级别下由于是一个事务执行完成,另一个事务才能开始执行,事务级别最高,因此可以达到隔离性。
版本链类其实是采用了乐观锁的机制。
在InnoDB引擎表中,它的聚簇索引记录中有两个必要的隐藏列:
对于不同的操作,存储的数据如下:
SELECT
innodb会根据以下两个条件检查每行记录:
a.innodb只查找版本号早于当前事务版本的数据行,<=当前事务版本号,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的
b.行的删除版本要么未定义,要么大于当前的事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。
INSERT
INNODB为新插入的每一行保存当前系统版本号作为行版本号
DELETE
innodb为删除的每一行保存当前系统版本号作为行删除标识
UPDATE
innodb为插入一行新纪录,保存当前系统版本号为行版本号,同时保存当前系统版本号到原来的行作为行删除标识
问题描述
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中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。
假设当前列表里的事务id为[80,100]。
那么通过对ReadView的了解,我们就知道READ UNCOMITTED为什么能读到脏数据了。而READ COMMITTED则可根据这个链表来读取必须是已经提交了的数据。
举个例子。此例子来源于其他博客:https://baijiahao.baidu.com/s?id=1629409989970483292&wfr=spider&for=pc 来帮助理解。
在RR级别下:
比如此时有一个事务id为100的事务,修改了name,使得的name等于小明2,但是事务还没提交。则此时的版本链是
那此时另一个事务发起了select 语句要查询id为1的记录,那此时生成的ReadView 列表只有[100]。那就去版本链去找了,首先肯定找最近的一条,发现trx_id是100,也就是name为小明2的那条记录,发现在列表内,所以不能访问。
这时候就通过指针继续找下一条,name为小明1的记录,发现trx_id是60,小于列表中的最小id,所以可以访问,直接访问结果为小明1。
那这时候我们把事务id为100的事务提交了,并且新建了一个事务id为110也修改id为1的记录,并且不提交事务。
这时候版本链就是
这时候之前那个select事务又执行了一次查询,要查询id为1的记录。
由于在RR级别下,此时读取到的ReadView还是最开始生成的独立的ReadView,因此读取到的还是60版本的小明1.
但是如果此时在RC级别下,再次select会重新生成一个ReadView此时,链表中为110,100已经不在链表中,说明该事务已经提交,所以读取到的值为事务id为100的值小明2.
锁时数据库区别于文件系统的一个重要特性。用于管理对共享资源的并发访问。
锁的类型:
一致性非锁定读:
指的是INNODB存储引擎通过行多版本控制的方式来读取当前执行的时间数据库中行的数据。如果读取的行正在执行DETELE或UPDATE操作,这是读取操作不会因此等待行上的锁的释放。相反地,会去读取行的一个快照数据。
正如上面小明的例子中,事务101第二次select时,其实事务110已经执行了update操作但是并没有提交这时采用的就是一致性非锁定读。是为了提高并发性。
一致性锁定读:
虽然数据库为了提高并发现支持一致性非锁定度,但是某些情况下用户需要显式地对数据库读取操作进行加锁以保证数据逻辑一致性。这时需要数据库支持加锁语句。即使是select语句。
锁算法
以上是关于mysql数据库 innodb存储引擎的事务隔离级别相关的知识。