MySQL脏读幻读不可重复读及事务的隔离级别和MVCC、LBCC实现 |
前言上一篇文章讲解了MySQL的事务的相关概念MySQL的事务特性概念梳理总结
事务因并发出现的问题有哪些 脏读概念:一个事务读取到其他事务未提交的数据 。 不可重复读概念:一个事务在一个时间段内 前后读取的数据不一致,或者出现了修改/删除 。 幻读概念:事务A 按照查询条件读取某个范围的记录,其他事务又在该范围内出入了满足条件的新记录,当事务A再次读取数据到时候我们发现多了满足记录的条数(幻行)
不可重复读与幻读的区别前提:两者都是读取到已经提交的数据 不可重复读:重点是在于修改,在一个事务中,同样的条件,第一次读取的数据与第二次【数据不一样】(因为中间有其他事务对这个数据进行了修改) 我们现在已经知道,原来事务并发会出现,脏读,不可重复读,幻读的问题 。 事务并发的三大问题其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决 。 事务的四个隔离级别我们通过事务的隔离级别来解决不同的问题,那么,不同的隔离级别解决了什么问题呢? 其实sql标准92版 官方都有定义出来
官网支持四种隔离级别: # 修改当前会话的隔离级别 # 读未提交 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; # 读已提交 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; # 可重复读 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; # 串行化 SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; 我们也可以通过SQL去查询当前的隔离级别 SHOW GLOBAL VARIABLES LIKE '%isolation%'; //全局隔离级别 SHOW SESSION VARIABLES LIKE '%isolation%'; set SESSION autocommit=0; //关闭自动提交 InnoDB默认的隔离级别是RR事务隔离级别越高,多个事务在并发访问数据库时互相产生数据干扰的可能性越低,但是并发访问的性能就越差 。(相当于牺牲了一定的性能去保证数据的安全性) Read UnCommited 读未提交 RU多个事务同时修改一条记录,A事务对其的改动在A事务还没提交时,在B事务中就可以看到A事务对其的改动 。
Read Commited 读已提交 RC多个事务同时修改一条记录,A事务对其的改动在A事务提交之后,在B事务中可以看到A事务对其的改动 。
Repeatable Read 可重复读 RR多个事务同时修改一条记录,这条记录在A事务执行期间是不变的(别的事务对这条记录的修改不被A事务感知) 。
Serializable 串行化多个事务同时访问一条记录(CRUD),读加读锁,写加写锁,完全退化成了串行的访问,自然不会收到任何其他事务的干扰,性能最低 。
可以看出,RU与串行化都没啥实用意义,主要还是看RC和RR,那么Mysql是怎么实现这两种隔离级别的呢? undo 版本链undo 版本链就是指undo log的存储在逻辑上的表现形式,它被用于事务当中的回滚操作以及实现 对于每一行记录,会有两个隐藏字段: 在上图中,最下方的undo log中记录了当前行的最新版本,而该条记录之前的版本则以版本链的形式可追溯,这也是事务回滚所做的事 。那undo log版本链和事务的隔离性有什么关系呢?那就要引入另一个核心机制:read view 。 read viewread view表示读视图,这个快照读会记录四个关键的属性:
当一个事务读取某条记录时会追溯undo log版本链,找到第一个可以访问的版本,而该记录的某一个版本是否能被这个事务读取到遵循如下规则:
RR中 Read View是事务第一次查询的时候建立的 。RC的Read View是事务每次查询的时候建立的 。 Oracle、Postgres等等其他数据库都有MVCC的实现 。 需要注意,在InnoDB中,MVCC和锁是协同使用的,这两种方案并不是互斥的 。 配合使用read view和undo log版本链就能实现事务之间并发访问相同记录时,可以根据事务id不同,获取同一行的不同undo log版本(多版本并发控制) 。 MVCC(Multi-Version Concurrent Control )多版本并发控制多版本并发控制,是什么意思呢?版本控制,我们在进行查询的时候是有版本的,后续在同一个事务里查询的时候,我们都是使用我们当初创建的快照版本 。 MVCC怎么去实现? 一个事务能看见的版本:
一个事务不能看见的版本:
下面通过模拟并发访问的两个事务操作,介绍MVCC的实现(具体来说就是可重复读和读已提交两个隔离级别的实现) 可重复读实现下面模拟两个并发访问同一条记录的事务AB的行为,假设这条记录初始时id=1,a=0,该记录两个隐藏字段row_trx_id = 100,roll_pointer = null 事务A的read view:
事务B的read view:
(ps. 这里因为AB事务是并发执行,因此两个事务创建的read view的max_trx_id = 103) 这里要注意的是,每次对一条记录发生修改,就会记录一个undo log的版本,则在A事务中第二次查询id=1的记录的a的值的时候,B事务对该记录的修改已经添加到版本链上了,此时这个undo log的trx_id = 102,在A事务的read view的m_idx数组中且不等于A事务的trx_id = 101,因此无法访问到,需要在向前回溯,这里找到trx_id = 100的记录版本(小于A事务read view的min_trx_id属性,因此可以访问到),故A事务第二次查询依旧得到a = 0,而不是B事务修改的a = 1 。 你可能有疑问,在A事务第二次查询的时候,B事务已经完成提交了,那么A事务的read view的m_idx数组应该移除102才对啊,它存的不是当前活跃的事务的id吗?·
那么也就明白了,在可重复读隔离级别下,因为read view只在第一条SQL执行时创建,因此并发访问的其他事务提交前改动的脏数据、以及并发访问的其他事务提交的改动数据都对当前事务是透明的(尽管确实是记录在了undo log版本链中) ,这就解决了脏读和不可重复读(即使其他事务提交的修改,对A事务来说前后查询结果相同)的问题! 读已提交实现还是借助上面事务处理的例子,所有的事务处理流程不变,只是将隔离级别调整为读已提交,读已提交依旧遵守read view和undo log版本链机制,它和可重复读级别的区别在于,每次执行sql,都会创建一个read view,获取最新的事务快照 。 而因为这个区别,读已提交产生了不可重复读的问题,下面来分析一下原因: 事务A第一次查询创建的read view:
事务B的read view:
事务A第二次查询创建的read view:
(ps. 这里因为AB事务是并发执行,因此两个事务创建的read view的max_trx_id = 103) 这里重点观察A事务的第二次查询,之前你可能就意识到了,在事务B完成提交后,当前系统中活跃的事务id应该移除102,但是因为在可重复读隔离级别下,A事务的read view只会在第一个SQL执行时创建,而在读已提交隔离级别下,每次执行SQL都会创建最新的read view,且此时 m_idx数组中移除了102,那么事务A在追溯undo log版本链的时候,最新版本记录的trx_id = 102,102不在A事务的m_idx数组中,且101 = min_trx_id <= 102 < max_trx_id = 103,因此可以访问到B事务的提交结果 。 那么对A事务来说,在事务过程中读取同一条记录第一次得到a=0,第二次得到a=1,所以出现了不可重复读的问题(这里B不提交的话A如果就进行了第二次查询,则102不会从A事务的read view移除,则A事务依旧访问不到B事务未提交的修改,因此脏读还是可以避免的!)
但是,大家有没有想过,刚才的一切都是对A提供便利,对B呢? LBCC锁的作用是什么?它跟Java里面的锁是一样的,是为了解决资源竞争的问题,Java里面的资源是对象,数据库的资源就是数据表或者数据行 。 基于锁的方式起始比较简单,就是一个事务在进行数据查询时,不允许其他事务修改 。也就是说,基于锁的机制就使得数据库无法支持并发事务的读写操作,这种方案在一定程度上影响了操作数据的效率 。 本文着重讲InnoDB引擎
在之前讲MySQL存储引擎的时候,我们知道了 InnoDB和MylSAM支持的锁 的类型是不同的 。InnoDB同时支持表锁和行锁,而MylSAM只支持表锁,用lock table的语法加锁 。 lock tables xxx read; lock tables xxx write; unlock tables ; 为什么支持行锁会成为InnoDB的优势?表锁和行锁的区别到底在哪?
锁的类型我们可以看到,官网把锁分成了8类 。我们把前面的两个行级别的锁(Shared andExclusive Locks),和两个表级别的锁(Intention Locks)称为锁的基本模式 。
show variables like 'innodb_autoinc_lock_mode'; --0: traditonal(每次都会产生表锁) --1: consecutive(会产生一个轻量锁,simple insert 会获得批量的锁,保证连续插入,默认值) --2: interleaved(不会锁表,来一个处理一个,并发最高) 空间索引的谓词锁:Predicate Locks for Spatial Indexes是5.7版本里面新增的空间索引的谓词锁 。 共享锁第一个行级别的锁就是我们在官网看到的Shared Locks(共享锁),我们获取了一行数据的读锁以后,可以用来读取数据,所以它也叫做读锁,注意不要在加上了读锁以后去写数据,不然的话可能会出现死锁的情况 。而且多个事务可以共享一把读锁 。 共享锁的作用:因为共享锁会阻塞其他事务的修改,所以可以用在不允许其他事务修改数据的情况 。 排它锁第二个行级别的锁叫做Exclusive Locks(排它锁),它是用来操作数据的,所以又叫做写锁 。只要一个事务获取了一行数据的排它锁,其他的事务就不能再获取这一行数据的共享锁和排它锁 。 排它锁的加锁方式有两种 这个是两个行锁,接下来就是两个表锁 。 意向锁意向锁是什么呢?我们好像从来没有听过,也从来没有使用过,其实他们是由数据库自己维护的 。 也就是说:
反过来:
意向锁跟意向锁是不冲突的,意向锁跟行锁也不冲突 。 那么这两个表级别的锁存在的意义是什么呢?
所以锁是用来解决事务对数据的并发访问的问题的 。那么,锁到底锁住了什么呢? 行锁的原理没有索引的表首先我们有三张表,一张没有索引的t1,一张有主键索引的t2,一张有唯一索引的t3 。 我们在两个会话里面手工开启两个事务 。 很遗憾,我们看到红灯亮起,这个加锁的操作被阻塞了 。这就有点奇怪了,第一个事务锁住了id=1的这行数据,为什么我不能操作id=3的数据呢? 有主键索引的表我们假设t2的表结构 。字段和t1是一样的,不同的地方是id上创建了一个主键索引 。里面的数据是1、4、7、10 。 第一种情况,使用相同的id值去加锁,冲突;使用不同的id 加锁,可以加锁成功 。那么,既然不是锁定一行数据,有没有 有唯一索引的表(上面假设锁住了字段)我们假设t3的表结构字段还是一样的, id上创建了一个主键索引,name 上创建了一个唯一索引 。里面的数据是1、4、7、10 。 在第一个事务里面,我们通过name字段去锁定值是4的这行数据 。 很遗憾,又被阻塞了,说明行锁锁住的是字段的这个推测也是错的,否则就不会出现第一个事务锁住了name,第二个字段锁住id失败的情况 。 既然锁住的不是record,也不是column,,行列都没锁,那InnoDB的行锁锁住的到底是什么呢?在这三个案例里面,我们要去分析一下他们的差异在哪里,也就是这三张表的结构,是什么区别导致了加锁的行为的差异?其实答案就是索引 。InnoDB的行锁,就是通过锁住索引来实现的 。 那么我们还有两个问题没有解决:
所以,一张表有没有可能没有索引?
所以,为什么锁表,是因为查询没有使用索引,会进行全表扫描,然后把每一个隐藏的聚集索引都锁住了 。
大家还记得在InnoDB里面,当我们使用辅助索引(二级索引)的时候,它是怎么检索数据的吗?辅助索引的叶子节点存储的是什么内容? 本质上是因为锁定的是同一行数据,是相互冲突的 。 InnoDB中LBCC要解决的问题问题1-幻读问题(InnoDB) 范围查询的时候,多次查询结果的数据行数一致 select * from table where id >=1 and id<=4 //锁定2,3 [解决幻读问题] 问题二, for update 实现了排他锁(行锁) --transaction1 select * from table where id=1 for update; //查询主键id=1 (行 锁,只锁定行) --transaction2 update table set name='111' where id=1; //阻塞 update table set name='222' where name =''; //阻塞 基于索引来决定的,如果where是索引,那么这个时候,直接加行锁. 问题三, 锁定整个表 select * from table for update; //表锁 update table set name='111' where id=1; //阻塞 锁的算法我们先来看一下我们测试用的表,t2,这张表有一个主键索引,前面我们已经见过了 。我们插入了4行数据,主键id分别是1、4、7、10 。
任何一个字符集,都有相应的排序规则: Record Lock (记录锁) [锁定的是索引]第一种情况,当我们对于唯一性的索引(包括唯一索引和主键索引)使用等值查询,精准匹配到一条记录的时候,这个时候使用的就是记录锁 。 顾名思义,记录锁就是为某行记录加锁,它封锁该行的索引记录,并不是真正的数据记录,锁的是索引的键值对 。 -- 记录锁:id 列为主键列或唯一索引列 SELECT * FROM user WHERE id = 1 FOR UPDATE; --意味着id=1的这条记录会被锁住 Gap Lock(间隙锁 锁定索引区间,不包括record lock)第二种情况,当我们查询的记录不存在,没有命中任何一个record,无论是用等值查询还是范围查询的时候,它使用的都是间隙锁 。 顾名思义 锁间隙,不锁记录 。 SELECT * FROM user WHERE id BETWEN 1 AND 4 FOR UPDATE; 那么意味着所有在(1,4)区间内的记录行都会被锁住,它是一个左右开区间的范围,意味着在这种情况下, 会锁住id为2,3的索引,但是1、4不会被锁定 next Key Lock(临键锁 锁定索引区间,包括record lock)第三种情况,当我们使用了范围查询,不仅仅命中了Record记录,还包含了Gap间隙,在这种情况下我们使用的就是临键锁,它是MySQL里面默认的行锁算法,相当于记录锁加上间隙锁 。
next Key Lock 可以理解为一种特殊的间隙锁,也可以理解为一种特殊的算法,每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据 。 为什么要锁住下一个左开右闭的区间?——就是为了解决幻读的问题 。 小结所以,我们再回过头来看下这张图片,为什么InnoDB的RR级别能够解决幻读的问题,就是用临键锁实现的 。 最后我们来总结一下四个事务隔离级别: Read Uncommited 这两个很好理解,一般也不用,主要是RR和RC的区别? Repeatable Read:RR隔离级别下,普通的select使用快照读(snapshot read),底层使用MVCC来实 Read Commited:RC隔离级别下,普通的select 都是快照读,使用MVCC 实现 。加锁的select都使用记录锁,因为没有Gap Lock 。 除了两种特殊情况——外键约束检查(foreign-key constraint checking)以及重复键检查(duplicate-key checking)时会使用间隙锁封锁区间 。 事务隔离级别怎么选?RU和Serializable肯定不能用 RC和RR主要有几个区别:
在RC中,一个update语句,如果读到一行已经加锁的记录,此时 InnoDB返回记录最近提交的版本,由MySQL上层判断此版本是否满足update的where 条件 。若满足(需要更新),则MySQL会重新发起一次读操作,此时会读取行的最新版本(并加锁) 。 实际上,如果能够正确地使用锁(避免不使用索引去枷锁),只锁定需要的数据,用默认的RR级别就可以了 到此这篇关于MySQL脏读幻读不可重复读及事务的隔离级别和MVCC、LBCC实现的文章就介绍到这了,更多相关MySQL脏读幻读 内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持! |