这听起来似乎没什么不对的,但是你仔细想一想,InnoDB中的行锁,锁住的是已经存在的数据。而对于即将要插入的数据,为什么也会被锁住呢?这是不符合行锁的定义的。
这个时候就可以说到间隙锁了。
简单来讲,就是这条语句不仅会锁住所查询的那行数据,还会把这行数据周围的间隙锁住,不让其他事务插入。
也就是说,行锁是锁住已有的数据,而间隙锁,是锁住即将要插入的位置,不让其他数据插入。
在官方文档有这么一句话:
Gap locking can be disabled explicitly. This occurs if you change the transaction isolation level to READ COMMITTED or enable the innodb_locks_unsafe_for_binlog system variable (which is now deprecated).
也就是说,间隔锁在“可重复读”事务隔离级别是默认生效的。所以,MySQL在“可重复读”的事务隔离级别下,是有办法解决幻读问题的。
下面我们来看看哪些情况InnoDB会给数据加上间隔锁,并且这里的间隔锁范围有多大,注意,下面列举的四种情况,指的是where条件中的字段的索引类型。
主键索引
唯一普通索引
非唯一普通索引
无索引
先定义这么一个表:
CREATE TABLE `t` ( `id` int(11) NOT NULL, `a` int(11) DEFAULT NULL, `b` int(11) DEFAULT NULL, `c` int(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `a` (`a`), KEY `b` (`b`) ) ENGINE=InnoDB;id是主键,a是一个唯一索引,b是一个普通索引,c不包含任何的索引字段。
然后插入以下的这些数据:
insert into t values(0,0,0,0),(5,5,5,5),(10,10,10,10);然后我们开始分析各种情况。
2.2 主键索引因为没有其他的数据,所以主键索引在数据页内的编排如上图,并且含有4个空隙。这里说的“空隙”,指的是数据可以插入的位置。
比如我要插入一个id为3的数据,这条数据就会插入到位于(0,5)这个空隙内。
下面我们开始尝试:
毫无疑问T3时刻的sql语句是会被阻塞的,原因是id = 5的这行数据已经被加锁了。那么,会不会存在有间隙锁呢?
因为这是一个主键索引,InnoDB必须保证id = 5的数据是唯一的,所以对于id=5的周围,比如(0,5)和(5,10),不需要再加间隙锁了。
那么换一个条件再试试,我们查找id大于6且id小于8的数据,此时事务B中的语句同样会被阻塞。
这是因为,在主键索引没有命中的时候,会对所在的空白范围,全部加锁。注意,我这里说的是未命中的所有空白范围,哪怕我这里的查找条件是大于6且小于8,但是加锁的范围不是(6,8),而是(5,10)。
你可以简单的理解为:从查找条件的最小值开始,往前找到第一个索引值;并且从查找条件的最大值开始,往后找到第一个索引值,这个范围就是加锁的范围。
你可能还会有一个疑问,如果是select * from t where id = 8 for update会怎么样呢?这个问题和上面一样,只要未命中,就加范围锁,锁住空隙(5,10)。
总结一下:对于主键索引来说,命中了,就只加行锁;没命中,则对查找范围的最小值往前找第一个主键,查找范围的最大值往后找第一个主键,并对这个范围加上间隙锁。
2.3 唯一索引对于唯一索引来说,和主键索引其实是差不多的。当索引命中之后,因为唯一索引同样保证了索引的唯一性,所以不需要给这行数据的周围加上间隙锁,只会给命中的数据加锁。
但是这里和主键索引不同的地方是,在给唯一索引a = 5加锁的同时,还会回表,将a = 5对应的主键id = 5这行记录加锁。所以,事务B的修改也同样会被阻塞。
这也是为了防止造成数据不一致的情况,比如我把a = 5的这行数据删了,然后事务B又通过这行数据的主键来对这行数据进行操作。
对于带有范围的查找,和上面主键索引的间隙锁规则是一样的,这里不再赘述。值得注意的是,在唯一索引中,只要命中了,就会相应的给这条索引对应的主键id也加锁。