目录
[MySQL 是如何实现这个魔术的?](#MySQL 是如何实现这个魔术的?)
[MVCC 的好处](#MVCC 的好处)
[MySQL 实现事务的四大法宝](#MySQL 实现事务的四大法宝)
[法宝一:Redo Log(重做日志) - 保证 持久性 (Durability)](#法宝一:Redo Log(重做日志) - 保证 持久性 (Durability))
[法宝二:Undo Log(回滚日志) - 保证 原子性 (Atomicity)](#法宝二:Undo Log(回滚日志) - 保证 原子性 (Atomicity))
[法宝三:锁(Locking) + MVCC - 保证 隔离性 (Isolation)](#法宝三:锁(Locking) + MVCC - 保证 隔离性 (Isolation))
[法宝四:事务的总体管理 - 保证 一致性 (Consistency)](#法宝四:事务的总体管理 - 保证 一致性 (Consistency))
[MVCC 与事务的紧密关系](#MVCC 与事务的紧密关系)
[不同隔离级别下的 MVCC 表现](#不同隔离级别下的 MVCC 表现)
[1. 读未提交 (Read Uncommitted)](#1. 读未提交 (Read Uncommitted))
[2. 读已提交 (Read Committed)](#2. 读已提交 (Read Committed))
[3. 可重复读 (Repeatable Read) - MySQL 默认](#3. 可重复读 (Repeatable Read) - MySQL 默认)
[4. 串行化 (Serializable)](#4. 串行化 (Serializable))
[MVCC 在事务中的完整工作流程](#MVCC 在事务中的完整工作流程)
[MVCC 的核心组件深度解析](#MVCC 的核心组件深度解析)
[1. Read View(读视图)的详细结构](#1. Read View(读视图)的详细结构)
[2. 版本链的遍历过程](#2. 版本链的遍历过程)
[MVCC 与锁的协同工作](#MVCC 与锁的协同工作)
[当前读 vs 快照读](#当前读 vs 快照读)
[MVCC 的维护成本](#MVCC 的维护成本)
[1. 合理设计事务](#1. 合理设计事务)
[2. 理解隔离级别的影响](#2. 理解隔离级别的影响)
[3. 监控 MVCC 相关指标](#3. 监控 MVCC 相关指标)
本篇由AI生成+个人修改
1.MVCC
好的,咱们用一个超简单的比喻来理解 MySQL 的 MVCC(多版本并发控制)。你可以把它想象成一个 "时间快照" 或者 "游戏存档" 技术。
用一个图书馆的故事来比喻
想象一个图书馆(这就是我们的数据库),里面有很多书(数据行)。很多人(事务)同时来借书、还书、读书。
没有 MVCC 的时候(老式管理):
如果有人正在读一本书(A事务在读),图书管理员就会把这本书锁起来,不让别人碰。这时候另一个人(B事务)想更新这本书的内容(比如修改错别字),他就必须等A读完才行。效率很低,大家经常要排队等待。
有了 MVCC 之后(智能管理):
图书管理员非常聪明,他这样做:
-
保存多个版本 :当 B 事务想要修改某本书(比如《MySQL入门》)时,管理员不会直接涂改原书。而是先复印一份原版 ,让 B 在复印件上修改。同时,原书还好好地留在书架上,让其他来读书的人(A事务)继续读。
-
原来的书 = 旧数据版本
-
修改后的复印件 = 新数据版本
-
-
给每个人看合适的版本:
-
对于早就在读的 A 事务,管理员始终给他看他刚进来时的书架状态(旧版本)。所以他读到的永远是那本没修改过的《MySQL入门》,即使后面的B事务已经修改了内容。这保证了A读到的数据始终一致。
-
对于新来的 C 事务,管理员会给他看最新的书架状态,也就是B修改后的那本《MySQL入门》(新版本)。
-
-
清理废稿:当最早的那个A事务读完书离开图书馆后,管理员就知道已经没人需要看那本旧版的《MySQL入门》了,他就会把那份旧的"复印件"扔进垃圾桶回收空间。
总结一下核心思想
MVCC 就是通过保存数据的多个历史版本 ,让读操作 和写操作 能够同时进行而不会互相阻塞。
-
读操作 (
SELECT):去读取那个符合它时间点的"历史快照"版本,不需要等写操作释放锁。 -
写操作 (
UPDATE, DELETE):会创建数据的一个新版本,不会直接覆盖旧数据,所以不会影响正在读旧数据的事务。
MySQL 是如何实现这个魔术的?
在技术上,InnoDB 引擎通过以下两个核心设计来实现 MVCC:
-
隐藏的版本字段:
-
每行数据其实都有两个(或三个)隐藏字段。
-
DB_TRX_ID:最后一个修改这行数据的事务ID。 -
DB_ROLL_PTR:回滚指针,指向这行数据的上一个历史版本(就像指向上一个存档点)。 -
(还有一个
DB_ROW_ID,暂时可以忽略)
-
-
Undo Log(回滚日志):
-
当更新数据时,旧版本的数据会被写入一个叫
Undo Log的地方。 -
那个
DB_ROLL_PTR指针就把所有历史版本串成一条版本链。读请求的时候,就是顺着这个链找到适合它的那个历史版本。
-
-
Read View(读视图):
-
每个事务在执行快照读 (普通
SELECT)时,都会生成一个"读视图"。 -
这个"读视图"就像一个规则,决定了在这个事务里,它能看到版本链中的哪个版本(哪些版本对它是可见的,哪些不可见)。
-
两种读方式
-
快照读 :普通的
SELECT ...语句。它基于 MVCC,读取的是历史快照,不加锁,所以很快。SELECT * FROM table WHERE ...;
-
当前读:读取的是数据的最新版本,并且会加锁,保证其他事务不会并发修改这条记录。
-
SELECT ... FOR UPDATE/SELECT ... LOCK IN SHARE MODE -
UPDATE .../DELETE .../INSERT ...(这些操作在执行前都会先"当前读"一下最新数据)
-
MVCC 的好处
-
大大提高了并发性能:读不阻塞写,写也不阻塞读。这是它最最大的优点。
-
保证了事务的隔离性 :比如默认的
REPEATABLE READ(可重复读)隔离级别,就是靠 MVCC 来实现的。一个事务内,多次读到的数据内容是一致的(都是同一个快照版本)。
一点小代价
-
需要额外的存储空间:要存多个数据版本和 Undo Log。
-
需要维护和清理:旧版本数据最终需要被清理掉(由 Purge 线程操作),否则会占用太多空间。
2.事务
好的,我们再用一个通俗易懂的比喻来讲讲 MySQL 是如何实现事务的。这次我们把它想象成一个 "银行转账操作"。
银行转账的故事
假设你要从你的账户(A账户)转 100 元给朋友(B账户)。这个操作包含两步:
-
扣款:从你的 A 账户减去 100 元。
-
收款:往朋友的 B 账户加上 100 元。
这两个步骤必须作为一个整体 来完成。要么全部成功 ,钱转过去了;要么全部失败,钱还原,就像什么都没发生过。绝不能发生"你账户扣了钱,但朋友没收到"这种中间状态。
MySQL 的事务就是为了保证这种"要么全做,要么全不做"的机制。它通过四大特性(ACID)来实现,下面我们看看 MySQL 具体是怎么做的。
MySQL 实现事务的四大法宝
为了实现"银行转账"这种安全操作,MySQL(主要说 InnoDB 引擎)拿出了四个核心法宝:日志 和 锁。
法宝一:Redo Log(重做日志) - 保证 持久性 (Durability)
-
比喻 :就像记账本。银行柜员办理业务时,不会直接去金库里搬钱,那样太慢了。他会先把"A账户-100,B账户+100"这个操作记录在记账本上。只要记账本写成功了,即使银行突然停电,恢复电力后,银行经理也能根据记账本上的记录,把最终结果更新到金库里。
-
技术实现:
-
redo log是物理日志,记录的是"在哪个数据页上做了什么修改"。 -
当事务提交时,首先会把所有修改优先写入
redo log文件(这个操作很快),然后再慢慢写回磁盘上的数据文件。 -
即使系统崩溃,重启后 MySQL 也能读取
redo log,将尚未写入数据文件的更改重新执行一遍(重做),从而保证已经提交的事务不会丢失。
-
法宝二:Undo Log(回滚日志) - 保证 原子性 (Atomicity)
-
比喻 :就像给你一个"后悔药" 。在做转账操作前,银行系统会先偷偷记录下 A 和 B 账户原来的余额(比如 A:1000, B:500)。如果转账过程中出了任何问题(比如系统错误,或者你反悔了),系统就可以根据这个记录,把数据回滚到操作前的状态(A:1000, B:500)。
-
技术实现:
-
undo log是逻辑日志,记录的是修改前的数据状态(比如每个字段的旧值)。 -
如果事务执行失败或主动发出
ROLLBACK命令,MySQL 就会利用undo log将数据恢复到事务开始前的模样。 -
它也是实现我们之前讲的 MVCC 的关键,用于构建数据的历史版本。
-
法宝三:锁(Locking) + MVCC - 保证 隔离性 (Isolation)
-
比喻 :保证多个转账操作不会互相干扰。
-
锁 :就像一把锁。当你正在修改 A 账户时,可以暂时给 A 账户加锁,防止别人同时修改它,避免出现数据混乱。
-
MVCC :就像多版本快照。别人在你转账期间来查询 A 账户的余额,看到的还是你修改之前的金额(旧版本),不会看到你减了 100 块但还没提交的"中间状态",避免了脏读。
-
-
技术实现:
-
写操作 (如
UPDATE)会使用行锁,锁定正在修改的那一行数据,防止其他事务同时修改它。 -
读操作 (如
SELECT)通过 MVCC 机制读取数据的快照版本,避免了加锁等待,提高了并发性能。 -
不同的隔离级别(如读未提交、读已提交、可重复读)是通过不同的锁策略 和 MVCC 读视图的生成时机来实现的。
-
法宝四:事务的总体管理 - 保证 一致性 (Consistency)
-
比喻 :这是最终结果,是前三个法宝共同追求的目标。就像银行系统要保证转账前后,所有账户的总金额不变。A-100 + B+100 = 0,总金额没变。
-
技术实现:
-
一致性其实是由原子性、隔离性、持久性共同来保障的。
-
数据库通过预定义的各种约束 (主键、外键、唯一索引、数据类型等)和业务逻辑,在事务执行前后都确保数据处于一致的状态。
-
如果事务中途失败,原子性(Undo Log)会回滚;如果并发访问,隔离性(锁和MVCC)会隔离;如果提交成功,持久性(Redo Log)会保存。这一切都是为了最终的数据一致性。
-
总结一下整个过程
还是以"银行转账"事务为例:
-
开始事务 (
BEGIN):告诉 MySQL,接下来的一系列操作是一个整体。 -
记录 Undo Log:MySQL 悄悄记下 A 和 B 账户的原始余额,为你准备好"后悔药"。
-
执行修改:
-
给 A 账户的行加上行锁,防止别人修改。
-
修改内存中的数据页(A账户-100)。
-
将修改记录到 Redo Log Buffer 和 Undo Log 中。
-
释放 A 账户的锁,给 B 账户加锁,然后重复类似过程(B账户+100)。
-
-
提交事务 (
COMMIT):-
将 Redo Log 正式写入磁盘文件(确保持久性)。
-
将修改后的数据页异步写回磁盘数据文件(这时即使崩溃,也有 Redo Log 兜底)。
-
释放所有锁。
-
清理不再需要的 Undo Log。
-
-
如果失败/回滚 (
ROLLBACK):-
直接使用之前记录的 Undo Log,将数据全部恢复原状。
-
释放所有锁。
-
所以,MySQL 的事务并不是一个魔法黑盒,而是通过 Redo Log、Undo Log、锁和 MVCC 这套精妙配合的机制,共同实现了 ACID 四大特性,保证了数据的安全与可靠。
3.MVCC与事务
好的,我们更深入地讲讲 MySQL 中 MVCC 与事务的关系,以及它们是如何协同工作的。
MVCC 与事务的紧密关系
MVCC 不是独立于事务的功能,而是实现事务隔离性的核心机制 。它们的关系就像汽车的发动机和变速箱------紧密配合,共同驱动汽车前进。
核心关系总结
-
MVCC 是手段 ,事务隔离性是目标
-
MVCC 主要解决 读-写并发 问题
-
锁机制主要解决 写-写并发 问题
-
两者配合共同实现事务的 ACID 特性
不同隔离级别下的 MVCC 表现
MySQL 通过调整 MVCC 的"规则",实现了不同的事务隔离级别:
1. 读未提交 (Read Uncommitted)
-
几乎不用 MVCC
-
直接读取数据的最新版本,包括其他事务未提交的修改
-
性能最好,但会出现脏读
2. 读已提交 (Read Committed)
-
每次 SELECT 都创建新的 Read View
-
只能看到其他事务已经提交的修改
-
解决了脏读,但可能出现不可重复读
-
示例:
sql
-- 事务A
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 看到 1000
-- 此时事务B更新并提交
-- UPDATE accounts SET balance = 900 WHERE id = 1; COMMIT;
SELECT balance FROM accounts WHERE id = 1; -- 看到 900(前后不一致)
3. 可重复读 (Repeatable Read) - MySQL 默认
-
只在第一次 SELECT 时创建 Read View
-
整个事务期间都使用同一个"快照"
-
解决了不可重复读
-
示例:
sql
-- 事务A
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 看到 1000
-- 此时事务B更新并提交
-- UPDATE accounts SET balance = 900 WHERE id = 1; COMMIT;
SELECT balance FROM accounts WHERE id = 1; -- 仍然看到 1000(保持一致)
4. 串行化 (Serializable)
-
基本不用 MVCC
-
通过加锁让事务串行执行
-
隔离性最强,但并发性能最差
MVCC 在事务中的完整工作流程
让我们通过一个完整的事务例子,看看 MVCC 如何参与其中:
场景:银行转账事务
sql
-- 事务A (事务ID=100)
BEGIN; -- 开始事务
-- 此时生成 Read View
-- 假设当前活跃事务: [90, 95](其他正在运行的事务)
-- 最小事务ID: 90, 最大事务ID: 101
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 1. 将原数据(balance=1000)写入Undo Log
-- 2. 更新内存中的数据页(balance=900)
-- 3. 设置 DB_TRX_ID = 100, DB_ROLL_PTR 指向Undo Log中的旧版本
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 类似操作...
-- 此时事务B (事务ID=105) 执行查询
SELECT balance FROM accounts WHERE id = 1;
-- MVCC 可见性判断:
-- 当前行 DB_TRX_ID=100 (等于当前活跃事务100? 不,事务B看不到事务A未提交的修改)
-- 因为100在活跃事务列表[90,95]之外,但事务A未提交,所以不可见
-- 通过DB_ROLL_PTR找到旧版本:balance=1000,返回这个值
COMMIT; -- 提交事务
-- 1. 写入Redo Log确保持久性
-- 2. 清理Read View
-- 3. 后续的Purge线程会清理不再需要的旧版本
MVCC 的核心组件深度解析
1. Read View(读视图)的详细结构
Read View 决定了事务能看到哪些数据版本,包含:
cpp
struct read_view_t {
trx_id_t low_limit_id; // 高水位线,大于等于此值的事务不可见
trx_id_t up_limit_id; // 低水位线,小于此值的事务可见
ulint n_trx_ids; // 活跃事务数量
trx_id_t* trx_ids; // 活跃事务ID列表
trx_id_t creator_trx_id; // 创建此Read View的事务ID
};
可见性判断算法:
-
如果
DB_TRX_ID < up_limit_id:可见(事务在快照前已提交) -
如果
DB_TRX_ID >= low_limit_id:不可见(事务在快照后开始) -
如果
DB_TRX_ID在活跃事务列表中:不可见(事务未提交) -
否则:可见(事务在快照时已提交)
2. 版本链的遍历过程
bash
版本链:当前版本 ← 版本1 ← 版本2 ← 版本3
(trx_id=200) (trx_id=150) (trx_id=100)
事务A(Read View: 活跃事务=[180], low_limit=201, up_limit=170)查询:
1. 检查当前版本 trx_id=200
- 200 >= 201? 否
- 200 在[180]中? 否
- 200 > up_limit=170? 是 → 不可见,继续找旧版本
2. 检查版本1 trx_id=150
- 150 < up_limit=170? 是 → 可见!返回此版本
MVCC 与锁的协同工作
MVCC 不是万能的,它需要与锁机制配合:
写操作仍然需要加锁
sql
-- 事务A
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 即使有MVCC,这个UPDATE仍然需要:
-- 1. 给id=1的行加排他锁(X锁)
-- 2. 防止其他事务同时修改同一行
-- 3. 执行"当前读",读取最新提交的版本
当前读 vs 快照读
-
快照读 :
SELECT ...→ 使用 MVCC,读取历史版本 -
当前读 :
SELECT ... FOR UPDATE、UPDATE、DELETE→ 使用锁,读取最新版本
sql
-- 事务A
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 快照读:看到1000
-- 事务B
UPDATE accounts SET balance = 900 WHERE id = 1; COMMIT;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE; -- 当前读:看到900
-- 这个SELECT FOR UPDATE会加锁并读取最新提交的数据
MVCC 的维护成本
版本链管理
-
每次 UPDATE 都会创建新版本
-
版本链过长会影响读取性能
-
Purge 线程负责清理不再需要的旧版本
空间开销
-
Undo Log 增长
-
历史版本占用额外空间
-
需要合理配置
innodb_max_purge_lag等参数
实际应用建议
1. 合理设计事务
sql
-- 不好的做法:长事务
BEGIN;
-- 复杂的业务逻辑...
-- 大量计算...
-- 网络请求...
COMMIT; -- 长时间不提交,版本链会很长
-- 好的做法:短事务
BEGIN;
UPDATE ... -- 快速完成
COMMIT;
2. 理解隔离级别的影响
sql
-- 如果需要实时数据,使用当前读
SELECT * FROM table FOR UPDATE;
-- 或者降低隔离级别到 READ COMMITTED
-- 如果需要一致性视图,保持默认的 REPEATABLE READ
3. 监控 MVCC 相关指标
sql
-- 查看长事务
SELECT * FROM information_schema.innodb_trx
ORDER BY trx_started DESC;
-- 查看锁信息
SHOW ENGINE INNODB STATUS;
总结
MVCC 与事务的关系可以概括为:
-
MVCC 是事务隔离性的"实现引擎"
-
通过多版本+读视图的巧妙设计,实现了读不阻塞写
-
不同隔离级别本质上是调整MVCC的可见性规则
-
MVCC处理读-写冲突,锁处理写-写冲突
-
两者协同工作,共同保障了数据库的并发性能和一致性
这种精妙的设计让 MySQL 能够在高并发环境下,既保证数据的一致性,又提供良好的性能,是现代数据库系统的核心技术之一。
本篇文章到此结束,如果对你有帮助可以点个赞吗~
个人主页有很多个人总结的 Java、MySQL、AI 等相关的知识,欢迎关注~