📌 PDF :大白话说Java面试题 --- 03-Mysql篇
第16题:MySQL 中锁的种类与行锁实现原理
📚 回答:
- 核心考点 :
大厂面试要求深入理解锁的分类体系 、InnoDB行锁的底层实现 、各种锁的加锁时机与兼容性 ,以及行锁与隔离级别的关联。面试官常追问:"Record Lock、Gap Lock、Next-Key Lock有什么区别?"、"意向锁的作用是什么?"、"行锁是怎么在索引上实现的?"
1. 锁的完整分类体系
MySQL的锁可以从三个维度分类:加锁思想 、锁粒度 、锁类型。
| 分类维度 | 类型 | 说明 |
|---|---|---|
| 加锁思想 | 乐观锁 / 悲观锁 | 逻辑上的并发控制策略,非数据库内置锁 |
| 锁粒度 | 表锁 / 页锁 / 行锁 | 锁的范围大小 |
| 锁类型(InnoDB行锁) | Record Lock / Gap Lock / Next-Key Lock / Insert Intention Lock | InnoDB行锁的具体实现 |
| 表级辅助锁 | 意向锁(IS/IX) | 用于表锁与行锁的协调 |
2. 乐观锁 vs 悲观锁(并发控制思想)
| 对比 | 乐观锁 | 悲观锁 |
|---|---|---|
| 核心思想 | 假设冲突少,先操作后检测 | 假设冲突多,先加锁后操作 |
| 实现方式 | 版本号(version)、CAS | SELECT FOR UPDATE、LOCK IN SHARE MODE |
| 是否数据库内置 | 否(应用层实现) | 否(依赖于数据库提供的锁) |
| 适用场景 | 读多写少 | 写多读少 |
乐观锁示例:
sql
-- 使用version字段实现乐观锁
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5;
-- 检查affected_rows,为0则重试
悲观锁示例:
sql
START TRANSACTION;
SELECT * FROM products WHERE id = 1 FOR UPDATE; -- 加排他锁
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
3. 按锁粒度分类
| 锁粒度 | 支持引擎 | 特点 | 性能 |
|---|---|---|---|
| 表锁 | MyISAM、InnoDB(DDL/意向锁) | 锁定整表,简单但并发差 | 低 |
| 页锁 | BDB(已废弃) | 锁定数据页,介于表锁和行锁之间 | 中 |
| 行锁 | InnoDB | 锁定索引记录,并发高 | 高 |
InnoDB表锁场景:
- DDL操作(如
ALTER TABLE)会加表锁 - 事务执行
LOCK TABLES ... WRITE显式加表锁 - 意向锁(IS/IX)是表级锁,用于快速判断表锁兼容性
4. InnoDB 行锁的四种类型(核心)
4.1 记录锁(Record Lock)
- 定义 :锁定索引记录(不是行本身),防止其他事务修改或删除该记录
- 加锁对象:索引项(没有索引时自动使用隐藏聚簇索引)
- SQL示例:
sql
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 锁定id=1的索引记录
4.2 间隙锁(Gap Lock)
- 定义 :锁定索引记录之间的间隙(开区间),不包括记录本身
- 作用 :防止其他事务在间隙中插入新记录,避免幻读
- 生效条件:RR隔离级别及以上
- SQL示例:
sql
-- 假设id值:1, 3, 5, 7, 9
SELECT * FROM users WHERE id BETWEEN 3 AND 5 FOR UPDATE;
-- 锁定间隙:(1,3)、(3,5)、(5,7)
4.3 Next-Key Lock
- 定义 :Record Lock + Gap Lock ,锁定一个左开右闭的区间
- 计算公式:Next-Key Lock = (前一个值, 当前值] 的间隙锁 + 当前值的记录锁
- 作用 :InnoDB在RR级别下的默认行锁算法,同时防止幻读和保证当前读一致性
- SQL示例:
sql
-- 假设id值:1, 3, 5, 7, 9
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- Next-Key Lock锁定范围:(3,5] 和 (5,7]
-- 即锁住id=5的记录,以及(3,5)和(5,7)的间隙
4.4 插入意向锁(Insert Intention Lock)
- 定义 :一种特殊的间隙锁,表示事务意图在某个间隙中插入数据
- 特性 :多个事务可以同时在同一间隙中持有插入意向锁,只要插入的位置不冲突(不同索引值)
- 作用:提高插入并发性,避免间隙锁完全阻塞所有插入
sql
-- 事务A锁定间隙(3,5)
SELECT * FROM users WHERE id = 4 FOR UPDATE; -- 加间隙锁(3,5)
-- 事务B在间隙(3,5)中插入id=4.5
INSERT INTO users VALUES (4.5, 'xxx'); -- 阻塞(与间隙锁冲突)
-- 事务C在间隙(3,5)中插入id=4.8
INSERT INTO users VALUES (4.8, 'xxx'); -- 也阻塞(间隙锁阻塞所有插入)
5. 行锁的加锁规则与退化
唯一索引等值查询:
| 条件 | 加锁类型 | 示例(id主键:1,3,5,7,9) |
|---|---|---|
| 命中 | Record Lock | WHERE id=5 → 锁5(Next-Key锁退化) |
| 未命中 | Gap Lock | WHERE id=4 → 锁(3,5)间隙 |
普通索引等值查询:
| 条件 | 加锁类型 | 示例(age普通索引:1,3,5,7,9) |
|---|---|---|
| 命中 | Next-Key Lock | WHERE age=5 → 锁(3,5]和(5,7) |
| 未命中 | Gap Lock | WHERE age=4 → 锁(3,5)间隙 |
范围查询:
sql
-- 唯一索引范围
SELECT * FROM users WHERE id > 5 FOR UPDATE;
-- 锁:所有id>5的Record Lock + 后续Gap Lock到无穷大
-- 普通索引范围
SELECT * FROM users WHERE age > 5 FOR UPDATE;
-- 锁:所有age>5的Next-Key Lock
6. 意向锁(Intention Lock)详解
6.1 什么是意向锁
意向锁是表级锁 ,表示事务意图对表中的某些行加锁。
| 锁类型 | 含义 | 加锁时机 |
|---|---|---|
| 意向共享锁(IS) | 事务意图对某些行加共享锁(S锁) | SELECT ... LOCK IN SHARE MODE |
| 意向排他锁(IX) | 事务意图对某些行加排他锁(X锁) | SELECT ... FOR UPDATE、UPDATE、DELETE |
6.2 意向锁的作用
避免表锁与行锁的冲突检测遍历:
- 事务A要对整表加
LOCK TABLES ... WRITE(表级X锁) - 如果没有意向锁,需要遍历所有行检查是否有行锁 → O(n)复杂度
- 有意向锁时,只需检查表上的意向锁 → O(1)复杂度
6.3 意向锁兼容性矩阵
| 锁类型 | IS | IX | S(表级) | X(表级) |
|---|---|---|---|---|
| IS | ✅ | ✅ | ✅ | ❌ |
| IX | ✅ | ✅ | ❌ | ❌ |
| S(表级) | ✅ | ❌ | ✅ | ❌ |
| X(表级) | ❌ | ❌ | ❌ | ❌ |
关键规律:
- 意向锁之间相互兼容(IS与IS、IS与IX、IX与IX都兼容)
- 意向锁与表级共享锁(S)部分兼容(IS兼容,IX不兼容)
- 意向锁与表级排他锁(X)不兼容
7. 行锁与隔离级别的关系
| 隔离级别 | 是否使用Gap Lock | 幻读风险 | 说明 |
|---|---|---|---|
| READ UNCOMMITTED | ❌ | 有 | 几乎不加锁 |
| READ COMMITTED | ❌ | 有 | 只有Record Lock,无Gap Lock |
| REPEATABLE READ | ✅ | 无 | InnoDB默认,使用Next-Key Lock |
| SERIALIZABLE | ✅ | 无 | 所有SELECT隐式加锁 |
RC vs RR的行锁差异:
sql
-- RC级别:只有Record Lock
-- RR级别:Next-Key Lock(Record Lock + Gap Lock)
-- 示例:假设id值:1, 3, 5, 7, 9
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- RC级别:只锁id=5的记录(其他事务可插入4、6)
-- RR级别:锁(3,5]和(5,7)(其他事务不可插入4、6)
8. 行锁的实现原理(源码层面)
8.1 行锁加在索引上
InnoDB的行锁本质是索引项锁:
- 通过主键索引查询 → 锁聚簇索引记录
- 通过二级索引查询 → 先锁二级索引记录,再锁聚簇索引记录
为什么必须是索引?
- 行锁通过索引定位记录
- 没有索引条件时,InnoDB会锁全表(所有聚簇索引记录)
8.2 两阶段锁协议(2PL)
- 加锁阶段:事务开始到提交前,可以随时加锁
- 解锁阶段:事务提交或回滚时,统一释放所有锁
- 无中途解锁 :InnoDB不支持
UNLOCK操作
8.3 锁的内存结构
每个锁在内存中对应一个lock_t结构,包含:
trx_id:持有锁的事务IDtype_mode:锁类型(S/X/IS/IX/GAP等)index:锁定的索引bitmap:行记录位图(一个锁可锁定多条记录)
9. 死锁检测与处理
9.1 死锁产生条件
四个必要条件:互斥、持有并等待、不可剥夺、循环等待。
9.2 典型死锁场景
sql
-- 事务A
START TRANSACTION;
UPDATE users SET name='A' WHERE id=1; -- 锁id=1
UPDATE users SET name='A' WHERE id=2; -- 等待事务B释放id=2
-- 事务B
START TRANSACTION;
UPDATE users SET name='B' WHERE id=2; -- 锁id=2
UPDATE users SET name='B' WHERE id=1; -- 等待事务A释放id=1
-- 死锁形成
9.3 InnoDB死锁处理机制
- 死锁检测 :维护事务等待图(
trx_t和lock_t),检测到循环等待时立即处理 - 死锁回滚 :回滚代价较小的事务(修改行数少的)
- 参数控制 :
innodb_deadlock_detect=ON(默认开启)innodb_lock_wait_timeout=50(死锁检测超时,默认50秒)
查看死锁信息:
sql
SHOW ENGINE INNODB STATUS; -- 查看最近死锁
SELECT * FROM information_schema.INNODB_TRX; -- 查看当前事务
SELECT * FROM information_schema.INNODB_LOCKS; -- 查看当前锁
10. 总结对比表
| 锁类型 | 粒度 | 作用 | 是否阻塞插入 | RR级别默认 |
|---|---|---|---|---|
| Record Lock | 行(索引记录) | 锁定单条记录 | 否 | ✅ |
| Gap Lock | 间隙 | 锁定索引间隙,防幻读 | ✅ | ✅ |
| Next-Key Lock | 行+间隙 | 锁定记录及前间隙 | ✅ | ✅(默认) |
| Insert Intention Lock | 间隙 | 表示意图插入 | 特殊(位置不冲突时兼容) | ✅ |
| 意向锁(IS/IX) | 表 | 协调表锁与行锁 | 否 | ✅ |
💡 面试官想要的满分总结:
"InnoDB的锁体系从三个维度理解:
一、按加锁思想:乐观锁(版本号/CAS,应用层实现)和悲观锁(数据库行锁/表锁)。
二、按锁粒度:表锁(MyISAM/DDL)、行锁(InnoDB核心)、页锁(已废弃BDB)。
三、InnoDB行锁四种类型:
Record Lock:锁定索引记录(行锁的基础)
Gap Lock:锁定索引间隙,防幻读(RR级别生效)
Next-Key Lock:Record Lock + Gap Lock,RR级别默认算法
Insert Intention Lock:特殊的间隙锁,提高插入并发性
**意向锁(IS/IX)**是表级锁,用于快速判断表锁兼容性,避免遍历所有行检查行锁。
实现原理:行锁实质是索引项锁,通过索引定位记录
遵循两阶段锁协议(2PL):事务结束时统一释放锁
死锁检测通过等待图实现,回滚代价较小的事务
一句话:InnoDB行锁 = 基于索引的记录锁 + RR级别下的间隙锁(防幻读),通过意向锁快速协调表锁与行锁,通过死锁检测自动处理循环等待。"
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯