MySQL 事务与 MVCC 机制是什么?

2025年 阅读约 15 分钟 面试指南 · MySQL

深入解析MySQL事务与MVCC机制:ACID特性、四种隔离级别实现原理、undo log版本链、ReadView可见性判断、MVCC如何实现可重复读、当前读与快照读区别,附面试模拟问答。

一句话总结

MySQL InnoDB 通过 MVCC(多版本并发控制)实现非锁定读,核心依赖 undo log 版本链 + ReadView。每行数据有隐藏列 trx_id(最近修改的事务ID)和 roll_pointer(指向 undo log),ReadView 根据事务启动时的活跃事务列表判断版本可见性。MVCC 只在 RC(读已提交)和 RR(可重复读)隔离级别下生效,RR 级别下同一个事务始终使用同一个 ReadView。

初级理解

事务 ACID 特性

特性含义InnoDB 实现方式
A 原子性事务要么全做,要么全不做undo log(回滚日志)
C 一致性事务前后数据满足约束由 AID 共同保证
I 隔离性并发事务互不干扰MVCC + 锁
D 持久性提交后数据永久保存redo log(重做日志)

四种隔离级别与并发问题

隔离级别脏读不可重复读幻读
READ UNCOMMITTED❌ 可能❌ 可能❌ 可能
READ COMMITTED✅ 解决❌ 可能❌ 可能
REPEATABLE READ(默认)✅ 解决✅ 解决⚠️ 部分解决
SERIALIZABLE✅ 解决✅ 解决✅ 解决
# 查看当前隔离级别 SELECT @@transaction_isolation; # MySQL 8.0 SELECT @@tx_isolation; # MySQL 5.7 # 设置隔离级别 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
一句话总结:MVCC 是 InnoDB 实现非锁定读的核心机制,通过版本链 + ReadView 让读不阻塞写、写不阻塞读。

中级深入

undo log 版本链

每行数据有两个隐藏列:

DB_TRX_ID(6字节):最近修改这行数据的事务 ID

DB_ROLL_PTR(7字节):回滚指针,指向 undo log 中的旧版本

每次更新都会生成一条 undo log,roll_pointer 串联成版本链

# 版本链示意(事务 ID 递增) # 初始:trx_id=100, name='张三', roll_ptr → null # 事务200 UPDATE name='李四': # 当前行:trx_id=200, name='李四', roll_ptr → undo(trx_id=100, name='张三') # 事务300 UPDATE name='王五': # 当前行:trx_id=300, name='王五', roll_ptr → undo(trx_id=200, name='李四') # → undo(trx_id=100, name='张三') # 版本链:当前行 → undo200 → undo100 → null # 事务根据 ReadView 沿版本链找到可见版本

ReadView — 可见性判断的核心

ReadView 包含四个关键信息:

字段含义
m_ids生成 ReadView 时,当前系统中活跃的(未提交的)事务 ID 列表
min_trx_idm_ids 中的最小值
max_trx_id生成 ReadView 时,系统下一个要分配的事务 ID(即最大事务ID+1)
creator_trx_id生成该 ReadView 的事务 ID

判断版本链中某个版本(trx_id)是否可见的规则:

# ReadView 可见性判断规则 def is_visible(trx_id, readview): # 1. trx_id == creator_trx_id → 可见(自己修改的) if trx_id == readview.creator_trx_id: return True # 2. trx_id < min_trx_id → 可见(修改它的事务在 ReadView 创建前已提交) if trx_id < readview.min_trx_id: return True # 3. trx_id >= max_trx_id → 不可见(修改它的事务在 ReadView 创建后才开始) if trx_id >= readview.max_trx_id: return False # 4. min_trx_id <= trx_id < max_trx_id: # 如果 trx_id 在 m_ids 中 → 不可见(事务还未提交) # 如果 trx_id 不在 m_ids 中 → 可见(事务已提交) if trx_id in readview.m_ids: return False return True

RC vs RR — ReadView 生成时机不同

隔离级别ReadView 生成时机效果
READ COMMITTED每次 SELECT 都生成新的 ReadView能读到其他事务已提交的修改(不可重复读)
REPEATABLE READ事务第一次 SELECT 时生成,之后复用始终读到事务开始时的数据(可重复读)
# RR 级别下可重复读示例 # 事务A(RR级别) BEGIN; SELECT name FROM users WHERE id = 1; # 生成 ReadView,读到 '张三' # 此时事务B UPDATE users SET name='李四' WHERE id=1; COMMIT; SELECT name FROM users WHERE id = 1; # 复用 ReadView,仍读到 '张三' COMMIT; # RC 级别下不可重复读示例 # 事务A(RC级别) BEGIN; SELECT name FROM users WHERE id = 1; # 生成 ReadView1,读到 '张三' # 事务B UPDATE users SET name='李四' WHERE id=1; COMMIT; SELECT name FROM users WHERE id = 1; # 生成 ReadView2,读到 '李四' COMMIT;
中级要点:MVCC = undo log 版本链 + ReadView 可见性判断;RC 每次 SELECT 新 ReadView,RR 复用同一个 ReadView。

高级拓展

快照读 vs 当前读

类型SQL 语句读取方式
快照读普通 SELECT读 MVCC 版本链中的可见版本(不加锁)
当前读SELECT ... FOR UPDATE / LOCK IN SHARE MODE读最新版本 + 加锁
当前读UPDATE / DELETE / INSERT读最新版本 + 加锁
# 快照读(MVCC,不加锁) SELECT * FROM users WHERE id = 1; # 当前读(加锁,读最新版本) SELECT * FROM users WHERE id = 1 FOR UPDATE; # 加 X 锁 SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE; # 加 S 锁

RR 级别下幻读如何解决?

MVCC 的快照读解决了部分幻读(SELECT 不会读到其他事务插入的数据),但当前读仍可能幻读:

# RR 级别幻读场景 # 事务A BEGIN; SELECT * FROM users WHERE age > 20; # 快照读,3条 # 事务B INSERT INTO users VALUES(4, '王五', 25); COMMIT; SELECT * FROM users WHERE age > 20; # 快照读,还是3条(MVCC解决) SELECT * FROM users WHERE age > 20 FOR UPDATE; # 当前读,4条!(幻读) COMMIT; # InnoDB 通过 Next-Key Lock(行锁+间隙锁)解决当前读的幻读 # SELECT ... FOR UPDATE 会锁住 age>20 的所有行 + 间隙

undo log 的清理

undo log 不会一直保留,当没有事务需要访问旧版本时,由 purge 线程清理。长事务会导致 undo log 堆积,占用大量磁盘空间。

# 查看 undo log 相关状态 SHOW ENGINE INNODB STATUS\G # 查看长事务 SELECT * FROM information_schema.innodb_trx WHERE trx_started < DATE_SUB(NOW(), INTERVAL 60 SECOND); # 查看 undo 表空间大小 SELECT TABLESPACE_NAME, FILE_SIZE/1024/1024 AS size_mb FROM information_schema.FILES WHERE TABLESPACE_NAME LIKE 'undo%';

实战场景

场景:排查长事务导致 undo log 堆积

# 1. 查看当前活跃事务 SELECT trx_id, trx_state, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_sec, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx ORDER BY trx_started; # 2. 找到持续时间超过 60 秒的事务 SELECT * FROM information_schema.innodb_trx WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60; # 3. 找到对应的连接并 KILL SELECT CONCAT('KILL ', trx_mysql_thread_id, ';') FROM information_schema.innodb_trx WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60; # 4. 预防:设置事务超时 SET SESSION innodb_lock_wait_timeout = 10; # 锁等待超时 10 秒 SET SESSION lock_wait_timeout = 10; # 元数据锁等待超时

面试模拟

面试官:MVCC 是如何实现可重复读的?

你:核心是 ReadView 的生成时机。RR 级别下,事务第一次 SELECT 时生成一个 ReadView,之后所有 SELECT 都复用这个 ReadView。ReadView 记录了当时活跃的事务列表,沿 undo log 版本链判断每个版本的可见性:如果版本的事务ID在活跃列表中(未提交),就不可见,继续往前找。这样即使其他事务提交了修改,本事务也读不到,实现了可重复读。

面试官:RC 和 RR 在 MVCC 实现上有什么区别?

你:区别在于 ReadView 的生成时机。RC 每次 SELECT 都生成新的 ReadView,所以能读到其他事务已提交的修改(不可重复读)。RR 只在第一次 SELECT 时生成 ReadView,之后复用,所以始终读到事务开始时的数据(可重复读)。

面试官:MVCC 能完全解决幻读吗?

你:不能完全解决。MVCC 的快照读(普通 SELECT)不会幻读,但当前读(SELECT FOR UPDATE)仍可能幻读。InnoDB 通过 Next-Key Lock(行锁+间隙锁)来解决当前读的幻读问题。