Contents
  1. 1. 唯一索引的锁定示例

InnoDB存储引擎有3种行锁的算法,其分别是:

  • Record Lock:单个行记录上的锁
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
  • Next-Key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身

Record Lock总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐匿的主键来进行行锁定。
Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是彩这种锁定算法,例如一个索引有10,11,13和20这四个值,那么该索引可能被Next-Key Locking的区间为:

[-∞,10]
(10,11]
(11,13]
(13,20]
(20,+∞)
采用Next-Key Lock的锁定技术称为Next-Key Locking。其设计的目的是为了解决Phantom Problem,这将在下一小节中介绍。而利用这种锁定技术,锁定的不是单个值,而是一个范围,是谓词锁(predict lock)的一种改进。除了next-key locking,还有previous-key locking技术。同样上述的索引10、11、13和20,若采用previous-key locking技术,那么可锁定的区间为:

[-∞,10]
(10,11]
(11,13]
(13,20]
(20,+∞)

若事务T1已经通过next-key locking锁定了如下范围:
(10,11]、(11,13]
当插入新的记录12时,则锁定的范围会变成:
(10,11]、(11,12]、(12,13]
然而,当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围。看下面的例子,首先根据如下代码创建测试表t:

drop table if exists t;
create table t(a int primary key);
insert into t values(1),(2),(5);
唯一索引的锁定示例
时间 会话A 会话B
1 BEGIN;
2 SELECT * FROM t WHERE a=5 FOR UPDATE;
3 BEGIN;
4 INSERT INTO t SELECT 4;
5 COMMIT; #成功,不需要等待
6 COMMIT;

表t中共有1、2、5三个值。在上面的例子中,在会话A中首先对a=5进行X锁定。而由于a是主键且唯一,因此锁定的仅是5这个值,而不是(2,5)这个范围,这样在会话B中插入值4而不会阻塞,可以立即插入并返回。即锁定由Next-Key Lock算法降级为了Record Lock,从而提高应用的并发性。正如前面所介绍的,Next-Key降级为Record Lock仅在查询的列是唯一索引的情况下。若是辅助索引,则情况会完全不同。同样,首先根据如下代码创建测试表z:

CREATE TABLE Z(a int,b int,primary key (a),key(b));
insert into z values(1,1),(3,1),(5,3),(7,6),(10,8);

表z的列b是辅助索引,若在会话A中执行下面的SQL语句:

SELECT * FROM Z WHERE b=3 FOR UPDATE;

很明显,这时SQL语句通过索引列b进行查询,因此其使用传统的Next-Key Locking技术加锁,并且由于有两个索引,其需要分别进行锁定。对于聚集索引,其仅对列a等于5的索引加上Record Lock。而对于辅助索引,其加上的是Next-Key Locking,锁定的范围是(1,3),特别需要注意的是,InnoDB存储引擎会对辅助索引下一个键值加上gap lock,即还有一个辅助索引范围为(3,6)的锁。因此,若在新会话B中运行下面的构架语句,都会被阻塞:

SELECT * FROM Z WHERE a=5 LOCK IN SHARE MODE;
INSERT INTO Z SELECT 4,2;
INSERT INTO Z SELECT 6,5;

第一个SQL语句不能执行,因为在会话A中执行的SQL语句已经聚集索引中列a=5的值加上X锁,因此执行会被阻塞。第二个SQL语句,主键插入4,没有问题,但是插入的辅助索引值2在锁定的范围(1,3)中因此执行同样会被阻塞。第三个SQL语句,插入的主键6没有被锁定,5也不在范围(1,3)之间。但插入的值5在另一个锁定范围(3,6)中,故同样需要等待。而下面的SQL语句,不会被阻塞,可以立即执行:

INSERT INTO Z SELECT 8,6;
INSERT INTO Z SEELCT 2,0;
INSERT INTO Z SELECT 6,7;

从上面的例子中可以看到,Gap Lock的作用是为了阻止多个事务将记录插入到同一个范围内,而这会导致Phantom Problem问题的产生。例如在上面的例子中,会话A中用户已经锁定了b=3的记录。若此时没有Gap Lock锁定(3,6),那么用户可以插入索引b列为3的记录,这会导致会话A中的用户再次执行同样查询时会返回不同的记录,导致Phantom Problem问题的产生。
用户可以通过以下两种方式来显式地关闭Gap Lock:

  • 将事务的隔离级别设置为READ COMMITTED
  • 将参数innodb_locks_unsafe_for_binlog设置为1

在上述的配置下,除了外键约束和唯一性检查依然需要的Gap Lock,其余情况仅使用Record Lock进行锁定。但需要牢记的是,上述设置破坏了事务的隔离性,并且对于replication,可能会导致主从数据的不一致。此外,从性能上来看,READ COMMITTED也不会优于默认的事务隔离级别READ REPEATABLE。
在InnoDB存储引擎中,对于INSERT的操作,其会检查插入记录的下一条记录是否被锁定,若已被锁定,则不允许查询。对于上面的例子,会话A已经锁定了表z中b=3的记录,即已经锁定了(1,3)的范围,这时若在其他会话中进行如下的插入同样会导致阻塞:

INSERT INTO Z SELECT 2,2;

因为在辅助索引列b上插入值为2的记录时,会监测到下一个记录3已经被索引。而将插入修改为如下的值,可以立即执行:

INSERT INTO Z SELECT 2,0;

最后再次提醒的是,对于唯一键值的锁定,Next-Key Lock降级为Record Lock仅存在于查询所有的唯一索引一列。若唯一索引由多个列组成,而查询是查找多个唯一索引列中的其中一个,那么查询其实是range类型查询,而不是point类型查询故InnoDB存储引擎依然使用Next-Key Lock进行锁定。

Contents
  1. 1. 唯一索引的锁定示例