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.
A gap might span a single index value, multiple index values, or even be empty.
Gap locks are part of the tradeoff between performance and concurrency, and are used in some transaction isolation levels and not others.
Gap locking is not needed for statements that lock rows using a unique index to search for a unique row. (This does not include the case that the search condition includes only some columns of a multiple-column unique index; in that case, gap locking does occur.) For example, if the id column has a unique index, the following statement uses only an index-record lock for the row having id value 100 and it does not matter whether other sessions insert rows in the preceding gap:
SELECT * FROM child WHERE id = 100;
If id is not indexed or has a nonunique index, the statement does lock the preceding gap.
简单来说间隙锁就是索引记录之间的一个锁,例如 SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE 会锁住c1字段10 到 20之间的值的行,如果是不存在的行,这个“空隙”也会被锁住。间隙锁是性能和并发之间权衡出来的一种设计,在部分的隔离级别才生效。间隙锁在使用唯一索引的时候是可以避免的。(但是如果是复合唯一索引,查询条件只使用了部分索引字段,间隙锁依然会产生)
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(40) DEFAULT '',
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=latin1;
T1 | T2 |
---|---|
start transaction; | - |
SELECT * FROM user WHERE id BETWEEN 1 AND 3 FOR UPDATE; | - |
UPDATE user SET name = “test” WHERE id = 2; | |
被阻塞 | |
commit; | |
执行成功 |
这里就是T1锁住了id为1至3的区间。T2无法更改这个区间的数据。
注意这句话:A gap might span a single index value, multiple index values, or even be empty.
意思可以覆盖empty,就是所谓的不存在的值。
例如:
T1 | T2 |
---|---|
start transaction; | - |
SELECT * FROM user WHERE id BETWEEN 1 AND 5 FOR UPDATE; | - |
INSERT INTO user(id, name) VALUES(4, “ceshi”); | |
被阻塞 | |
commit; | |
执行成功 |
id = 4是不存在的值,但是一样是阻塞了T2,锁住了这个间隙,这就是Gap的由来。
A gap might span a single index value, multiple index values, or even be empty.
Gap locking is not needed for statements that lock rows using a unique index to search for a unique row. (This does not include the case that the search condition includes only some columns of a multiple-column unique index; in that case, gap locking does occur.)
间隙锁在通过唯一键去查询的时候是不会加的。(但是这不包括通过组合唯一索引的部分字段去搜索的时候的情况)
接下来MySql文档提供了一个官方的例子,说是
if the id column has a unique index, the following statement uses only an index-record lock for the row having id value 100 and it does not matter whether other sessions insert rows in the preceding gap:
SELECT * FROM child WHERE id = 100;
If id is not indexed or has a nonunique index, the statement does lock the preceding gap.
如果id字段有唯一索引,这个语句只锁行。如果没有索引,这个语句会锁前面的间隙。
这里就是我比较疑惑的地方!示范的这个SQL的这个语句其实没加任何锁,就算是加锁SELECT * FROM child WHERE id = 100 FOR UPDATE,就会锁前面的间隙是id < 100的行么?
于是我开始了尝试。
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(40) DEFAULT '',
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=latin1;
只有一行数据。
T1 | T2 |
---|---|
start transaction; | - |
SELECT * FROM user WHERE age = 10 FOR UPDATE; | - |
INSERT INTO user(name,age) VALUES(“test”,5); | |
被阻塞 | |
commit; | |
执行成功 |
所以得到的结论是,不加索引会给前面的前面的行加锁吗。这里说的前面的行,是id还是age呢?
不管是哪种,lock the preceding gap. 字面意思是锁前面的间隙,我把id和age都调大就不会产生间隙锁了,我把INSERT语句改一下:
T1 | T2 |
---|---|
start transaction; | - |
SELECT * FROM user WHERE age = 10 FOR UPDATE; | - |
INSERT INTO user(id, name,age) VALUES(5, “test”,20); | |
被阻塞 | |
commit; | |
执行成功 |
诶?! 怎么跟描述的不太一致,为什么还是产生了锁。我不走这个字段呢。
T1 | T2 |
---|---|
start transaction; | - |
SELECT * FROM user WHERE age = 10 FOR UPDATE; | - |
INSERT INTO user(name) VALUES( “test”); | |
被阻塞 | |
commit; | |
执行成功 |
来看锁的状态:
其实当执行SELECT * FROM user WHERE age = 10 FOR UPDATE。查询SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;是没任何显示的,只有阻塞的情况,才会把锁的信息记录下来。这也是我觉得MySQL比较奇怪的地方。
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(40) DEFAULT '',
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_age` (`age`)
) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=latin1;
T1 | T2 |
---|---|
start transaction; | - |
SELECT * FROM user WHERE age = 10 FOR UPDATE; | - |
INSERT INTO user(id, name,age) VALUES(5, “test”,20); | |
执行成功 |
SELECT * FROM child WHERE id = 100;
文档链接: https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html