在MySQL锁机制漫谈(一)一文中,我们主要是探究了以下MySQL(主要是InnoDB)的锁的机制,但是我们平常经常使用的SQL语句一般都会加上什么锁,我自己也并不太熟悉,因此本文就罗列一些SQL语句在在执行过程中添加的锁情况。注意,本文主要是基于MySQL的官方文档而来,有不同意见也可以继续讨论。
一个UPDATE或者DELETE语句一般会在执行SQL操作的过程中对扫描到的每一个索引记录添加记录锁(record lock).InnoDB不会记录具体的where条件,而仅仅知道那些索引范围被扫描过。这些锁通常来说都是会阻塞在记录前面的间隙中插入数据的next-key lock。但是由于间隙锁可以被显式禁用,这同时会导致next-key锁定无法使用。
如果通过一个二级索引进行查询,同时该索引记录是独占的,InnoDB会找到对应的聚集索引并加锁。
如果你的表中没有适合SQL语句的索引,此时MySQL就会扫描整个表来执行语句,因此表中的每一行就被锁住了,这回导致任何其他事物的insert操作都会被阻塞。所以,好的索引能够极大的减少不必要的行扫描。
对于SELECT … FOR UPDATE或者 SELECT … LOCK IN SHARE MODE来说,锁被你获取是进行行扫描,而且会在对比发现呢不符合结果集(也即不符合WHERE后的条件)而被释放。但是在某些情况下,由于结果集和数据源之间的关系在查询过程丢失,行锁可能不会立即被释放(如在一个UNION操作中,在没有评估这些行是否符合结果集前,被扫描的行可能会插入一个临时表中。在这种情况下,临时表和原始表中的行的关系就丢失了,后续的行锁不会立即释放知道整个查询的结束)。
InnoDB在执行SQL语句时会添加如下类型的锁:
SELECT … FROM 这是一个一致性读,会读取数据库的一个快照版本同时不会加锁(即所谓的一致性非锁定读),但这不会发生在数据库的隔离级别设置为SERIALIZABLE。在SERIALIZABLE隔离级别下,每个查询会在每一个扫描到的索引上添加共享的next-key锁。但是,对于使用唯一索引来查询唯一行的语句来说,此时只会加上record lock。
SELECT … FROM … LOCK IN SHARE MODE 该语句会在查询时每一个扫描到的索引上添加共享的next-key锁。但是,对于使用唯一索引来查询唯一行的语句来说,此时只会加上record lock。
SELECT … FROM … FOR UPDATE 该语句会在查询时每一个扫描到的索引上添加排他的next-key锁。但是,对于使用唯一索引来查询唯一行的语句来说,此时只会加上record lock。对于碰到的索引记录,该语句会通过执行SELECT … FROM … LOCK IN SHARE MODE 或者在特定的隔离级别上读来阻塞其他事务。一致性读会忽略读视图中的任何加在记录上的锁。
UPDATE … WHERE 该语句会在查询时每一个扫描到的索引上添加排他的next-key锁。但是,对于使用唯一索引来查询唯一行的语句来说,此时只会加上record lock。当UPDATE操作修改一个聚集索引记录时,隐式的锁会影响二级索引。UPDATE操作会在一个二级索引上加上共享锁当在插入一个二级索引记录之前执行重复检查扫描或者当进行插入一个二级索引记录时。
DELETE … FROM … WHERE 该语句会在查询时每一个扫描到的索引上添加排他的next-key锁。但是,对于使用唯一索引来查询唯一行的语句来说,此时只会加上record lock。
INSERT 操作在插入行上加上了排他锁,这是一个基于索引的锁,而非next-key lock (也即不是间隙锁,因此也就不会阻止其他事务在要插入的行前面执行插入语句)。
在插入该行记录前,会加一种叫做插入意向间隙锁的gap lock。该锁是这样告知一个事务有插入的意向的-即如果多个事务在同一个索引间隙上执行插入操作,如果各事务不是在同一个插入点上进行掺入操作就不需要互相等待。假设现有值为4和7的索引记录,分别有两个事务尝试插入值5和6,这两个事务都会先去获取插入意向锁而非在被插入的行上获取排他锁去锁住4和7之间的间隙,由于这两行的插入也不存在冲突,故也不会去互相阻塞。
如果出现了key重复的错误,就会在该重复key索引记录上加上共享锁。如果多个事务尝试在同一个行记录上进行插入同时某个事务已经在该行上拥有一个排他锁,那么这种共享锁就会导致死锁的出现。
假设一个场景,一个InnoDB表t1有如下结构:
CREATE TABLE t1 (i INT, PRIMARY KEY (i)) ENGINE = InnoDB;
现在假设有三个事务按顺序在执行以下操作,
事务1:
START TRANSACTION;
INSERT INTO t1 VALUES(1);
事务2:
START TRANSACTION;
INSERT INTO t1 VALUES(1);
事务3:
START TRANSACTION;
INSERT INTO t1 VALUES(1);
事务1:
ROLLBACK;
事务1的第一个操作会获取一个排他锁;事务2和事务3的操作会导致主键重复错误,同时两者都会请求获取一个共享锁在该行。当事务1进行回滚,会释放在该行上的排他锁,同时事务2和事务3的排队的共享锁请求会成功。在这个点,事务2和事务3会形成死锁。没有一方会获取该行的排他锁由于各自都持有共享锁。
INSERT…ON DUPLICATE KEY UPDATE 该操作与简单的INSERT操作不同。当发生key重复错误,该操作会在要更新的行上加上排他锁而非共享锁。而对重复主键值这是一个排他索引记录锁;对重复的唯一键值,这回事一个排他next-key锁。
INSERT INTO T SELECT…FROM S WHERE … 该操作会在要插入的表T的每一行加上排他索引记录锁。如果表的事务隔离模型是READ_COMMITTED或innodb_locks_unsafe_for_binlog始能同时手误隔离级别不是SERIALIZZABLE,InnoDB会在表S上进行一致性读(无锁)。否则,InnoDB会在表S上的行加上共享next-key锁。