硬核图解:MySQL 是如何利用 MVCC + 锁实现“可重复读”的?

硬核图解:MySQL 是如何利用 MVCC + 锁实现"可重复读"的?

在数据库面试或架构设计中,我们经常会被问到一个经典问题:"MySQL 的 RR(Repeatable Read)隔离级别是如何解决不可重复读的?"

很多同学脱口而出:"MVCC!"

这个答案对,但也不全对。MVCC(多版本并发控制)确实是核心,但它解决的是读写并发 时的视图一致性问题。而在真实的业务场景中,数据库必须同时处理并发读并发写 。仅仅依靠 MVCC 是不够的,必须配合锁机制才能构建起完整的隔离性防线。

今天,我们就通过一个真实的"银行转账 + 财务审计"场景,像剥洋葱一样拆解 MySQL 底层是如何通过 MVCC 和锁的协同,来保证数据绝对安全的。

一、 案发现场:转账与审计的并发博弈

为了还原真实场景,我们构建一个简单的账户表 accounts,包含 ID、姓名和余额。

初始数据如下(总余额 3500.00):

id name balance
1 Alice 1000.00
2 Bob 500.00
3 Charlie 2000.00

现在,系统中有两个并发事务正在执行:

  • 事务 T1 (TRX_ID=100):转账业务。Alice 向 Bob 转账 100 元。
  • 事务 T2 (TRX_ID=200):审计业务。查询所有账户的总余额。

我们的目标是观察:在 T1 修改数据但未提交、以及提交后的各个阶段,T2 到底看到了什么?

二、 时序拆解:上帝视角看数据流转

为了讲清楚 MVCC 的工作原理,我们将时间轴拉长,一步步看 MySQL 内部发生了什么。

T1 & T2 事务开启

sql 复制代码
-- 事务 T1 (ID=100)
START TRANSACTION;

-- 事务 T2 (ID=200)
START TRANSACTION;

此时,两个事务都领取到了自己的事务 ID。数据库中的数据尚未变动。

T2 第一次查询:快照生成(Read View)

事务 T2 发起第一次总额查询:

sql 复制代码
SELECT SUM(balance) FROM accounts;

底层发生了什么? 在 RR 隔离级别下,这是 T2 的第一条快照读(Snapshot Read)。MySQL 会在此时此刻为 T2 生成一个 Read View(读视图)。这个视图包含了当前系统中所有"活跃"(未提交)的事务 ID。

  • T2 的 Read View 核心参数
    • m_ids: [100, 200] (当前活跃的事务是 T1 和 T2)
    • min_trx_id: 100 (最小活跃 ID)
    • max_trx_id: 201 (下一个将要分配的 ID)

结果 :T2 看到的数据是初始状态,总额 3500.00

T3 事务 T1 执行转账:版本链诞生

事务 T1 开始干活了,执行转账 SQL:

sql 复制代码
UPDATE accounts SET balance = 900.00 WHERE id = 1;  -- Alice -100
UPDATE accounts SET balance = 600.00 WHERE id = 2;  -- Bob +100

底层发生了什么? 这里涉及到了 MVCC 的核心------Undo Log 版本链。 MySQL 不会直接覆盖旧数据,而是生成新记录,并用回滚指针(Roll_Ptr)指向旧记录。

此时 id=1 (Alice) 的行结构变成了这样:

  1. 最新版本 :Balance=900, DB_TRX_ID=100 (T1的ID)
  2. Undo Log 旧版本 :Balance=1000, DB_TRX_ID=50 (上次修改的事务ID)

同时,T1 会对 id=1 和 id=2 这两行数据加上排他锁(X锁),防止其他事务(如 T3)同时修改。

T4 事务 T2 第二次查询:复用快照

在 T1 修改完但未提交时,T2 再次查询总额:

sql 复制代码
SELECT SUM(balance) FROM accounts;

结果预测 : 如果发生脏读,T2 会算成 3500(900+600+2000)。但在 RR 级别下,结果依然是 3500.00(1000+500+2000)。

为什么?可见性判断算法生效 T2 复用 了 T2 时刻生成的 Read View。当扫描到 Alice 的行(最新 balance=900)时,MySQL 拿着这行的事务 ID (TRX_ID=100) 去问 Read View:

"事务 100 修改的数据,我能看吗?"

Read View 答:

"100 在我的活跃列表 [100, 200] 里。这意味着我创建快照时,它还没提交呢。不可见!"

于是,MySQL 顺着 Undo Log 链条往下找,找到了旧版本(balance=1000, TRX_ID=50)。TRX_ID=50 小于最小活跃 ID,说明早就提交了,可见

同理,Bob 的账户也读取了旧版本。

T5 & T6 T1 提交与 T2 第三次查询

sql 复制代码
-- T1 提交
COMMIT; 

-- T2 第三次查询
SELECT SUM(balance) FROM accounts;

最关键的时刻来了:T1 已经提交了,数据在物理上已经变成了 900 和 600。T2 此时再查,能看到变化吗?

结果 :依然是 3500.00(1000+500+2000)。

这就是 "可重复读"(Repeatable Read) 的含义。 在 RR 级别下,事务只在第一次 SELECT 时生成 Read View,后续所有的查询都复用这个 Read View。 虽然 T1 提交了,但在 T2 的 Read View 眼里,T1 依然属于"创建快照时还未提交的家伙",所以它的修改依然不可见。

三、 技术解密:MVCC 与锁的完美协同

通过上面的流程,我们可以总结出 MySQL 处理并发的核心逻辑:读写分离

这里的"读写分离"不是指主从架构,而是指读操作和写操作使用不同的并发控制机制

1. MVCC:守护"一致性读"

T2 在整个过程中,无论 T1 怎么折腾,无论 T1 是否提交,T2 就像在一个独立的平行宇宙中,看到的数据始终一致。

  • 机制:Read View + Undo Log。
  • 防御:脏读(Dirty Read)、不可重复读(Non-repeatable Read)。

2. 锁:守护"并发写"

在 T1 更新数据的同时,如果有一个事务 T3 也想修改 Alice 的余额:

sql 复制代码
UPDATE accounts SET balance = 800 WHERE id = 1;

T3 会被阻塞。因为 T1 持有了行级锁(Record Lock)。只有 T1 提交释放锁后,T3 才能执行。

  • 机制:行锁(Row Lock)、间隙锁(Gap Lock)。
  • 防御:更新丢失(Lost Update)、写冲突。

3. 为什么是最佳搭档?

如果不使用这套组合拳,我们会陷入两个极端:

  • 没有 MVCC:T2 为了读取数据一致性,必须加读锁(S锁)。这会导致 T1 想转账时被阻塞,读写串行化,性能极差。
  • 没有锁:T1 和 T3 同时修改,发生"更新丢失",钱算错了,这是金融系统的灾难。

MVCC 让读不阻塞写,锁让写不覆盖写。

四、 总结

我们常说的"MySQL 隔离性",在底层其实是由两套机制共同支撑的:

  1. Read View 可见性规则

    python 复制代码
    if trx_id == creator_id:
        return "可见 (自己改的)"
    if trx_id < min_trx_id:
        return "可见 (早就是历史了)"
    if trx_id in active_ids:
        return "不可见 (还在跑着呢)"
  2. 锁机制:确保同一时刻只有一个事务能修改同一行数据,维护数据的物理完整性。

下次再遇到"转账并发"的问题,不要只盯着代码层面的 synchronized 或分布式锁,别忘了数据库底层已经为你筑起了最坚固的防线。

思考题:如果将隔离级别改为 RC(读已提交),T6 时刻 T2 查询的结果会变成多少?为什么?欢迎在评论区讨论。

相关推荐
DarkAthena7 小时前
【DuckDB】探索函数调用新范式:点操作符链式调用
数据库·sql·duckdb
自己的九又四分之三站台7 小时前
PG GraphQL详细介绍与基本使用
数据库·sql·graphql
大模型RAG和Agent技术实践7 小时前
SQL Agent从“黑盒“到“全透明“:基于LangGraph+Phoenix的可观测性实战指南
数据库·人工智能·sql·agent·langgraph
rchmin7 小时前
Redis BitMap介绍及使用场景示例
数据库·redis·缓存
Dxy12393102167 小时前
MySQL 日志全解析
数据库·mysql
思成Codes7 小时前
MySQL——最左前缀法则
数据库·mysql
是店小二呀7 小时前
【MySQL】MySQL 从安装到理解
android·mysql·adb
杨云龙UP7 小时前
Windows环境下安装SQL Server 2016企业版+SP3补丁+SSMS连接操作手册_20251230
运维·服务器·数据库·sql·算法·sqlserver·哈希算法
雪碧聊技术7 小时前
基于Redis的分布式锁
数据库·redis·分布式