不同SQL语句在InnoDB中产生的锁
一般来说,加锁读,UPDATE,或者DELETE这些SQL语句,会在执行时扫描的每一条记录上加记录锁,无论语句中的WHERE条件是否将某一行排除在外。InnoDB不会记得WHERE条件是什么,只能知道扫描了哪些索引。 这里加的锁通常都是next-key锁,会阻塞向当前记录前面的一段"间隙"里面插入数据的操作。[间隙锁]可以显示的禁用,从而也就不会产生next-key锁。更多信息可以查看InnoDB中的锁。事务隔离级别也会影响加锁的机制,参考事务隔离级别。
如果在查询中使用了辅助索引,并且在辅助索引记录上加了排它锁,InnoDB同时也会在相应的聚集索引记录上加上锁。
如果在你的语句中没有合适的索引可以使用,MySQL必须要扫描整个表来处理,表中的每一行都会被锁住,从而阻塞了其他用户往表中插入数据。创建良好的索引非常重要,它可以让你的查询无需扫描很多行记录。
InnoDB设置各种锁的机制如下:
-
SELECT ... FROM是一致性读,它从数据库的一个快照中读取数据,不会加任何锁,除非事务隔离级别被设置为SERIALIZABLE,对于SERIALIZABLE级别,查询过程中会在它遇到的每一条记录加上共享next-key锁。然而,如果使用了唯一索引来查询唯一的记录,则只需要在一条记录上加锁。
-
对于加锁读(SELECT with FOR UPDATE or LOCK IN SHARD MODE),UPDATE和DELETE语句,如何设置锁取决于语句中使用的是唯一索引及唯一查询,还是一个范围查询。
-
对于查询中遇到的索引记录,SELECT ... FOR UPDATE会阻塞其他事务进行https://dev.mysql.com/doc/refman/5.6/en/select.html操作,同时也会阻塞某些隔离级别下的读操作。一致性读忽略当前读取视图中任何记录上的锁。
-
UPDATE ... WHERE ...给查询中遇到的每一条记录加上next-key锁。然而,如果使用了唯一索引来查询唯一的记录,则只需要在一条记录上加锁。
-
当UPDATE修改一条聚集索引时,会在相应的辅助索引上产生锁。UPDATE操作在插入新的辅助索引记录之前,会扫描进行重复检查,此时会在辅助索引上加上共享锁。
-
DELETE FROM ... WHERE ...在遇到的每条记录上都加上排它next-key锁。然而,如果使用了唯一索引来查询唯一的记录,则只需要在一条记录上加锁。
-
INSERT在要插入的记录上加排它锁。这是一个记录锁,不是next-key锁(也就是没有间隙锁),因此不会阻止其他事务向这条记录之前的间隙中插入数据。
在插入数据之前,会加上一种间隙锁,叫做插入意向锁。这个锁显示了一种意图,即当前事务准备向表中插入数据,多个事务可以向同一个间隙中同时插入数据,只要它们不向同一个位置插入数据。假设现在表中有索引记录4和7。不同的事务尝试插入值5和6,在获取插入行的排它锁之前,它们都在4到7之间的间隙上加上了插入意向锁,它们不会阻塞彼此,因为要插入的行是不冲突的。(译注:这段文档字面意思很容易懂,但是整体要表达的意思不好理解,它似乎在强调意向锁不会导致多个插入操作相互阻塞,但是确让人疑惑意向锁到底有什么用)在插入数据时,如果出现了重复key的错误,会在重复的记录上加共享锁。共享锁的这种使用方式可能会导致死锁:在多个事务尝试向同一个位置插入数据时,如果某个其他事务已经这条记录的排它锁,并且这个事务删除了这条记录。假设一个表有如下结构:
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的操作会得到一个重复key错误,然后它们都会请求在这条数据上加共享锁(译注:不懂为什么)。当事务1会回滚后,它会释放这条数据上的排它锁,然后事务2和事务3请求的共享锁就会成功。在此时,事务2和事务3就死锁了:它们都无法获取到这条数据的排它锁,因为他们都占有了共享锁。
一个类似的情况是,表中已经有了值为1的这条数据,然后三个事务按如下顺序操作:
事务1:START TRANSACTION; DELETE FROM t1 WHERE i = 1;
事务2:
START TRANSACTION; INSERT INTO t1 VALUES(1);
事务3:
START TRANSACTION; INSERT INTO t1 VALUES(1);
事务1:
COMMIT;
事务1首先会获取这条数据的排它锁。事务2和事务3的操作会得到一个重复key错误,然后它们都会请求在这条数据上加共享锁。当事务1提交后,它会释放这条数据上的排它锁,然后事务2和事务3请求的共享锁就会成功。在此时,事务2和事务3就死锁了:它们都无法获取到这条数据的排它锁,因为他们都占有了共享锁。
-
INSERT ... ON DUPLICATE KEY UPDATE和单纯的INSERT不同:当遇到重复key错误时,不会去请求共享锁,而是请求排它锁。对于重复的主键索引,会请求获取一个排它记录锁;对于重复的普通索引,会请求获取next-key锁。
-
在唯一key没有冲突的情况下,REPLACE的行为和INSERT类似。其他情况下,在准备替换的记录上加排它next-key锁。
-
INSERT INTO T SELECT ... FROM S WHERE ...
在插入到表T中的每一行上加排它锁(没有间隙锁)。如果事务隔离级别是READ COMMITTED或者开启了innodb_locks_unsafe_for_binlog并且隔离级别不是SERIALIZABLE,InnoDB使用类似一致性读的方式来查询。其他情况下,InnoDB在表S中的数据加上共享next-key锁。
CREATE TABLE ... SELECT ...和[INSERT ... SELECT]类似,使用一致性读或者使用共享next-key锁来SELECT。
当使用[SELECT]来构建REPLACE INTO t SELECT ... FROM s WHERE ... or UPDATE t ... WHERE col IN (SELECT ... FROM s ...)
,InnoDB在表s的数据上加共享next-key锁。 -
当初始化表中被指定为
AUTO_INCREMENT
的列时,InnoDB在这一列的索引尾部加上一个排它锁。在访问auto-increment计数器时,InnoDB使用一个专用的AUTO-INC
表锁模式,此模式下,锁只会持续到当前SQL语句结束,而不是持续到整个事务结束。当AUTO-INC
表锁被占用时,其他事务不能向表中插入数据。参考InnoDB事务模型
InnoDB在获取AUTO_INCREMENT
列的值时,不需要加任何锁。 -
如果表上有
FOREIGN KEY
约束,任何需要做约束检查的的插入、更新和删除操作都会在被检查的数据上加共享锁。 -
LOCK TABLES设置表锁,表锁是由InnoDB之上的MySQL层来设置的。如果
innodb_table_locks
为1(默认情况)并且autocommit
为0,并且MySQL层能够知道底层的锁,那么innodb能够察觉到表锁的存在。
此外,InnoDB的死锁检测机制不能够探测到和表锁关联的死锁。同时,由于高层的MySQL不知道底层的锁,可能出现一种情况: 当前事务加上了表锁,但是其他事务同时也加上了一些底层的锁。然而,这并不会影响事务的完整性,如死锁检测和回滚中所描述的。另外可以参考InnoDB表的限制。