The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.
以上为MySQL官方定义,我们简单翻译下:幻读湖现在一个事务中,同一个查询语句在不同的时间返回了不同的结果。比如:一个select语句执行了两次,但是第二次返回的rows不是第一次返回的,那么这行就可以称之为幻读。
在数据库定义的四种隔离级别中最高隔离级别SERIALIZABLE_READ可以保证不出现幻读的问题。而RR(Repeatable Read)级别下,针对当前读,RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),不存在幻读现象。
但是,严格意义上,间隙锁并没有完全解决幻读,比如如下:
set session transaction isolation level repeatable read;
首先设置事务隔离级别为 RR。
Session A | Session B |
---|---|
start transaction; | start transaction; |
select * From test where id < 10; | |
insert into test values(5, 2, 2); | |
insert into test values(5, 2, 2); | |
commit; | |
commit; |
在如上的插入场景下,A事务读取行数并没有id为5的记录,但是当我们插入记录时,产生了锁等待,一定意义上也产生了幻读。
我们知道在RC(Read Committed)级别下,我们产生的行锁。
而在RR级别下,引入了新的锁GAP(间隙锁)和Next-Key locking。
To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row locking with gap locking. InnoDB performs row-level locking in such a way that when it searches or scans a table index, it sets shared or exclusive locks on the index records it encounters. Thus, the row-level locks are actually index-record locks. In addition, a next-key lock on an index record also affects the “gap” before that index record. That is, a next-key lock is an index-record lock plus a gap lock on the gap preceding the index record. If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order.
如上为5.7版本的解决方案,我们简单翻译下:为了阻止幻读,InnoDB提供一种称为next-key locking的算法,next-key locking结合了索引行锁和gap locking。nex-key lock不仅会锁定当前的索引行,还会锁定索引行前后的记录。
A gap lock is a lock on a gap between index records, or a lock on the gap before the first or after the last index record. For example, SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; prevents other transactions from inserting a value of 15 into column t.c1, whether or not there was already any such value in the column, because the gaps between all existing values in the range are locked.
GAP锁,即锁定索引之前和之后的记录。
A gap might span a single index value, multiple index values, or even be empty.
如下测试,我们首先设置隔离级别为RR(因为间隙锁,只会再RR级别生效),同时我们清空整表数据,以下事务针对一个空表。
Session A | Session B |
---|---|
start transaction; | start transaction; |
SELECT * FROM test WHERE id BETWEEN 4 and 10 FOR UPDATE; | |
insert into test values(5, 2, 2); | |
commit; | |
commit; |
如上,当Session B 执行insert语句时,会产生锁等待,而整表都是空行。同样的语句,在RC隔离级别下不会产生锁等待(因为RC没有GAP锁)
show engine innodb status;
查看对应的锁信息
---TRANSACTION 4116897, ACTIVE 723 sec
1 lock struct(s), heap size 1184, 0 row lock(s)
MySQL thread id 66, OS thread handle 0x70000b16a000, query id 1712 localhost root cleaning up
Trx read view will not see trx with id >= 4116898, sees < 4116896
---TRANSACTION 4116896, ACTIVE 729 sec
2 lock struct(s), heap size 360, 1 row lock(s)
MySQL thread id 65, OS thread handle 0x70000b126000, query id 1625 localhost root cleaning up
If id is not indexed or has a nonunique index, the statement does lock the preceding gap.
此处也需要注意一个前提,事务执行前,改行锁定,而不是事务执行时insert 然后对insert行加锁,此时因为其他事务不可见数据,仍然会产生间隙所。
如下RR级别,插入行id为4的记录
insert into test values(4, 2, 2);
Session A | Session B |
---|---|
start transaction; | start transaction; |
SELECT * FROM test WHERE id = 4 FOR UPDATE; | |
insert into test values(5, 2, 2); | |
commit; | |
commit; |
如上事务则会执行成功,不会产生间隙锁。
Gap locks in InnoDB are “purely inhibitive”, which means that their only purpose is to prevent other transactions from inserting to the gap. Gap locks can co-exist. A gap lock taken by one transaction does not prevent another transaction from taking a gap lock on the same gap. There is no difference between shared and exclusive gap locks. They do not conflict with each other, and they perform the same function
GAP锁的目的主要是为了解决插入问题,不同的事务可以同时持有GAP锁。
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). Under these circumstances, gap locking is disabled for searches and index scans and is used only for foreign-key constraint checking and duplicate-key checking.
A next-key lock is a combination of a record lock on the index record and a gap lock on the gap before the index record.
By default, InnoDB operates in REPEATABLE READ transaction isolation level. In this case, InnoDB uses next-key locks for searches and index scans, which prevents phantom rows
参考: https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html