MySQL 事务与 MVCC 详解 ⭐⭐⭐

面试题:解释 MySQL 中的 ACID 属性,并说明 MVCC 的实现原理

核心回答

MySQL InnoDB 通过 ACID 特性保证事务的可靠性,通过 MVCC(多版本并发控制)实现高效的并发读写。


一、ACID 特性

1. 原子性(Atomicity)

定义:事务是一个不可分割的工作单位,要么全部成功,要么全部失败回滚。

实现机制:undo log(回滚日志)

事务执行过程:
1. 开始事务
2. 执行操作(同时记录 undo log)
   - 插入 → 记录删除操作
   - 删除 → 记录插入操作
   - 更新 → 记录旧值
3. 提交事务 → 清空 undo log
4. 回滚事务 → 执行 undo log 恢复数据

2. 一致性(Consistency)

定义:事务执行前后,数据库必须从一个一致性状态变为另一个一致性状态。

保证方式

3. 隔离性(Isolation)

定义:多个事务并发执行时,一个事务的执行不应影响其他事务。

实现机制:锁 + MVCC

4. 持久性(Durability)

定义:事务一旦提交,对数据库的修改就是永久的,即使系统故障也不会丢失。

实现机制:redo log(重做日志)

数据写入流程:
1. 修改内存中的数据页(Buffer Pool)
2. 写入 redo log(WAL 机制,先写日志再写磁盘)
3. 事务提交时,redo log 强制刷盘(fsync)
4. 后台线程异步将数据页刷盘

故障恢复:
- 重启时检查 redo log,将未刷盘的数据恢复

二、事务隔离级别

并发问题

问题 描述 示例
脏读 读到其他事务未提交的数据 事务 A 修改未提交,事务 B 读到修改后的值
不可重复读 同一事务内多次读取,结果不同 事务 A 两次读取之间,事务 B 修改并提交
幻读 同一事务内多次查询,结果集不同 事务 A 两次查询之间,事务 B 插入新记录

四种隔离级别

-- 查看隔离级别
SELECT @@transaction_isolation;

-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
隔离级别 脏读 不可重复读 幻读 实现机制
READ UNCOMMITTED 不加锁,直接读最新数据
READ COMMITTED MVCC(每次查询生成新 ReadView)
REPEATABLE READ(默认) ✓(部分解决) MVCC(事务开始时生成 ReadView)
SERIALIZABLE 所有操作加排他锁

三、MVCC 多版本并发控制

核心思想

读写不阻塞:写操作生成新版本数据,读操作根据版本号读取历史版本。

数据存储结构(每行记录隐藏字段):

| DB_TRX_ID | DB_ROLL_PTR | 列1 | 列2 | ... |
|-----------|-------------|-----|-----|-----|

- DB_TRX_ID:最后修改该记录的事务 ID
- DB_ROLL_PTR:回滚指针,指向 undo log

undo log 版本链

事务 100:update age=20 → 事务 200:update age=30

当前记录:age=30, trx_id=200, roll_ptr → undo log
                                ↓
                         undo log:age=20, trx_id=100, roll_ptr → undo log
                                                ↓
                                          undo log:age=18, trx_id=null(初始值)

ReadView(读视图)

作用:判断当前事务可见哪个版本的数据。

结构

ReadView {
    m_ids: [100, 200]          // 生成 ReadView 时活跃的事务 ID 列表
    min_trx_id: 100            // 最小活跃事务 ID
    max_trx_id: 201            // 下一个分配的事务 ID(最大事务 ID + 1)
    creator_trx_id: 50         // 创建该 ReadView 的事务 ID
}

可见性判断规则

对于某条记录的 trx_id:

  1. trx_id == creator_trx_id:当前事务修改的,可见
  2. trx_id < min_trx_id:在 ReadView 生成前已提交,可见
  3. trx_id >= max_trx_id:在 ReadView 生成后启动,不可见
  4. min_trx_id <= trx_id < max_trx_id
    • 如果 trx_id 在 m_ids 中:未提交,不可见
    • 如果 trx_id 不在 m_ids 中:已提交,可见

不同隔离级别的 MVCC 实现

READ COMMITTED(读已提交)

特点:每次查询都生成新的 ReadView

事务 A(trx_id=100):
    SELECT * FROM user WHERE id=1;  -- 生成 ReadView1,看到已提交数据
    
    -- 事务 B 修改并提交
    
    SELECT * FROM user WHERE id=1;  -- 生成 ReadView2,看到 B 修改后的数据(不可重复读)

REPEATABLE READ(可重复读)

特点:事务开始时生成 ReadView,整个事务期间复用

事务 A(trx_id=100):
    START TRANSACTION;  -- 生成 ReadView
    
    SELECT * FROM user WHERE id=1;  -- 使用 ReadView,看到数据版本 V1
    
    -- 事务 B 修改并提交
    
    SELECT * FROM user WHERE id=1;  -- 仍使用 ReadView,看到数据版本 V1(可重复读)

MVCC 解决幻读?

快照读(普通 SELECT):MVCC 解决幻读

-- 事务 A
START TRANSACTION;
SELECT * FROM user WHERE age > 18;  -- 结果:3 条记录(生成 ReadView)

-- 事务 B 插入一条 age=20 的记录并提交

SELECT * FROM user WHERE age > 18;  -- 结果:仍是 3 条(使用相同 ReadView)

当前读(SELECT FOR UPDATE):MVCC 不能解决幻读,需要 Gap Lock

-- 事务 A
START TRANSACTION;
SELECT * FROM user WHERE age > 18 FOR UPDATE;  -- 加间隙锁

-- 事务 B 插入 age=20 的记录 → 阻塞等待

SELECT * FROM user WHERE age > 18 FOR UPDATE;  -- 结果:4 条记录

四、锁机制

锁类型

按粒度分

锁类型 描述 使用场景
行锁 锁定单行记录 InnoDB 默认,高并发
表锁 锁定整个表 MyISAM 默认,DDL 操作
页锁 锁定数据页 BDB 存储引擎

按功能分

锁类型 描述 兼容性
共享锁(S 锁) 读锁,允许多个事务同时读 与 S 锁兼容,与 X 锁冲突
排他锁(X 锁) 写锁,禁止其他事务读写 与 S 锁、X 锁都冲突

InnoDB 行锁算法

锁类型 描述 示例
Record Lock 锁定单个记录 WHERE id = 1
Gap Lock 锁定索引间隙,防止幻读 WHERE id > 5 AND id < 10
Next-Key Lock Record Lock + Gap Lock 默认行锁算法

锁兼容性矩阵

  S 锁 X 锁
S 锁 兼容 冲突
X 锁 冲突 冲突

五、高频面试题

Q1: ACID 的实现原理?

特性 实现机制
原子性 undo log
一致性 原子性 + 隔离性 + 持久性 + 约束
隔离性 锁 + MVCC
持久性 redo log

Q2: MVCC 解决了什么问题?

  1. 读写不阻塞:读操作不需要加锁,提高并发性能
  2. 实现非阻塞读:SELECT 操作不会被写操作阻塞
  3. 实现可重复读:通过 ReadView 保证同一事务内多次读取结果一致

Q3: 为什么 REPEATABLE READ 下还会幻读?

Q4: redo log 和 undo log 的区别?

特性 redo log undo log
作用 保证持久性,故障恢复 保证原子性,事务回滚
内容 物理日志,记录数据页修改 逻辑日志,记录反向操作
写入时机 事务执行过程中持续写入 事务执行过程中持续写入
清理时机 数据页刷盘后清理 事务提交后,无其他事务引用时清理

Q5: 如何查看当前事务的锁情况?

-- 查看锁等待
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

-- 查看当前锁
SELECT * FROM information_schema.INNODB_LOCKS;

-- 查看事务状态
SELECT * FROM information_schema.INNODB_TRX;

参考链接: