- 背景
项目发展很快最近接触了很多高并发下的代码规范,其中一个重要的点就是MySQL的锁竞争问题,其中一些问题非常的复杂,接下来抽时间系统的学习下MySQL锁的部分,虽然还不到源码的级别,但是希望深入的理解官方文档并给出对应的用例。这篇博客主要分析下间隙锁。
Gap 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.
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之间的值的行,如果是不存在的行,这个“空隙”也会被锁住。间隙锁是性能和并发之间权衡出来的一种设计,在部分的隔离级别才生效。间隙锁在使用唯一索引的时候是可以避免的。(但是如果是复合唯一索引,查询条件只使用了部分索引字段,间隙锁依然会产生)
- 建表DDL语句
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
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的行么?
于是我开始了尝试。
- 验证其说法
还是user表.
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); | |
执行成功 |
所以加索引可以避免锁表!依稀记得很早之前有看过文章说索引的确可以避免锁表,所以合理的添加索引能够提高单表的并发写入能力,但是非聚集索引的回表操作会增加IO开销,因此也可能在MySQL的执行引擎里放弃索引使用全表扫描,那样依然会扫全表,那数据量大的之后的非聚集索引查询,即大数据量的加锁操作非常危险基本不可控需要避免。话说回来,本文是我在看官方文档关于间隙锁的描述,关于官方文档的几个疑问我还没搞清楚
那现在再来看MySQL的官方文档就很奇怪的,If id is not indexed or has a nonunique index, the statement does lock the preceding gap. 如果没有索引,应该是锁整个表才对,为什么这里的强调的是 lock the preceding gap.锁前面的行呢?前面的行是按哪个顺序排序的前面的行?生序还是降序?而且文档的SQL :
SELECT * FROM child WHERE id = 100;
默认InnoDB是不加任何锁的,为什么说此时会加间隙锁?网上没查到好的解释,如果有谁知道,请评论区分享一下,我参考的官网链接如下:
文档链接: https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html