四 事务隔离级别以及各级别下的并发访问问题
现在来看看MySQL数据库为我们提供的四种隔离级别: ① Serializable (串行化):可避免脏读、不可重复读、幻读的发生。 ② Repeatable read (可重复读):可避免脏读、不可重复读的发生。 ③ Read committed (读已提交):可避免脏读的发生. ④ Read uncommitted (读未提交):最低级别,任何情况都无法保证。 事务隔离级别对应可以规避的问题
开启间隙锁, 间隙锁会封锁该条记录相邻两个键之间的空白区域,防止其它事务在这个区域内插入、修改、删除数据;所谓间隙是将数据分为不同区间,对该区间范围进行加锁,区间的规则为左开右闭,比如当数据为1,3,5时,对应的区间为(-∞,1],(1,3],(3,5],(5,+∞];
他这个行锁+间隙锁就组成了next—key lock(Next-Key Locks,就是Record lock和gap lock的结合,即除了锁住记录本身,还要再锁住索引之间的间隙。),innodb在可重复度隔离级别下,采用next-key lock来防止幻读,因此实现了最高的隔离级别。
底层实现离不开数据行里的DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID字段,除此之外还需要undo日志,以及read view。
原理实现就是下列几个关键内容:
说起DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID,那就要先知道MySQL一条记录是由记录的额外信息部分和记录的真实数据两部分组成。记录的额外记录部分存有变长字段长度列表、NULL值列表等,而记录的真实数据部分又由真实数据以及DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID这三个隐藏列组成。
比如现在有一个记录Field1、Field2、Field3数据分别为11、12、13,现在事务要修改该记录,将Field2修改为32。则这条记录首先会加载X锁,首先undo log中会拷贝一条修改前的记录,并赋值DB_ROW_ID。此时被X锁锁住的记录的DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID分别进行赋值,并且DB_ROLL_PTR的记录会指向undo log中的DB_ROW_ID的值。
如果此时又有一个事务对该记录进行了修改,则undo log日志中又会增加一条日志。
这样就是快照读版本的实现了。
其实,真正实现RR隔离级别下的幻读现象,是由next-key锁解决的。next-key锁又分为了(行锁 + gap锁)
5.2.1行锁 行锁就是Record Lock,就是对单个行记录加的锁。X锁和S锁就是行锁。
5.2.2 Gap锁 Gap就是索引树种,插入新数据的间隙。间隙锁即锁定一个记录的范围,但是不锁定记录本身。间隙锁是为了避免同一事务的两次当前读出现幻读的情况。需要注意的是,Gap锁在RU、RC隔离级别下时不存在的,在RR、Serializable隔离级别下都只支持Gap锁。这就是为什么RU、RC隔离级别下无法避免幻读,RR、Serializable能够避免幻读的原因。
下面讨论的都是在RR隔离级别下出现Gap锁的场景。
1.在RR隔离级别下,无论删、改、查,当前读若用到主键索引或者唯一键索引,会使用Gap锁吗?
答:如果where条件全部命中,则不会用Gap锁,只会加记录锁。
怎么去理解where条件全部命中,不用加Gap锁只需要加记录锁就行了呢?这是因为比如A事务需要修改操作所有记录,此时B事务使用主键索引id来进行where条件查询来进行删除操作,此时只需要锁住where命中的id记录即可,那么就能防止事务A出现幻读现象。
如图,tb中name为主键索引,id为唯一索引。某个事务使用delete from tb where id = 9进行删除操作,首先where条件全部命中,所以先会为id为9的这个记录的唯一索引加上行锁,然后会为name为d的主键索引(聚镞索引)加上排他锁。这是为了防止其他事务对where name = 9进行操作,导致数据不一致的情况。
2.在RR隔离级别下,无论删、改、查,当前读若用到主键索引或者唯一键索引,且如果where条件部分命中
或者全不命中,则会加Gap锁。对于这种情况,就包含了范围查询以及精确查询非全部命中的情况。
例子1:比如现在事务A要删除一条不存在的id为7的记录,此时事务B要新增一条id为8的记录,会发现事务B一直
处于等待中,这是因为精准查询全部都不命中,会对该记录范围加Gap锁。
例子2:【tb_student中存在id为5,6,9的学生】比如在事务A中使用语句select * from tb_student where id in (5,7,9) lock in share mode;使用当前读(共享锁)来查询学生信息。在另外一个事务B中去进行新增id为6,7,8的学生,发现事务一直在等待中。这里是因为where id in (5,7,9)部分命中,所以会为(5,9]加Gap锁,锁的范围为左开右闭。因此事务B新增id为7,8的记录会被Gap锁锁住,这就是精准查询不全部命中的情况。
3.Gao锁会用在非唯一索引或者不走索引的当前读中
非唯一索引
比如图中某一事务A执行delete from tb1 where id = 9,因为id是非唯一索引,如果没有加Gap锁,在事务B新增一条id为9的记录时,A事务执行完delete语句后,就会发现成功删除3条记录,出现了幻觉,所以给id为9的记录加上Gap锁来防止幻读的发生。 至于Gap锁的范围,如上为:(-∞,2], (2, 6], (6, 9], (9, 11], (11, 15], (15, +∞)中的 (6, 9], (9, 11]
不走索引
对于不走索引的情况,InnoDB会为所有的Gap加锁,相当于锁表。
表象:快照读(非阻塞读),伪MVCC 底层:next-key(行锁+Gap锁) a. 在RU、RC隔离级别下不存在Gap锁,所以在RU、RC隔离级别下无法解决幻读;在RR、Serializable隔离级别下都实现了Gap锁,所以解决了幻读现象。 b. 在RR隔离级别下,如果删、改、查语句的where条件走的是主键索引或者唯一索引 i. where条件全部命中,则给该记录加上记录锁。 ii. where条件不全部命中,则给该记录周围加上Gap锁。 iii. 加上记录锁或者是Gap锁都是为了防止RR隔离级别下发生幻读现象。 c. 在RR隔离级别下,如果删、改、查语句的where条件没有走索引或者是非唯一索引或非主键索引 在当前读where条件如果没有走非唯一索引或者没有走索引,则会使用Gap锁锁住当前记录的Gap,防止幻读的发生
记录中存储的隐藏列DB_TRX_ID、DB_ROW_ID、DB_ROLL_ID undo日志根据上述隐藏列来进行记录数据回滚(版本回滚) review机制