Innodb对于行级锁算法的选用

很多人知道相对于Mysql的其他存储引擎,Innodb有一个明显的特点,那就是支持行级锁,下面就让我们了解一下Innodb的行级锁吧。

行级锁主要有三种算法:

  • Record Lock:单个行记录上的锁。

  • Gap Lock:间隙锁。锁定一个范围,但不包含记录本身

  • Next-Key Lock:锁定一个范围,并且包含记录本身。

在InooDB中对于行的查询都是采用Next-Key Lock这种锁定算法,该锁定算法的使用存在三种情况:

  1. 当进行锁定的列是主键且唯一时,Next-Key Lock降级为Record Lock。
  2. 当进行锁定的列是辅助索引时,NextKey-Lock索引并且对辅助索引下一个键值使用gap Lock。
  3. 当进行锁定的列并不是索引时,锁定全表。

下面我们就该三种情况进行解析:

  • 当进行锁定的列是主键且唯一时,Next-Key Lock降级为Record Lock

首先构建数据:

DROP TABLE IF EXIST t;
CREATE TABLE t(id INT PRIMARY KEY);

INSERT INTO t SELECT 1;
INSERT INTO t SELECT 2;
INSERT INTO t SELECT 5;

进行实验,建议在开始前将等待锁的超时时间设置到最小SET GLOBAL innodb_lock_wait_timeout=1;

线程A 线程B
BEGIN;
SELECT * FROM t WHERE id = 5 FOR UPDATE; // 对目标语句加排外X锁。
BEGIN;
INSERT INTO t SELECT 4; //成功
INSERT INTO t SELECT 6; // 成功
INSERT INTO t SELECT 5; // 抛出异常Lock wait timeout exceeded; try restarting transaction
COMMIT;
COMMIT;

如果线程A查询的行不存在,则不会进行加锁操作。

需要注意的一点是,锁的超时并不会导致当前事务的回滚,提交之后操作成功的插入命令仍然会生效。若希望超时时回滚事务,可以在mysql的配置文件中设置innodb_rollback_on_timeout=on;。另外,由于在Innodb中,对数据加锁实际上是在索引上加锁,因此不要试图在存在锁时修改表索引。

  • 当进行锁定的列是辅助索引时,NextKey-Lock索引并且对辅助索引下一个键值使用gap Lock

首先构建数据:

DROP TABLE IF EXIST t;
CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  KEY `id` (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO t SELECT 1;
INSERT INTO t SELECT 3;
INSERT INTO t SELECT 6;

进行实验:

线程A 线程B
BEGIN;
SELECT * FROM t WHERE id = 3 FOR UPDATE; BEGIN;
INSERT INTO t SELECT 2; // 抛出异常
INSERT INTO t SELECT 4; // 抛出异常
INSERT INTO t SELECT 1; // 抛出异常
INSERT INTO t SELECT 6; // 成功
INSERT INTO t SELECT 0; // 成功
COMMIT;
COMMIT;

Next-Key Lock锁定算法是为了解决Phantom Problem(幻像问题),采用该算法的锁定技术称为Next-Key Locking,该锁定技术锁定的不是单个值,而是一个范围。文末解释Next-Key Lock锁定算法为什么能够解决幻像问题。

在上述例子中,针对上锁的值3而言,Next-Key Lock需要锁定的是区间(1, 3],同样是为了解决Phantom Problem,需要对辅助索引下一个键值使用gap Lock,锁定区间(4, 6)。而上述实验中插入值1也抛出异常的原因可以视为将值1插入到了(1, 3]区间中:

插入1的位置
0
1
2
3
4

用户可以通过一下两种方式显式关闭锁:

  1. 将事务的隔离级别设置为READ COMMITTED。set session transaction isolation level read committed

  2. 将参数innodb_locks_unsafe_for_binlog设置为1。sel session innodb_locks_unsafe_for_binlog = 1

  • 当进行锁定的列并不是索引时,锁定全表:

首先构建数据:

DROP TABLE IF EXIST t;
CREATE TABLE `t` (
  `id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO t SELECT 1;
INSERT INTO t SELECT 3;
INSERT INTO t SELECT 6;

进行实验:

线程A 线程B
BEGIN;
SELECT * FROM t WHERE id = 3 FOR UPDATE; BEGIN;
INSERT INTO t SELECT 2; // 抛出异常
INSERT INTO t SELECT 4; // 抛出异常
INSERT INTO t SELECT 1; // 抛出异常
INSERT INTO t SELECT 6; // 抛出异常
INSERT INTO t SELECT 0; // 抛出异常
COMMIT;
COMMIT;

以上就是对于行级锁而言的三种情况。由此可知InnoDB实现行级锁的前提是使用索引,因此在存在事务且并发量较大的系统中,对SQL语句与索引的设计应该将行级锁考虑进去。

上文有提到InnoDB利用Next-Key Lock来解决Phantom Problem,而幻像问题其实就是幻读,是指某个事务两次读取之间,其他事务添加了一行数据,而导致两次查询数据不一致的问题。为了解决幻读,一般都需要进行全表锁定,采用最高的隔离级别——序列化。

但Mysql通过Next-key locking解决了该问题, Next-key locking其实是 gap lock 和 record lock 的统称,解决幻读问题是用 gap lock 来解决的。

下面举个例子:

构造数据:

DROP TABLE IF EXIST t;
CREATE TABLE `t` (
  `id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO t SELECT 1;
INSERT INTO t SELECT 3;
INSERT INTO t SELECT 6;

实验步骤:

线程A 线程B
SET SESSION transaction isolation level read committed;
BEGIN;
SELECT * FROM t WHERE id >3 FOR UPDATE; // 输出为5 BEGIN;
INSERT INTO t SELECT 6; // 成功
COMMIT;
SELECT * FROM t WHERE id >3 FOR UPDATE; // 输出为5 6

在RC的隔离级别中会出现两次查询数据不一致的问题,而Mysql的Next-key locking通过将[3, +∞]加上X排他锁解决了幻读问题。

不过到这里大家也会发现,该例子举的是范围查找,那么上面的精准查找为什么又要采用Next-key locking呢?因为Next-key locking是通过直接在底层 B+ 树上锁住区间的方式来实现的,并且排他锁只能锁住指定的行,防止其被更新,对非唯一键的插入就没办法了,因此对于上述例子如果此时再插入一条 id=3 的记录,那就出现幻读了。通过将加锁点最近两端的值进行锁定(即(1, 6]),以此来避免幻读。

最后留下一个问题,为什么对主键进行锁定能够降级成为Record Lock,而唯一索引却不能?

你可能感兴趣的:(mysql)