跳过正文
  1. 面试题库/

06|锁

·7570 字·16 分钟
目录
MySQL面试题库 - 这篇文章属于一个选集。
§ 6: 本文

1. 详细说一下 MySQL 数据库中锁的分类(重要)
#

分析

不一定要把每个锁都说出来,重点表达自己用过或者熟悉哪些锁,也要强调只有 InnoDB 存储引擎实现了行级锁,不过行级锁有哪些还是要说出来。

  • 全局锁:通过 flush tables with read lock 语句会将整个数据库就处于只读状态了,这时其他线程执行以下操作,增删改或者表结构修改都会阻塞。全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。
  • 表级锁:MySQL 里面表级别的锁有这几种:
    • 表锁:通过 lock tables 语句可以对表加表锁,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。
    • 元数据锁:当我们对数据库表进行操作时,会自动给这个表加上 MDL。对一张表进行 CRUD 操作时,加的是 MDL 读锁;对一张表做结构变更操作的时候,加的是 MDL 写锁;MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。
    • 意向锁:当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。意向锁的目的是为了快速判断表里是否有记录被加锁。
  • 行级锁:InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。
    • 记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的,满足读写互斥、写写互斥。
    • 间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
    • Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
    • 插入意向锁,当插入位置的下一条记录有间隙锁,那么就会生成插入意向锁,然后进入阻塞状态。

回答

根据锁粒度的不同,MySQL 的锁可以分为全局锁、表级锁、行级锁。

我比较熟悉的是表级锁和行级锁,比如我们对一张表结构进行修改的时候,MySQL 就会对这张表加一个元数据锁,元数据锁是属于表级锁的。

行级锁目前只有 InnoDB 存储引擎实现了,MyISAM 存储引擎是不支持行级锁的,只有表锁。InnoDB 存储引擎实现的行级锁主要有记录锁、间隙锁、临键锁、插入意向锁这些,当我们对表记录进行 select for update,或者增删改的时候,都会对记录加行级锁。

2. MySQL 怎么实现乐观锁?(重要)
#

分析

乐观锁假定在大多数情况下,数据在操作期间不会被其他事务修改,所以在操作数据时不会对数据加锁。只有在更新数据时,才会去检查数据在自己操作期间是否被其他事务修改过,如果没有被修改,就执行更新操作;如果被修改了,则根据不同的实现方式进行相应处理,比如重试或者抛出异常。

实现乐观锁的方式:

  • 版本号机制:在表中增加一个版本号字段,每次更新数据时,版本号加 1。在更新数据前,先读取数据的版本号,在更新时将版本号作为条件进行判断,如果版本号与之前读取的一致,就更新数据并将版本号加 1;如果不一致,说明数据已被其他事务修改,更新失败。
  • 时间戳机制:与版本号机制类似,只是使用时间戳来记录数据的修改时间。

乐观锁适合的场景:

  • 读多写少:在这种场景下,数据被其他事务修改的概率较低,使用乐观锁可以减少加锁和解锁的开销,提高系统的并发性能。例如,商品信息的展示页面,大部分操作是读取商品信息,只有很少的操作是修改商品信息,就可以使用乐观锁。
  • 冲突较少:当多个事务同时修改同一数据的情况很少发生时,乐观锁可以避免悲观锁带来的性能问题。比如,用户个人信息的修改,不同用户修改自己信息的冲突概率较低,适合使用乐观锁。

在数据库表中添加一个版本号字段(通常为整数类型),用于记录数据的版本信息。例如,有一个 products 表,存储商品信息,可添加一个 version 字段:

CREATE TABLE products (
  id INT PRIMARY KEY,
  name VARCHAR(255),
  price DECIMAL(10, 2),
  version INT DEFAULT 0
);

当要对数据进行更新操作时,首先需要从数据库中查询出要操作的数据及其当前版本号。例如,要更新 id 为 1 的商品信息:

SELECT id, name, price, version
FROM products
WHERE id = 1;

假设查询结果为:

idnamepriceversion
1手机50000

在应用程序中对查询到的数据进行业务处理。例如,将商品价格从 5000 元调整为 5500 元。

在更新数据时,将版本号作为条件进行判断。如果版本号与之前查询时的版本号一致,说明数据在操作期间没有被其他事务修改,更新数据并将版本号加 1;如果版本号不一致,说明数据已被其他事务修改,更新失败。更新语句如下:

UPDATE products
SET name = '手机', price = 5500, version = version + 1
WHERE id = 1 AND version = 0;

执行更新语句后,需要检查更新操作影响的行数。如果影响的行数为 1,说明更新成功;如果影响的行数为 0,说明版本号校验失败,数据在操作期间已被其他事务修改。

alt text

回答

可以在数据库表增加一个版本号字段,利用这个版本号字段在数据库中实现乐观锁。

具体的实现,每次更新数据的时候,都要带上版本号,同时将版本 +1。比如现在要更新 id = 1,版本号为 2 的记录。这时候先要获取 id = 1 的版本号,然后更新语句写成 update table set name = "小明", version = version + 1 where id = 1 and version = 2

如果这个版本号与表记录中的版本号一致的话,就能更新成功,如果不相等则不进行更新,然后需要重新获取该记录的最新版本号,然后再尝试更新数据。

3. 在线上修改表结构,会发生什么?
#

分析

考察表级锁:元数据锁。

回答

线上环境可能存在很多事务都在读写这张表,如果对这张表进行了表结构修改,就会发生阻塞,原因是有事务对这张表进行读写操作的时候,会生成元数据读锁,而修改表结构的时候,会生成元数据写锁,这时候就产生了读写冲突,所以修改表结构的操作就会阻塞,并且后续事务的增删查改操作都会阻塞。

4. 创建索引的时候会锁表吗?
#

分析

对表结构修改,增加字段、删除字段、增加索引、删除索引、更改索引名字、更改字段名字等等,这些操作都会加 MDL 写锁(元数据写锁),属于表级锁,会和 MDL 读锁发生读写冲突。

回答

会的,创建索引的时候会加 MDL 写锁,如果这时候有其他事务对这张表进行增删查改的话,这些事务就都会被阻塞,原因是有事务对这张表进行读写操作的时候,会生成 MDL 读锁,这时候就产生了读写冲突。

5. InnoDB 存储引擎中的行级锁有哪些?(重要)
#

分析

主要是有记录锁、间隙锁、临键锁、插入意向锁。

记录锁

Record Lock 称为记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的:

  • 当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容)。
  • 当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)。

举个例子,当一个事务执行了下面这条语句:

mysql > begin;
mysql > select * from t_test where id = 1 for update;

就是对 t_test 表中主键 id 为 1 的这条记录加上 X 型的记录锁,这样其他事务就无法对这条记录进行修改了。

当事务执行 commit 后,事务过程中生成的锁都会被释放。

间隙锁

Gap Lock 称为间隙锁,只存在于可重复读隔离级别和串行化隔离级别(读已提交隔离级别不存在间隙锁),目的是为了解决可重复读隔离级别下幻读的现象。

假设表中有一个范围 id(3, 5) 的间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效地防止幻读现象的发生。

alt text

间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但是并没有什么区别,间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的。

临键锁

Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。

假设表中有一个范围 id(3, 5] 的 next-key lock,那么其他事务即不能插入 id = 4 记录,也不能修改 id = 5 这条记录。

所以,next-key lock 既能保护该记录,又能阻止其他事务将新记录插入到被保护记录前面的间隙中。

next-key lock 是包含间隙锁和记录锁的,如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型 next-key lock 时,是会被阻塞的。

比如,一个事务持有了范围为 (1, 10] 的 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,就会被阻塞。

虽然相同范围的间隙锁是多个事务相互兼容的,但对于记录锁,我们是要考虑 X 型与 S 型关系,X 型的记录锁与 X 型的记录锁是冲突的。

插入意向锁

一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。

如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。

举个例子,假设事务 A 已经对表加了一个范围 id(3, 5) 的间隙锁。

alt text

当事务 A 还没提交的时候,事务 B 向该表插入一条 id = 4 的新记录,这时会判断到插入的位置已经被事务 A 加了间隙锁,于是事务 B 会生成一个插入意向锁,然后将锁的状态设置为等待状态。等到事务 A 提交了,事务 B 的插入意向锁状态设置为正常状态,然后设置锁的状态,如果锁状态是等待状态,并不是意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁,此时事务 B 就会发生阻塞,直到事务 A 提交了事务。

插入意向锁名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁,属于行级别锁。

如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点。因而从这个角度来说,插入意向锁确实是一种特殊的间隙锁。

插入意向锁与间隙锁的另一个非常重要的差别是:尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。

回答

InnoDB 实现的行级锁有记录锁、间隙锁、临键锁、插入意向锁。我们在使用增删改或者锁定读语句的时候,都会对记录加行级锁。

  • 记录锁,可以避免其他事务对该记录进行删除和更新操作。
  • 间隙锁,可以避免其他事务往间隙里插入新记录。
  • 临键锁,是记录锁和间隙锁的组合,所以它既可以避免其他事务对该记录进行删除和更新操作,也可以避免其他事务往间隙里插入新记录。
  • 插入意向锁,插入意向锁和间隙锁是互斥的关系,其他事务插入的时候,发现插入位置的下一条记录有间隙锁的话,才会生成的插入意向锁,并且这时候锁的状态是阻塞状态,目的是告诉用户插入的位置存在间隙锁。

6. 间隙锁的工作原理是什么?
#

分析

间隙锁是可重复读隔离级别和串行化隔离级别下才有的锁(读已提交隔离级别只有记录锁,没有间隙锁),主要是为了防止幻读的问题,它可以阻止其他事务往间隙里插入新记录。

alt text

假设表中有一个范围 id(3, 5) 的间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效地防止幻读现象的发生。

回答

间隙锁防止其他事务往间隙插入新记录,从而可以避免幻读的问题。具体的原理是当其他事务插入记录的时候,当发现插入位置的下一条记录有间隙锁,就会生成插入意向锁,然后锁设置为阻塞状态,目的是告诉用户插入的位置存在间隙锁。

7. 一条 Update 语句没有带 where 条件,加的是什么锁?
#

分析

InnoDB 加锁是索引加锁,可重复读级别下,加锁的基本单位是 next-key 锁。读已提交隔离级别下,加锁的基本单位是记录锁。

更新没有带 where 条件,会全表扫描,会对每一条记录都加锁。

回答

  • 可重复读级别下,更新没有带 where 条件,会全表扫描,会对每一条记录都加 next-key 锁,相当于锁住了全表。
  • 读已提交隔离级别下,没有间隙锁,更新没有带 where 条件,是全表扫描,那么会对每一条记录都加记录锁。

8. 带了 where 条件没有命中索引,加的是什么锁?
#

分析

同上。

回答

没有命中索引,是全表扫描,那么:

  • 在可重复读级别下,全表扫描的话,会对每一条记录都加 next-key 锁。
  • 在读已提交隔离级别下,因为没有间隙锁,全表扫描的时候,会对每一条记录都加记录锁。

9. 两条更新语句更新同一条记录,加的是什么锁?
#

分析

在可重复读级别下,加锁的基本单位是 next-key 锁,但是在一些场景下,会退化成记录锁或者间隙锁。

这道题是更新同一条记录,就认为是等值查询的场景。要考虑这几种情况:

第一种情况:如果更新条件的字段是唯一索引,加什么锁?

当我们用唯一索引进行等值查询的时候,查询的记录存在不存在,加锁的规则也会不同:

  • 当查询的记录是「存在」的,在索引树上定位到这一条记录后,将该记录的索引中的 next-key lock 会退化成「记录锁」。
  • 当查询的记录是「不存在」的,在索引树找到第一条大于该查询记录的记录后,将该记录的索引中的 next-key lock 会退化成「间隙锁」。

第二种情况:如果更新条件的字段是非唯一索引,加什么锁?

当我们用非唯一索引进行等值查询的时候,因为存在两个索引,一个是主键索引,一个是非唯一索引(二级索引),所以在加锁时,同时会对这两个索引都加锁,但是对主键索引加锁的时候,只有满足查询条件的记录才会对它们的主键索引加锁。

针对非唯一索引等值查询时,查询的记录存在不存在,加锁的规则也会不同:

  • 当查询的记录「存在」时,由于不是唯一索引,所以肯定存在索引值相同的记录,于是非唯一索引等值查询的过程是一个扫描的过程,直到扫描到第一个不符合条件的二级索引记录就停止扫描,然后在扫描的过程中,对扫描到的二级索引记录加的是 next-key 锁,而对于第一个不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。同时,在符合查询条件的记录的主键索引上加记录锁。
  • 当查询的记录「不存在」时,扫描到第一条不符合条件的二级索引记录,该二级索引的 next-key 锁会退化成间隙锁。因为不存在满足查询条件的记录,所以不会对主键索引加锁。

第三种情况:如果更新条件的字段是没有索引,加什么锁?

没有加索引的查询,如果锁定读查询语句没有使用索引列作为查询条件,或者查询语句没有走索引查询,导致扫描是全表扫描。那么,每一条记录的索引上都会加 next-key 锁,这样就相当于锁住全表,这时如果其他事务对该表进行增、删、改操作的时候,都会被阻塞。

回答

在可重复读级别下,可能有这些情况:

  • 如果更新条件的字段是唯一索引,还要看更新的记录是否存在:
    • 如果存在,那么这条记录加的是记录锁,只锁住该条记录。
    • 如果这条记录不存在,则加间隙锁。
  • 如果更新条件的字段是非唯一索引,还要看更新的记录是否存在:
    • 如果存在,由于非唯一索引会存在相同值的记录,所以非唯一索引等值查询,实际上是一个扫描的过程,那么会针对符合更新条件的二级索引记录,加 next-key 锁,最后扫描到第一个不符合更新条件的二级索引记录就会停止扫描,然后对第一个不符合更新条件的记录加间隙锁,同时,在符合更新条件的记录的主键索引上加记录锁。
    • 如果不存在,会对第一个不符合更新条件的二级索引记录加间隙锁。
  • 如果更新条件的字段是没有索引或者没有命中索引,那么就是全表扫描,会对每一条记录都加 next-key 锁。

10. 两条更新语句更新同一条记录的不同字段,加的是什么锁?
#

分析

InnoDB 加锁是加在行记录索引上的,不是针对更新的字段加锁。所以是不是更新同一个字段,没有关系。只要在这条记录上有更新操作,就会对这条记录加锁。所以这题的加锁情况,也跟上一题一样。

回答

在可重复读级别下,可能有这些情况:

  • 如果更新条件的字段是唯一索引,还要看更新的记录是否存在:
    • 如果存在,那么这条记录加的是记录锁,只锁住该条记录。
    • 如果这条记录不存在,则加间隙锁。
  • 如果更新条件的字段是非唯一索引,还要看更新的记录是否存在:
    • 如果存在,由于非唯一索引会存在相同值的记录,所以非唯一索引等值查询,实际上是一个扫描的过程,那么会针对符合更新条件的二级索引记录,加 next-key 锁,最后扫描到第一个不符合更新条件的二级索引记录就会停止扫描,然后对第一个不符合更新条件的记录加间隙锁,同时,在符合更新条件的记录的主键索引上加记录锁。
    • 如果不存在,会对第一个不符合更新条件的二级索引记录加间隙锁。
  • 如果更新条件的字段是没有索引或者没有命中索引,那么就是全表扫描,会对每一条记录都加 next-key 锁。

11. 可重复读场景,下面的场景会发生什么?
#

题目描述:

(id, no, name, age, score)

(15, S0001, Bob, 25, 34)
(18, S0002, Alice, 24, 77)
(20, S0003, Jim, 24, 5)
(30, S0004, Eric, 23, 91)
(37, S0005, Tom, 22, 22)
(49, S0006, Tom, 25, 83)
(50, S0007, Rose, 23, 89)

事务A执行:
time1: update students set score=100 where id=25
time3: insert into students(id, no, name, age, score) value(25, 'S0025', 'sony', 28, 90)

事务B执行:
time2: update students set score=100 where id=26
time4: insert into students(id, no, name, age, score) value(26, 'S0026', 'ace', 28, 90)

分析

这题要注意的是事务 A 和事务 B 更新语句,都是更新不存在的 id,这时候会加间隙锁,有了间隙锁就会阻止插入操作。

事务 A 和事务 B 都在执行 insert 语句后,都陷入了等待状态,都在相互等待对方释放间隙锁,于是就发生了死锁。

alt text

回答

事务 A 和事务 B 在执行完 update 语句后都持有范围为 (20, 30) 的间隙锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。

12. 了解过 MySQL 死锁问题吗?
#

分析

解释 MySQL 死锁是如何发生的。

回答

了解过,在并发事务中,当两个事务出现循环资源依赖,这两个事务都在等待别的事务释放资源时,就会导致这两个事务都进入无限等待的状态,这时候就发生了死锁。

13. MySQL 怎么排查死锁问题?
#

分析

获取死锁日志,分析死锁日志。

回答

在遇到线上死锁问题时,我们应该第一时间获取相关的死锁日志。我们可以通过 show engine innodb status 命令来获取死锁信息。

然后就分析死锁日志。死锁日志通常分为两部分,上半部分说明了事务 1 在等待什么锁,下半部分说明了事务 2 当前持有的锁和等待的锁。

通过阅读死锁日志,我们可以清楚地知道两个事务形成了怎样的循环等待,然后根据当前各个事务执行的 SQL 分析出加锁类型以及顺序,逆向推断出如何形成循环等待,这样就能找到死锁产生的原因了。

14. MySQL 怎么避免死锁?
#

分析

要明确 MySQL 死锁是不能完全避免的,得通过一些手段降低死锁的概率。

  • 缩短锁持久的时间,来降低死锁的概率。
  • 通过减少间隙锁,来降低死锁的概率。
  • 通过减少加锁范围,来降低死锁的概率。
  • 通过 MySQL 参数设置,来降低死锁的概率:
    • 设置锁等待超时参数:innodb_lock_wait_timeout
    • 开启主动死锁检测:innodb_deadlock_detect

回答

实际上死锁是不能完全避免的,只要会加锁,在并发的场景就会发生死锁,但是我们可以通过一些手段,降低发生死锁的概率。

MySQL 的锁是在事务提交的时候才会释放的,所以可以通过缩短锁持久的时间,来降低死锁的概率:

  • 如果事务中需要锁多个行,要把最可能造成锁冲突的锁申请时机尽量往后放,这样事务的持久锁时间就会比较短。
  • 避免大事务,尽量将大事务拆成多个小事务来处理,因为大事务占用耗时长,意味着占用锁占用时间长,与其他事务冲突的概率也会变高。

可以通过减少间隙锁,来降低死锁的概率:

  • 如果能确定幻读和不可重复读对应的影响不大,可以考虑将隔离级别改成 RC,因为 RC 隔离级别没有间隙锁,可以避免间隙锁导致的死锁。

可以通过减少加锁范围,来降低死锁的概率:

  • 给表添加合理的索引,如果不走索引将会为表的每一行记录加行级锁,死锁的概率就会大大增大。

可以通过 MySQL 参数设置,来降低死锁的概率:

  • 设置合适的锁等待超时时间,当一个事务的等待时间超过该值后,将回滚当前语句(而不是整个事务),如果要回滚整个事务,需要将 innodb_rollback_on_timeout 开启值为 ON。开启这个参数之后,锁超时就会对这个事务进行回滚,于是锁就释放了。
  • 开启主动死锁检测,主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。
MySQL面试题库 - 这篇文章属于一个选集。
§ 6: 本文