面试官观点:innodb引擎只会使用行锁。
面试君观点:innodb引擎支持行锁,但在不使用索引或无索引的时候会使用表锁。
虽然当时未做过多争论,不过这个问题还是挺有意思,MySQL的innodb引擎到底会锁表么?
这个在网上搜索,大部分的回答是innodb是在索引上实现行锁的,如果没有索引或无法利用索引,那么可能会升级为锁表。
从《MySQL技术内幕 InnoDB存储引擎 第2版》第六章“锁”中的描述innodb引擎有两种锁:共享锁和排它锁。这两种所都是行锁。
共享锁:允许事物读取一行数据。
排它锁:允许事物删除或更新一行锁。
另外,其还有一种特殊的锁名为意向锁,意向锁也分为两种:意向共享锁和意向排它锁。意向锁为表锁。
意向共享锁:允许事物读取几行数据。
意向排它锁:允许事物删除或更新机行锁。
为了搞清楚这些概念,需要做一点实验,观察innodb是如何执行锁的。
创建库qw,创建2张表book和noindex。
表book内2个字段,其中id字段为自增长主键,num字段列值非唯一,name字段可为null。
表noindex没有创建主键和索引,其pwd2字段值只有65和31两种(即本列值非唯一)。
实验1:在表b上模拟2个排它锁,各锁定1行数据执行更新
打开两个控制台(控制台A,控制台B)分别执行如下:
USE qw;
BEGIN;
UPDATE book SET `name`='a567' WHERE id=15;
#COMMIT;
USE qw;
BEGIN;
UPDATE book SET `NAME`='b567' WHERE id=16;
#COMMIT;
注意:两个命令都没有提交,以模拟并发排它锁。
在其他控制台执行:
USE information_schema;
SELECT * FROM INNODB_TRX
观察事务执行状态:
这里显示两个事务都储运运行状态,也就是两者没有冲突。
执行:SELECT * FROM INNODB_LOCKS
此时没有记录。
提交或回滚数据,然后进行第二个实验。
实验2:修改实验1的sql的条件,改为大于和小于
USE qw;
BEGIN;
UPDATE book SET `name`='a567' WHERE id<15;
#COMMIT;
USE qw;
BEGIN;
UPDATE book SET `NAME`='b567' WHERE id>25;
#COMMIT;
注意:两个条件范围没有交集
这里显示两个事务都储运运行状态,也就是两者没有冲突。
执行:SELECT * FROM INNODB_LOCKS
此时没有记录。
实验3:修改实验1的sql的条件,改为大于和小于,范围有交集
USE qw;
BEGIN;
UPDATE book SET `name`='x67' WHERE id<15;
#COMMIT;
USE qw;
BEGIN;
UPDATE book SET `NAME`='y56' WHERE id<20 and id >13;
#COMMIT;
注意:两个条件范围有交集
这里发现其中一个事务处于锁等待状态。
容易理解,拍它锁不能同时用于同一行。
执行:SELECT * FROM INNODB_LOCKS
此时可以看到2个排它锁
实验4:使用无索引表noindex执行对1条数据更新操作
USE qw;
BEGIN;
UPDATE noindex SET pwd2='a567' WHERE name1='n2';
COMMIT;
USE qw;
BEGIN;
UPDATE noindex SET pwd2='a567' WHERE name1='n12';
COMMIT;
注意:表noindex没有索引,但是name1没有重复值(可以精品匹配)
如图,其中一个事务处于等待。
上面做了4个实验,总结如下:
锁行数 | 主键或索引 | 是否有交集 | 事务状态 | |
实验1 | 1 | 有,使用 | 无 | 两个事务同时运行 |
实验2 | 多行 | 有,使用 | 无 | 两个事务同时运行 |
实验3 | 多行 | 有,使用 | 有 | 一个事务运行,一个等待 |
实验4 | 1 | 无 | 无 | 一个事务运行,一个等待 |
分析:
实验1,有索引并使用作为条件,锁1行数据无交集,2个事务排它锁没有锁冲突,因此可以同时运行。
实验2,有索引并使用作为条件,但条件为一个范围(欲锁多条数据),事务A和事务B条件无交集,排它锁没有锁冲突,因此可以同时运行。
实验3,有索引并使用作为条件,但条件为一个范围(欲锁多条数据),事务A和事务B条件有交集,排它锁有锁冲突(后执行的事务发现欲加锁的行已经有排它锁),因此不可以同时运行。
实验4,没有索引,条件为精确匹配(数据唯一),事务A和事务B条件无交集,排它锁按道理不应该有冲突,事务可以同时运行,但是,实际情况是从innodb_trx看到两个事务有一个在等待。在innodb_lock内看到事务的锁类型是RECORD,是行锁。不能同时运行的原因只能是事务B(后运行的事务)要锁定的行,被事务A锁定了。然而根据条件事务A只需要锁1行,这是否就印证了innodb的锁是依靠索引来锁的?因为没有索引,所以就锁了全部的行。
是不是真的锁了全部记录?测试一下,对所有行尝试update,结果均出现阻塞。insert数据也是阻塞。
结论是确实锁了全部的行。虽然是行锁,但实际上是全表锁定。
为了进一步证实跟索引有关,我们在表nobook的name1字段添加索引,在进行测试。
两个事务已经可以同时运行。这印证了行锁需要索引支持。
面试官和面试君的分歧还无法给出明确的结论。从实验数据来看lock_type没有出现table,但是在无索引情况下虽然是行锁,但是锁了所有的行。最后逇实验也证实了行锁需要索引支持来实现。因此二人所说都有一定道理。
补充实验5:使用表book的索引字段进行更新操作。字段aid有创建索引但是不唯一。
BEGIN ;
UPDATE qw.book SET num=24 WHERE `name`='x567';
#commit;
USE qw;
BEGIN ;
UPDATE qw.book SET num=23 WHERE `name`='y567';
#commit;
其现象与实验4一致。
使用explain对更新语句分析
表book记录总数1930条,mysql优化器给出的预测是会影响1930条记录,也就是全表。表有主键但是没有可利用的索引。
如果条件可以上使用主键,那么影响的数据条数会怎样?
这个语句虽然是一个范围条件,但是优化器认为可以使用主键,并且影响的数据为20条。
补充实验6:这次数据条件有变化,看下图
字段aid有重复值,且不连续。
分别在两个控制台执行如下:
BEGIN ;
SELECT * FROM noindex WHERE aid=15 FOR UPDATE;
#commit ;
BEGIN;
INSERT INTO noindex(name1,pwd2,aid) VALUES('n18','22',14);
#commit;
此时观察innodb_locks表如下:
后执行的sql除了X之外,多了一个GAP(间隙锁)。这个是锁定一个范围的算法,不包括记录本身。
如果将第二句aid值改为16:
BEGIN;
INSERT INTO noindex(name1,pwd2,aid) VALUES('n18','22',16);
#commit;
先执行的sql也出现了GAP。
根据资料介绍,innodb引擎使用在不唯一索引条件是,实际会自动添加GAP锁。本例中条件a=15 for update,引擎添加了GAP锁,锁定了(6,20)范围,15前一个值是6,后一个值是20.在这一范围内插入数据时,因为GAP的存在而会处于等待。
而之外的值不会被阻塞。比如改为aid值22
INSERT INTO noindex(name1,pwd2,aid) VALUES('n18','22',22);
这句执行则不会受第一个sql锁定影响。
第二条要插入aid为14的记录,因为第一条锁定aid=15之外还想前锁定最近一条记录形成一个范围(最近的是6),14刚好在范围内,所以必须等待第一个操作释放锁后才能执行。
小结:经过学习资料和实验,基本可以得出以下结论
1.innodb支持行锁,并且不升级表锁
主要原因是行锁开销比较小,3千万条记录的表,锁全部行大约使用90M内存。
2.innodb行锁在非唯一辅助索引上,锁定不是一行记录,是一个范围
相反,如果是唯一的辅助索引,是锁一行记录。
3.锁冲突时,后发生的锁请求会等待(阻塞)。 如果事物之间形成锁请求等待依赖闭环,就会出现死锁。
wait-for-graph是一种死锁预测方式,有助于预防死锁。