3749 字
19 分钟
100 道常见 MySQL 数据库题目锁篇
2026-04-16 15:28:46
无标签

43. MySQL 中有哪几种锁?列举一下。#

答:

MySQL 里的锁可以从多个维度分类。 面试时如果只答“表锁、行锁”不算错,但不够完整。更推荐从下面几个角度去讲:

  1. 按锁粒度划分
  2. 按锁的读写性质划分
  3. 按 InnoDB 行锁实现细分

1)按锁粒度划分#

这是最常见的一种分类。

(1)表锁#

表锁是对整张表加锁。

特点:

  • 加锁快
  • 开销小
  • 冲突概率高
  • 并发度低

常见于:

  • MyISAM
  • 某些 LOCK TABLES
  • 元数据锁场景

一旦给整张表加了排他锁,其他事务对这张表的读写都会受到很大影响。


(2)行锁#

行锁是对某一行记录加锁。

特点:

  • 锁粒度小
  • 并发能力强
  • 加锁开销更大
  • 可能发生死锁

InnoDB 支持行锁,但要注意:

InnoDB 的行锁其实是加在索引上的,不是直接锁物理行。

如果查询条件没走索引,可能就会锁很多行,甚至效果接近表锁。


(3)页锁#

页锁是对数据页加锁,粒度介于表锁和行锁之间。 它在 MySQL 面试中提得相对少,更多是概念性了解。

特点:

  • 加锁粒度居中
  • 并发能力一般
  • 可能死锁

MySQL 主流讨论中更常关注表锁和行锁。


2)按锁的读写性质划分#

(1)共享锁(S 锁)#

共享锁也叫读锁

特点:

  • 多个事务可以同时持有共享锁
  • 允许读
  • 不允许其他事务加排他锁写入

例如某事务读一条记录并加共享锁后,其他事务也可以读,但不能修改。


(2)排他锁(X 锁)#

排他锁也叫写锁

特点:

  • 一个事务持有排他锁后,其他事务不能再对该数据加共享锁或排他锁
  • 也就是既不允许别人读锁,也不允许别人写锁(严格来说普通快照读另说,当前读会受影响)

排他锁通常出现在:

  • update
  • delete
  • select ... for update

3)按 InnoDB 行锁实现细分#

InnoDB 的“行锁”其实还可以细分为:

  • Record Lock(记录锁)
  • Gap Lock(间隙锁)
  • Next-Key Lock(临键锁)
  • Insert Intention Lock(插入意向锁)

这部分通常是面试重点。


4)总结#

如果面试官问 MySQL 有哪些锁,你可以这样答:

MySQL 的锁可以按粒度分为表锁、行锁、页锁;按性质分为共享锁和排他锁;在 InnoDB 中,行锁又进一步分为记录锁、间隙锁、临键锁和插入意向锁。

一句话总结:

MySQL 锁的本质是并发控制手段,粒度越大并发越低,粒度越小控制越精细。


44. 说说 InnoDB 里的行锁实现?#

答:

InnoDB 的行锁并不是简单的“锁住一行数据”,而是基于索引实现的一套锁机制。 常见的实现主要有四种:

  1. Record Lock(记录锁)
  2. Gap Lock(间隙锁)
  3. Next-Key Lock(临键锁)
  4. Insert Intention Lock(插入意向锁)

理解这几种锁,是理解 InnoDB 如何解决并发更新和幻读问题的关键。


1)Record Lock(记录锁)#

记录锁就是:

直接锁住某条索引记录本身。


例子

假设表中主键有这些值:

1, 6, 8, 12

执行:

SELECT * FROM user WHERE id = 6 FOR UPDATE;

如果 id 是主键,并且精确命中一条记录,那么 InnoDB 通常会对 id=6 这一条索引记录加记录锁。


特点

  • 只锁住这条记录
  • 不锁前后间隙
  • 常出现在唯一索引等值命中场景

2)Gap Lock(间隙锁)#

间隙锁是:

锁住两条索引记录之间的“空隙”,但不锁具体记录本身。

它锁的是一个范围内“还没有数据的位置”。


例子

表中已有主键:

1, 6, 8, 12

执行:

SELECT * FROM user WHERE id = 3 FOR UPDATE;

因为 id=3 这条记录并不存在,所以 InnoDB 可能会锁住区间:

(1, 6)

也就是 1 和 6 之间的间隙。


作用

主要是为了防止其他事务在这个间隙里插入新记录,从而避免幻读。


特点

  • 锁空隙,不锁现有记录
  • 防止插入
  • 常见于范围查询或等值查询未命中时

3)Next-Key Lock(临键锁)#

临键锁是:

记录锁 + 间隙锁 的组合

它锁住的是一个“左开右闭”的区间。

例如:

  • (1, 6]
  • (6, 8]

也就是:

  • 锁住前面的间隙
  • 也锁住右边那条记录

例子

还是这组数据:

1, 6, 8, 12

执行:

SELECT * FROM user WHERE id > 5 AND id <= 7 FOR UPDATE;

这里可能锁住:

  • (1, 6]
  • (6, 8)

或者按具体范围落在相应临键区间上。

通常 MySQL 默认行锁机制就是 Next-Key Lock,尤其在可重复读隔离级别下。


作用

Next-Key Lock 的目标也是:

防止幻读

因为它不仅锁记录,还顺便把前面的间隙锁住了,别人就不能在这个范围偷偷插入新数据。


4)Insert Intention Lock(插入意向锁)#

插入意向锁是:

当事务准备向某个间隙插入记录时,先声明“我想插这里”的一种锁。

它不是说插入已经成功,而是表示:

  • 我准备在这个 gap 中插入
  • 如果 gap 没被别的锁阻塞,我就继续插

例子

假设某事务 T1 已对间隙 (1,6) 加了 gap lock。 这时事务 T2 想插入 id=4

INSERT INTO user(id) VALUES(4);

那么 T2 会在 (1,6) 上申请插入意向锁,并等待。 如果同时 T3 想插入 id=3,它也会申请插入意向锁。


特点

  • 插入意向锁之间不互斥
  • 但会和 gap lock / next-key lock 互斥
  • 本质上是插入前的等待声明

5)总结这四种锁#

锁类型锁定对象作用
Record Lock某条记录防止修改/删除该记录
Gap Lock记录之间的间隙防止插入
Next-Key Lock间隙 + 右侧记录防止插入 + 锁记录
Insert Intention Lock插入目标间隙的意向声明准备插入

6)一句话总结#

InnoDB 的行锁本质上是“索引锁”,通过记录锁、间隙锁和临键锁组合,既控制并发更新,又解决幻读问题。


45. 意向锁是什么,知道吗?#

答:

意向锁(Intention Lock)是 InnoDB 中一种表级锁,它的作用不是直接锁具体数据,而是:

表示“某个事务准备在这张表的某些行上加什么类型的锁”。

它主要是为了解决:

表锁和行锁共存时,如何高效判断是否冲突。


1)为什么需要意向锁?#

想象一个问题:

如果事务 A 给表里的某几行加了行锁, 这时事务 B 想给整张表加表锁,MySQL 怎么知道能不能加?

如果没有意向锁,数据库就得:

  • 去扫描整张表
  • 看每一行是否已经被加锁

这显然效率太低。

所以 InnoDB 引入意向锁,让事务在加行锁之前,先在表级别做一个“声明”。


2)意向锁的本质#

可以把它理解成:

“我后面打算给这张表里的某些行加锁,你先别急着给整张表加互斥锁。”

也就是说,意向锁本质是一种锁的声明机制


3)意向锁有哪些类型?#

常见有两种:

(1)意向共享锁(IS)#

表示:

事务准备在表中的某些行上加共享锁(S 锁)


(2)意向排他锁(IX)#

表示:

事务准备在表中的某些行上加排他锁(X 锁)

例如执行:

SELECT * FROM user WHERE id = 1 FOR UPDATE;

在给 id=1 这一行加行级排他锁前,InnoDB 会先给整张表加一个 IX(意向排他锁)


4)意向锁解决了什么问题?#

它解决的是:

表锁和行锁共存时的快速冲突判断问题。

如果某张表上已经有意向排他锁,那么另一个事务想加表级排他锁时,就能快速知道:

  • 表里已经有行锁活动
  • 当前不能直接加整表互斥锁

而不必扫描整张表每一行。


5)意向锁之间冲突吗?#

通常:

  • 意向锁之间一般不冲突
  • 它主要是和表级排他锁冲突

因为它只是“声明”,不是实际锁定某一行数据。


6)不要和插入意向锁混淆#

这两个名字很像,但完全不是一回事。

意向锁#

  • 表级锁
  • 用于表锁和行锁协调

插入意向锁#

  • Gap 上的一种锁
  • 用于插入等待控制

面试里这两个很容易被混淆,要分清楚。


7)一句话总结#

意向锁是一种表级声明锁,作用是让 InnoDB 能快速判断表锁和行锁是否冲突。


46. MySQL 的乐观锁和悲观锁了解吗?#

答:

乐观锁和悲观锁不是 MySQL 独有概念,而是并发控制的两种思想。 它们的核心区别在于:

  • 悲观锁:先假设冲突很多,先锁住再操作
  • 乐观锁:先假设冲突很少,提交时再检查是否冲突

1)悲观锁#

悲观锁认为:

并发冲突很可能发生,所以每次操作数据前都先加锁。

也就是说,它对并发修改持“悲观”态度。


在 MySQL 里的体现

常见悲观锁操作包括:

SELECT * FROM user WHERE id = 1 FOR UPDATE;

或者:

SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;

其中:

  • FOR UPDATE 通常加排他锁
  • LOCK IN SHARE MODE(新版本推荐 FOR SHARE)加共享锁

这些都属于典型悲观锁思路。


特点

  • 安全性高
  • 冲突时控制更直接
  • 但容易阻塞,影响并发性能

适用场景

适合:

  • 冲突概率高
  • 数据一致性要求高
  • 金融、库存扣减、强一致更新

2)乐观锁#

乐观锁认为:

并发冲突不常发生,所以先不加锁,提交更新时再判断数据是否被别人改过。

它对并发持“乐观”态度。


常见实现方式

通常通过:

  • 版本号(version)
  • 时间戳(timestamp)

来实现。


例子:版本号方式

表里有字段:

id | balance | version

读取数据时拿到:

balance = 1000
version = 3

更新时带上版本条件:

UPDATE account
SET balance = 900, version = version + 1
WHERE id = 1 AND version = 3;

如果这条 SQL 影响行数为 1,说明没人改过,更新成功。 如果影响行数为 0,说明版本不匹配,数据已经被别人改过,需要重试或报错。


特点

  • 不阻塞读写
  • 并发性能好
  • 但更新冲突时需要业务处理失败重试

适用场景

适合:

  • 读多写少
  • 并发冲突概率低
  • 能接受重试机制
  • 应用层自己控制并发一致性

3)两者对比#

对比项悲观锁乐观锁
冲突假设假设冲突多假设冲突少
控制方式先加锁再操作更新时校验
并发性能较低较高
一致性控制数据库层直接保证应用层参与较多
常见实现for updateversion/timestamp

4)一句话总结#

悲观锁是“先锁再改”,乐观锁是“先改前提下提交时校验”。


47. MySQL 遇到过死锁问题吗?你是如何解决的?#

答:

死锁是数据库并发里很典型的问题。 它指的是:

两个或多个事务互相持有对方需要的锁,并且彼此等待,最终谁也无法继续执行。

InnoDB 支持死锁检测,所以遇到死锁时,MySQL 通常会主动回滚其中一个事务,让另一个事务继续执行。


1)死锁是怎么发生的?#

一个经典例子:

事务 A#

UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;

事务 B#

UPDATE account SET balance = balance - 50 WHERE id = 2;
UPDATE account SET balance = balance + 50 WHERE id = 1;

假设执行顺序是:

  1. A 先锁住 id=1
  2. B 先锁住 id=2
  3. A 再去锁 id=2,但被 B 占着,只能等
  4. B 再去锁 id=1,但被 A 占着,只能等

这样就形成了循环等待,也就是死锁。


2)MySQL 遇到死锁后会怎样?#

InnoDB 会检测死锁。 一旦发现死锁,会自动选择一个“代价较小”的事务回滚,并返回错误:

Deadlock found when trying to get lock; try restarting transaction

也就是说:

  • 不会无限卡死
  • 但业务会收到异常,需要处理

3)怎么排查死锁?#

(1)查看死锁日志#

最常用命令:

SHOW ENGINE INNODB STATUS;

里面会输出最近一次死锁的详细信息,包括:

  • 哪两个事务发生死锁
  • 各自执行了什么 SQL
  • 持有什么锁
  • 等待什么锁

这是排查死锁最重要的入口。


(2)找到相关 SQL#

确认死锁发生时,涉及的是哪些 SQL、哪些表、哪些索引。


(3)分析加锁顺序#

大多数死锁,本质上是:

多个事务加锁顺序不一致。

比如:

  • A 先锁 1 再锁 2
  • B 先锁 2 再锁 1

这就是经典死锁模型。


(4)模拟复现#

如果现场复杂,可以在测试环境复现同样 SQL 和顺序,确认死锁原因。


4)如何解决死锁?#

方法一:统一加锁顺序#

这是最有效的方法。

例如无论哪个事务,都规定:

  • 先锁 id 小的
  • 再锁 id 大的

这样就不会形成循环等待。


方法二:尽量缩小锁范围#

确保 SQL:

  • 命中索引
  • 只锁必要记录
  • 避免无索引更新

这样可以减少锁冲突概率。


方法三:缩短事务时间#

事务越长,锁持有时间越长,越容易死锁。 所以要尽量:

  • 少做无关操作
  • 不要在事务里调用外部接口
  • 不要在事务里做复杂计算

方法四:拆大事务#

大事务往往锁很多行、持续很久,容易和别人形成复杂冲突。 能拆就拆。


方法五:业务侧重试#

因为数据库已经帮你回滚了一个事务,所以业务层通常可以:

  • 捕获死锁异常
  • 做有限次数重试

很多系统都会对死锁和锁等待超时做自动重试机制。


5)面试回答模板#

如果面试官问“遇到过死锁吗,怎么处理”,你可以这么答:

遇到过。一般先通过 SHOW ENGINE INNODB STATUS 查看最近一次死锁日志,定位涉及的事务和 SQL,然后重点分析加锁顺序、索引是否命中、锁范围是否过大。解决上通常会统一加锁顺序、缩短事务、避免大事务和无索引更新,并在业务层对死锁异常做有限重试。


6)一句话总结#

死锁的本质是循环等待,排查要看死锁日志,解决重点是统一加锁顺序、缩小锁范围、缩短事务时间。

Comment seems to stuck. Try to refresh?✨