InnoDB存储引擎有3种行锁的算法,其分别是:
- Record Lock:单个行记录上的锁
- Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
- Next-Key Lock∶Gap Lock+Record 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 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,+∞)。可以看到其区别就是左侧值可以取到(Previous-Key Locking)还是右侧值可以取到(Next-Key Lock)。
假设当前有一张数据表内容t如下
| a (Primary Key) | b(Key) |
|---|---|
| 1 | 1 |
| 3 | 1 |
| 5 | 3 |
| 7 | 6 |
| 10 | 8 |
当会话A正在执行以下语句时
SELECT * FROM t WHERE b=3 FOR UPDATE;
这时SQL语句通过索引列b进行查询,其使用传统的Next-Key Locking技术加锁,并且由于有两个索引,其需要分别进行锁定。对于聚集索引,其仅对列a等于5的索引加上Record Lock。而对于辅助索引,其加上的是Next-Key Lock,锁定的范围是(1,3),特别需要注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上gap lock,即还有一个辅助索引范围为(3,6)的锁。因此,若在新会话B中运行下面的SQL语句,都会被阻塞:
SELECT*FROM t WHERE a=5 LOCK IN SHARE MODE;
INSERT INTO t SELECT 4,2;
INSERT INTO t 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 t SELECT 8,6;
INSERT INTO t SELECT 2,0;
INSERT INTO t SELECT 6,7;
Gap Lock的作用
从上面的例子中可以看到,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。
Next-Key Lock降级为Record Lock
当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围。
对于表t,当会话A正在执行以下语句时
SELECT * FROM T WHERE a=5 FOR UPDATE;
会话B执行以下语句不需要等待,因为此时索引时唯一索引,降级为Record Lock
INSERT INTO T SELECT 4, 1;
对于唯一键值的锁定,Next-Key Lock降级为Record Lock仅存在于查询所有的唯一索引列。若唯一索引由多个列组成,而查询仅是查找多个唯一索引列中的其中一个,那么查询其实是range类型查询,而不是point类型查询,故InnoDB存储引擎依然使用Next-Key Lock进行锁定。
解决Phantom Problem
InnoDB存储引擎采用Next-Key Locking机制来避免Phantom Problem(幻像问题)。这点可能不同于与其他的数据库,如Oracle数据库,因为其可能需要在SERIALIZABLE的事务隔离级别下才能解决Phantom Problem。Phantom Problem是指在同一事务下,连续执行两次同样的SQL语句可能导致不同的结果,第二次的SQL语句可能会返回之前不存在的行。
继续考虑上文的表t,若执行下列SQL
SELECT * FROM t WHERE a > 7 FOR UPDATE;
若事务没有进行提交操作,上述结果应该返回的值是10一条结果。此时,另一个事务插入了8这个值,那么该事务再次执行上述SQL会得到8、10两条记录。这与第一次执行得到的结果不同,违反了事务的隔离性([[Transaction#I(isolation)隔离性。]]),即当前事务能看到其他事务的结果。
InnoDB存储引擎采用Next-Key Locking的算法避免Phantom Problem。对于上述的SQL语句SELECT * FROM t WHERE a > 7 FOR UPDATE,其锁住的不是10这单个值,而是对(7,+∞)这个范围加了X锁。因此任何对于这个范围的插入都是不被允许的,从而避免Phantom Problem。
此外,用户可以通过InnoDB存储引擎的Next-Key Locking机制在应用层面实现唯一性的检查。例如:
SELECT*FROM table WHERE col=xxx LOCK IN SHARE MODE;
# If not found any row:
# unique for insert value
INSERT INTO table VALUES(...);
如果用户通过索引查询一个值,并对该行加上一个SLock,那么即使查询的值不在,其锁定的也是一个范围,因此若没有返回任何行,那么新插入的值一定是唯一的。如果在进行第一步SELECT…LOCK IN SHARE MODE操作时,有多个事务并发操作,这时会导致死锁,因为几个事务都需要等待其他事务的完成,所以只有一个事务的插入操作会成功,而其余的事务会抛出死锁的错误。
References
[1] 姜承尧 《MySQL技术内幕 : InnoDB存储引擎(第2版) 》