InnoDB存储引擎中的锁

锁是数据库系统区别与文件系统的一个关键特性。数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。但不同数据库之间或不同存储引擎之间,对于锁的实现方法各不相同,本文主要介绍MySQL InnoDB存储引擎中的锁。

锁种类

根据对数据库的操作,可分为以下两种

  • 排他锁(又称写锁,X锁)
  • 共享锁(又称读锁,S锁)

顾名思义,读操作时,获取共享锁,共享锁之间是兼容的;而写操作时,获取排他锁,并与其他任何锁都不兼容。

举个例子 ,当事务A中对数据对象a加上排他锁,则只允许事务A对对象a进行读写操作,其他事务想要对a操作只能等待事务A释放锁;而当对a加上共享锁时,则多个事务都可以同时读,但如果此时想对数据对象a进行写操作,只能等待其他共享锁释放。

锁粒度

InnoDB中根据锁粒度,主要分为以下两种

  • 行级锁
  • 表级锁

此外,InnoDB存储引擎支持多粒度锁定,允许事务中存在行级锁和表级锁。不过,当事务A对表中记录上写锁,而事务B想对表上表级锁时,事务B需要要遍历表中的所有行,看看是否有被锁定,这样效率很低。所以,为了更有效的支持多粒度锁定,InnoDB存储引擎引入了意向锁,在事务想要给表中的某几行加锁时,需要先给表上加上对应类型的锁。根据操作类型,也分为两种:

  • 意向排他锁(又称IX锁)
  • 意向共享锁(又称IS锁)

不过由于InnoDB存储引擎支持行级锁,因此意向锁并不会阻塞除全表扫以外的任何请求。主要是为了表示是否有请求锁定表中的某几行

一致性读

InnoDB中,通过多版本控制的方式来读取当前时间,数据库中的数据,称之为一致性非锁定读。在读取的行正在执行DELETE或UPDATE操作,这时执行select ... from table操作并不会去等待行上的X锁释放,相反地,InnoDB会去读取行的一个快照。

在不同的隔离级别下,读取的方式不同,并不是每个隔离级别下都采用非锁定的一致性读,就算都是一致性非锁定读也有不同的表现。

而显式的对读取操作进行加锁以保证数据逻辑的一致性,则为一致性锁定读,其中根据不同操作,加不同类型的锁。

  • select ... for update:对读取的行记录加排他锁
  • select ... lock in share mode:对读取的行记录加共享锁

需要注意的是,使用一致性锁定读,前提是在一个事务中,务必加上beginstart transactionset autocommit=1

锁算法

InnoDB存储引擎中有3种行级锁算法,分别是

  • Record Lock: 记录锁,锁定一条记录
  • Gap Lock:间隙锁,锁定一个范围,但不包括记录本身
  • Next-Key Lock:Gap Lock + Record Lock,锁定一个范围并锁定记录本身

在默认隔离级别可重复读下,采用了Next-Key Locking的机制来避免幻读。

假设有这么一张users表:

1
2
3
4
5
6
7
8
9
CREATE TABLE users(
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
age INT,
job VARCHAR(255),
PRIMARY KEY(id),
KEY(name),
KEY(age)
);

表中有这么几条记录

1
2
3
4
5
6
7
8
9
+------|-------------|--------------|-------+
| id | name | job | age |
|------|-------------|--------------|-------|
| 1 | pony | a | 10 |
| 2 | tony | b | 20 |
| 3 | tom | c | 30 |
| 4 | jeff | d | 40 |
| 5 | donald | e | 50 |
+------|-------------|--------------|-------+

当我们执行

1
select * from users where age = 20 for update;

InnoDB会为(10,20]的记录加上Next-Key Lock,而为(20,30)记录加上Gap Lock。

然而当我们执行

1
select * from users where id = 1 for update;

此时,InnoDB只会锁住id=1这条记录,这是因为在查询列是唯一索引的情况下,Next-Key Lock 会降级为Record Lock,从而提高并发。不过如果唯一索引是由多个列组成,而查询仅是多个列中的一个,那么查询会是range类型,最终还是会使用Next-Key Lock 进行锁定。

参考:

《高性能MySQL》

https://zhuanlan.zhihu.com/p/29150809

https://draveness.me/mysql-innodb.html