可重复读隔离级别,不允许存在幻读,该隔离级别之所以能够有效防止幻读现象的出现,是因为可重复读这个隔离级别有用到GAP锁(间隙锁)。下面我们以解析SQL语句为切入点,来解释个中原因。
前提条件:①数据库的存储引擎为InnoDB; ②数据库的隔离级别为“可重复读”。
SQL:DELETE FROM user WHERE id = 10;
(1)当id是聚簇索引或唯一索引时:
此时是没有使用到GAP锁的,但是也保证了幻读现象的出现。
原因:如果id是主键,那么主键必然是“唯一且不为空”的;如果id是唯一索引,那么也保证了数据的唯一性。一个等值查询,最多只能返回一条记录,而且新的相同取值的记录,一定不会在新插入进来,因此也就避免了GAP锁的使用。
(2)当id是非唯一索引时:
此时就会用到GAP锁,此时的加锁行为如下图所示:
GAP锁锁住的位置,并非记录本身,而是两条记录之间的GAP。
没有幻读:就是同一个事务中,连续做两次当前读 (例如:SELECT * FROM user WHERE id = 10 FOR UPDATE;),这两次当前读返回的是完全相同的记录 (记录数量一致,记录本身也一致),即第二次的当前读,不会比第一次返回更多的记录 (幻象)。
如何保证两次当前读返回一致的记录,那就需要在第一次当前读与第二次当前读之间,其他的事务不会插入新的满足条件的记录并提交。为了实现这个功能,GAP锁应运而生。
如图中所示,有哪些位置可以插入新的满足条件的项 (id = 10),考虑到B+树索引的有序性,满足条件的项一定是连续存放的。记录[6,c]之前,不会插入id=10的记录;[6,c]与[10,b]间可以插入[10, a]、[10,aa]等,[10,b]与[10,d]间,可以插入新的[10,bb],[10,c]等;[10,d]与[11,f]间可以插入满足条件的[10,e],[10,z]等;而[11,f]之后也不会插入满足条件的记录。因此,为了保证[6,c]与[10,b]间,[10,b]与[10,d]间,[10,d]与[11,f]不会插入新的满足条件的记录,MySQL选择了用GAP锁,将这三个GAP给锁起来。
Insert操作,如insert [10,aa],首先会定位到[6,c]与[10,b]间,然后在插入前,会检查这个GAP是否已经被锁上,如果被锁上,则Insert不能插入记录。因此,通过第一遍的当前读,不仅将满足条件的记录锁上 (X锁),同时还是增加3把GAP锁,将可能插入满足条件记录的3个GAP给锁上,保证后续的Insert不能插入新的id=10的记录,也就杜绝了同一事务的第二次当前读,出现幻象的情况。
结论:可重复读隔离级别下,id列上有一个非唯一索引,对应SQL:DELETE FROM user WHERE id = 10;首先,通过id索引定位到第一条满足查询条件的记录,加记录上的X锁,加GAP上的GAP锁,然后加主键聚簇索引上的记录X锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录[11,f],此时,不需要加记录X锁,但是仍旧需要加GAP锁,最后返回结束。
参考博客:http://hedengcheng.com/?p=771#_Toc374698322【作者:何登成 原文:MySQL 加锁处理分析】