Skip to content

事务与锁机制深度解析

面试官:MySQL 的事务有什么特性?

:ACID,即原子性、一致性、隔离性、持久性…

面试官:那 InnoDB 是如何保证这四个特性的?

这个追问把很多人问住了。能说清「Redo Log 保证持久性、Undo Log 保证原子性、MVCC 保证隔离性」的候选人,才能真正脱颖而出。


Q1:事务的 ACID 特性分别是什么?InnoDB 如何保证?

Section titled “Q1:事务的 ACID 特性分别是什么?InnoDB 如何保证?”
必考

ACID 四大特性

特性含义InnoDB 实现机制
A - 原子性事务要么全部成功,要么全部失败Undo Log(回滚日志)
C - 一致性事务前后数据保持一致状态由 A、I、D 共同保证
I - 隔离性并发事务之间互不干扰锁 + MVCC
D - 持久性事务提交后数据永久保存Redo Log(重做日志)

实现原理详解

事务执行过程:
1. 修改数据前 → 写 Undo Log
┌─────────────────────────────┐
│ Undo Log: 记录修改前的数据 │
│ 用于回滚和 MVCC │
└─────────────────────────────┘
2. 修改数据 → 写 Buffer Pool(内存)
┌─────────────────────────────┐
│ Buffer Pool: 数据页缓存 │
│ 修改在内存中,不直接写磁盘 │
└─────────────────────────────┘
3. 修改数据 → 写 Redo Log Buffer
┌─────────────────────────────┐
│ Redo Log: 记录修改后的数据 │
│ 用于崩溃恢复 │
└─────────────────────────────┘
4. 事务提交 → 刷 Redo Log 到磁盘
┌─────────────────────────────┐
│ Redo Log 持久化 │
│ 保证持久性 │
└─────────────────────────────┘

崩溃恢复流程

MySQL 崩溃重启后:
1. 读取 Redo Log → 重做已提交事务的修改
2. 读取 Undo Log → 回滚未提交事务的修改
结果:数据恢复到一致状态

Q2:事务的隔离级别有哪些?分别解决什么问题?

Section titled “Q2:事务的隔离级别有哪些?分别解决什么问题?”
必考

四种隔离级别

隔离级别脏读不可重复读幻读实现机制
读未提交❌ 可能❌ 可能❌ 可能
读已提交✅ 避免❌ 可能❌ 可能MVCC(每次读取生成新快照)
可重复读✅ 避免✅ 避免⚠️ 可能MVCC(事务开始时生成快照)
串行化✅ 避免✅ 避免✅ 避免锁(读加共享锁,写加排他锁)

三种读问题详解

-- 1. 脏读:读到其他事务未提交的数据
事务A: UPDATE users SET age = 30 WHERE id = 1; -- 未提交
事务B: SELECT age FROM users WHERE id = 1; -- 读到 30
事务A: ROLLBACK; -- 回滚
-- 事务B 读到了"脏数据"
-- 2. 不可重复读:同一事务内两次读取结果不同(针对 UPDATE)
事务A: SELECT age FROM users WHERE id = 1; -- 读到 25
事务B: UPDATE users SET age = 30 WHERE id = 1;
事务B: COMMIT;
事务A: SELECT age FROM users WHERE id = 1; -- 读到 30
-- 事务A 两次读取结果不一致
-- 3. 幻读:同一事务内两次读取记录数不同(针对 INSERT/DELETE)
事务A: SELECT * FROM users WHERE age > 20; -- 3 条记录
事务B: INSERT INTO users(age) VALUES(25);
事务B: COMMIT;
事务A: SELECT * FROM users WHERE age > 20; -- 4 条记录
-- 事务A 多了一条"幻影记录"

MySQL 默认隔离级别:可重复读(REPEATABLE READ)


链式追问二:MVCC 多版本并发控制

Section titled “链式追问二:MVCC 多版本并发控制”

Q3:MVCC 是什么?如何实现可重复读?

Section titled “Q3:MVCC 是什么?如何实现可重复读?”
高频

MVCC 核心思想:为每个事务生成一个数据快照,不同事务看到不同版本的数据。

实现机制

每行数据包含三个隐藏字段:
┌────────────────────────────────────────────┐
│ id | name | age | DB_TRX_ID | DB_ROLL_PTR │
└────────────────────────────────────────────┘
↑ ↑
│ └─ 回滚指针(指向 Undo Log)
└─ 事务ID(最后修改该行的事务)
Undo Log 链:
当前数据 → Undo Log 1 → Undo Log 2 → Undo Log 3
(age=30) (age=25) (age=20) (age=18)
↑ ↑ ↑ ↑
TRX_ID=103 TRX_ID=102 TRX_ID=101 TRX_ID=100

Read View(读视图)

事务读取数据时,会生成一个 Read View,包含:

Read View {
m_ids: [101, 102, 103] // 活跃事务ID列表
min_trx_id: 101 // 最小活跃事务ID
max_trx_id: 104 // 下一个将分配的事务ID
creator_trx_id: 102 // 创建该 Read View 的事务ID
}

可见性判断规则

读取某行数据时,根据 DB_TRX_ID 判断是否可见:
1. DB_TRX_ID == creator_trx_id
→ 是自己修改的,可见 ✅
2. DB_TRX_ID < min_trx_id
→ 事务已提交,可见 ✅
3. DB_TRX_ID >= max_trx_id
→ 事务在 Read View 生成后才开启,不可见 ❌
4. min_trx_id <= DB_TRX_ID < max_trx_id
→ 检查 DB_TRX_ID 是否在 m_ids 中
- 在 m_ids 中:事务未提交,不可见 ❌
- 不在 m_ids 中:事务已提交,可见 ✅
5. 不可见时 → 沿 Undo Log 链查找可见版本

RC 和 RR 的区别

隔离级别Read View 生成时机效果
读已提交(RC)每次 SELECT 都生成新的 Read View可以看到其他事务已提交的修改
可重复读(RR)事务第一次 SELECT 时生成 Read View整个事务期间看到的数据一致

高频

答案:MVCC 只能解决”快照读”的幻读,无法解决”当前读”的幻读。

快照读 vs 当前读

-- 快照读:基于 MVCC,读取历史版本
SELECT * FROM users WHERE age > 20;
-- 当前读:读取最新版本,加锁
SELECT * FROM users WHERE age > 20 FOR UPDATE; -- 加 Next-Key Lock
SELECT * FROM users WHERE age > 20 LOCK IN SHARE MODE; -- 加共享锁
INSERT/UPDATE/DELETE -- 也是当前读

幻读场景

-- 事务A(RR 级别)
SELECT * FROM users WHERE age > 20; -- 快照读,3 条记录
-- 事务B
INSERT INTO users(age) VALUES(25);
COMMIT;
-- 事务A
SELECT * FROM users WHERE age > 20; -- 快照读,仍是 3 条(MVCC 解决了幻读)
UPDATE users SET name = 'test' WHERE age = 25; -- 当前读,更新成功!
SELECT * FROM users WHERE age > 20; -- 快照读,变成 4 条(幻读出现)

解决方案:Next-Key Lock(间隙锁)

事务A 执行:
SELECT * FROM users WHERE age > 20 FOR UPDATE;
加锁范围:
(age=20, +∞) 的所有间隙都被锁定
事务B 尝试插入 age=25:
→ 被阻塞,等待锁释放

本质一句话:MVCC 解决快照读的幻读,Next-Key Lock 解决当前读的幻读。


必考

锁的分类

按锁粒度分类:
├── 全局锁(FTWRL)
│ └─ 锁整个实例,用于全库逻辑备份
├── 表级锁
│ ├─ 表锁(LOCK TABLES)
│ ├─ 元数据锁(MDL)
│ └─ 意向锁(IS/IX)
└── 行级锁(InnoDB)
├─ 记录锁(Record Lock)
├─ 间隙锁(Gap Lock)
└─ 临键锁(Next-Key Lock)
按锁类型分类:
├── 共享锁(S Lock)
│ └─ 读锁,多个事务可同时持有
└── 排他锁(X Lock)
└─ 写锁,独占锁

行级锁详解

锁类型锁定范围作用
记录锁单行记录防止其他事务修改/删除该行
间隙锁两个记录之间的间隙防止其他事务插入新记录
临键锁记录 + 间隙同时防止修改和插入

加锁示例

-- 假设 users 表有 id: 1, 5, 10
-- 记录锁
UPDATE users SET age = 30 WHERE id = 5;
-- 只锁 id=5 这一行
-- 间隙锁
UPDATE users SET age = 30 WHERE id > 5 AND id < 10;
-- 锁 (5, 10) 这个间隙,防止插入 id=6,7,8,9
-- 临键锁(默认)
SELECT * FROM users WHERE id > 5 FOR UPDATE;
-- 锁 (5, 10] + (10, +∞)
-- 防止插入 id=6,7,8,9 和修改 id=10

Q6:什么情况下会产生死锁?如何避免?

Section titled “Q6:什么情况下会产生死锁?如何避免?”
高频

死锁场景

-- 事务A
UPDATE users SET age = 30 WHERE id = 1; -- 持有 id=1 的锁
-- 等待 id=2 的锁
-- 事务B
UPDATE users SET age = 25 WHERE id = 2; -- 持有 id=2 的锁
UPDATE users SET age = 25 WHERE id = 1; -- 等待 id=1 的锁
-- 死锁产生:A 等 B,B 等 A

死锁检测

InnoDB 死锁检测机制:
1. 等待图(wait-for graph)检测循环依赖
2. 发现死锁 → 选择代价最小的事务回滚
3. 返回错误:Deadlock found when trying to get lock

避免死锁的方法

  1. 按固定顺序访问表和行

    -- ❌ 不同事务按不同顺序访问
    事务A: UPDATE table1 → UPDATE table2
    事务B: UPDATE table2 → UPDATE table1
    -- ✅ 统一顺序
    事务A: UPDATE table1 → UPDATE table2
    事务B: UPDATE table1 → UPDATE table2
  2. 大事务拆小事务

    -- ❌ 大事务持有锁时间长
    BEGIN;
    -- 大量操作
    COMMIT;
    -- ✅ 小事务快速提交
    BEGIN;
    -- 少量操作
    COMMIT;
  3. 降低隔离级别

    • RC 级别没有间隙锁,死锁概率更低
  4. 添加合理索引

    • 避免锁升级(行锁 → 表锁)

死锁排查

-- 查看最近的死锁信息
SHOW ENGINE INNODB STATUS;
-- 查看当前锁等待
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

Q7:乐观锁和悲观锁有什么区别?

Section titled “Q7:乐观锁和悲观锁有什么区别?”
加分

对比

维度悲观锁乐观锁
思想假设冲突概率高,先加锁假设冲突概率低,提交时检查
实现数据库锁(SELECT FOR UPDATE)版本号/CAS
性能并发度低,锁开销大并发度高,无锁开销
适用场景写多读少、冲突概率高读多写少、冲突概率低

乐观锁实现

-- 方式1:版本号
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 10;
-- 如果 affected_rows = 0,说明版本号已变化,重试
-- 方式2:时间戳
UPDATE products
SET stock = stock - 1, update_time = NOW()
WHERE id = 1 AND update_time = '2024-01-01 10:00:00';

悲观锁实现

-- 先加锁
SELECT * FROM products WHERE id = 1 FOR UPDATE;
-- 修改
UPDATE products SET stock = stock - 1 WHERE id = 1;
-- 提交释放锁
COMMIT;

实战选择

场景1:秒杀抢购
→ 冲突概率极高 → 悲观锁(FOR UPDATE)
场景2:文章阅读数
→ 冲突概率低 → 乐观锁(版本号)
场景3:账户余额
→ 强一致性要求 → 悲观锁

事务 ACID 实现:
- 原子性 → Undo Log
- 一致性 → A + I + D 共同保证
- 隔离性 → 锁 + MVCC
- 持久性 → Redo Log
MVCC 核心:
- Undo Log 链(多版本数据)
- Read View(可见性判断)
- 快照读 vs 当前读
锁机制:
- 行锁:记录锁、间隙锁、临键锁
- 死锁:检测 + 回滚
- 乐观锁 vs 悲观锁:根据冲突概率选择