43. MySQL 中有哪几种锁?列举一下。
答:
MySQL 里的锁可以从多个维度分类。 面试时如果只答“表锁、行锁”不算错,但不够完整。更推荐从下面几个角度去讲:
- 按锁粒度划分
- 按锁的读写性质划分
- 按 InnoDB 行锁实现细分
1)按锁粒度划分
这是最常见的一种分类。
(1)表锁
表锁是对整张表加锁。
特点:
- 加锁快
- 开销小
- 冲突概率高
- 并发度低
常见于:
- MyISAM
- 某些
LOCK TABLES - 元数据锁场景
一旦给整张表加了排他锁,其他事务对这张表的读写都会受到很大影响。
(2)行锁
行锁是对某一行记录加锁。
特点:
- 锁粒度小
- 并发能力强
- 加锁开销更大
- 可能发生死锁
InnoDB 支持行锁,但要注意:
InnoDB 的行锁其实是加在索引上的,不是直接锁物理行。
如果查询条件没走索引,可能就会锁很多行,甚至效果接近表锁。
(3)页锁
页锁是对数据页加锁,粒度介于表锁和行锁之间。 它在 MySQL 面试中提得相对少,更多是概念性了解。
特点:
- 加锁粒度居中
- 并发能力一般
- 可能死锁
MySQL 主流讨论中更常关注表锁和行锁。
2)按锁的读写性质划分
(1)共享锁(S 锁)
共享锁也叫读锁。
特点:
- 多个事务可以同时持有共享锁
- 允许读
- 不允许其他事务加排他锁写入
例如某事务读一条记录并加共享锁后,其他事务也可以读,但不能修改。
(2)排他锁(X 锁)
排他锁也叫写锁。
特点:
- 一个事务持有排他锁后,其他事务不能再对该数据加共享锁或排他锁
- 也就是既不允许别人读锁,也不允许别人写锁(严格来说普通快照读另说,当前读会受影响)
排他锁通常出现在:
updatedeleteselect ... for update
3)按 InnoDB 行锁实现细分
InnoDB 的“行锁”其实还可以细分为:
- Record Lock(记录锁)
- Gap Lock(间隙锁)
- Next-Key Lock(临键锁)
- Insert Intention Lock(插入意向锁)
这部分通常是面试重点。
4)总结
如果面试官问 MySQL 有哪些锁,你可以这样答:
MySQL 的锁可以按粒度分为表锁、行锁、页锁;按性质分为共享锁和排他锁;在 InnoDB 中,行锁又进一步分为记录锁、间隙锁、临键锁和插入意向锁。
一句话总结:
MySQL 锁的本质是并发控制手段,粒度越大并发越低,粒度越小控制越精细。
44. 说说 InnoDB 里的行锁实现?
答:
InnoDB 的行锁并不是简单的“锁住一行数据”,而是基于索引实现的一套锁机制。 常见的实现主要有四种:
- Record Lock(记录锁)
- Gap Lock(间隙锁)
- Next-Key Lock(临键锁)
- 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 = 1000version = 3更新时带上版本条件:
UPDATE accountSET balance = 900, version = version + 1WHERE id = 1 AND version = 3;如果这条 SQL 影响行数为 1,说明没人改过,更新成功。 如果影响行数为 0,说明版本不匹配,数据已经被别人改过,需要重试或报错。
特点
- 不阻塞读写
- 并发性能好
- 但更新冲突时需要业务处理失败重试
适用场景
适合:
- 读多写少
- 并发冲突概率低
- 能接受重试机制
- 应用层自己控制并发一致性
3)两者对比
| 对比项 | 悲观锁 | 乐观锁 |
|---|---|---|
| 冲突假设 | 假设冲突多 | 假设冲突少 |
| 控制方式 | 先加锁再操作 | 更新时校验 |
| 并发性能 | 较低 | 较高 |
| 一致性控制 | 数据库层直接保证 | 应用层参与较多 |
| 常见实现 | for update | version/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;假设执行顺序是:
- A 先锁住
id=1 - B 先锁住
id=2 - A 再去锁
id=2,但被 B 占着,只能等 - 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)一句话总结
死锁的本质是循环等待,排查要看死锁日志,解决重点是统一加锁顺序、缩小锁范围、缩短事务时间。