【MySQL】通俗易懂的 MVCC 与事务

目录

1.MVCC

用一个图书馆的故事来比喻

总结一下核心思想

[MySQL 是如何实现这个魔术的?](#MySQL 是如何实现这个魔术的?)

两种读方式

[MVCC 的好处](#MVCC 的好处)

一点小代价

2.事务

银行转账的故事

[MySQL 实现事务的四大法宝](#MySQL 实现事务的四大法宝)

[法宝一:Redo Log(重做日志) - 保证 持久性 (Durability)](#法宝一:Redo Log(重做日志) - 保证 持久性 (Durability))

[法宝二:Undo Log(回滚日志) - 保证 原子性 (Atomicity)](#法宝二:Undo Log(回滚日志) - 保证 原子性 (Atomicity))

[法宝三:锁(Locking) + MVCC - 保证 隔离性 (Isolation)](#法宝三:锁(Locking) + MVCC - 保证 隔离性 (Isolation))

[法宝四:事务的总体管理 - 保证 一致性 (Consistency)](#法宝四:事务的总体管理 - 保证 一致性 (Consistency))

总结一下整个过程

3.MVCC与事务

[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 之后(智能管理):

图书管理员非常聪明,他这样做:

  1. 保存多个版本 :当 B 事务想要修改某本书(比如《MySQL入门》)时,管理员不会直接涂改原书。而是先复印一份原版 ,让 B 在复印件上修改。同时,原书还好好地留在书架上,让其他来读书的人(A事务)继续读。

    • 原来的书 = 旧数据版本

    • 修改后的复印件 = 新数据版本

  2. 给每个人看合适的版本

    • 对于早就在读的 A 事务,管理员始终给他看他刚进来时的书架状态(旧版本)。所以他读到的永远是那本没修改过的《MySQL入门》,即使后面的B事务已经修改了内容。这保证了A读到的数据始终一致。

    • 对于新来的 C 事务,管理员会给他看最新的书架状态,也就是B修改后的那本《MySQL入门》(新版本)。

  3. 清理废稿:当最早的那个A事务读完书离开图书馆后,管理员就知道已经没人需要看那本旧版的《MySQL入门》了,他就会把那份旧的"复印件"扔进垃圾桶回收空间。


总结一下核心思想

MVCC 就是通过保存数据的多个历史版本 ,让读操作写操作 能够同时进行而不会互相阻塞。

  • 读操作SELECT):去读取那个符合它时间点的"历史快照"版本,不需要等写操作释放锁。

  • 写操作UPDATE, DELETE):会创建数据的一个新版本,不会直接覆盖旧数据,所以不会影响正在读旧数据的事务。

MySQL 是如何实现这个魔术的?

在技术上,InnoDB 引擎通过以下两个核心设计来实现 MVCC:

  1. 隐藏的版本字段

    • 每行数据其实都有两个(或三个)隐藏字段。

    • DB_TRX_ID:最后一个修改这行数据的事务ID。

    • DB_ROLL_PTR:回滚指针,指向这行数据的上一个历史版本(就像指向上一个存档点)。

    • (还有一个DB_ROW_ID,暂时可以忽略)

  2. Undo Log(回滚日志)

    • 当更新数据时,旧版本的数据会被写入一个叫 Undo Log 的地方。

    • 那个DB_ROLL_PTR指针就把所有历史版本串成一条版本链。读请求的时候,就是顺着这个链找到适合它的那个历史版本。

  3. Read View(读视图)

    • 每个事务在执行快照读 (普通SELECT)时,都会生成一个"读视图"。

    • 这个"读视图"就像一个规则,决定了在这个事务里,它能看到版本链中的哪个版本(哪些版本对它是可见的,哪些不可见)。

两种读方式

  • 快照读 :普通的 SELECT ... 语句。它基于 MVCC,读取的是历史快照,不加锁,所以很快。

    • SELECT * FROM table WHERE ...;
  • 当前读:读取的是数据的最新版本,并且会加锁,保证其他事务不会并发修改这条记录。

    • SELECT ... FOR UPDATE / SELECT ... LOCK IN SHARE MODE

    • UPDATE ... / DELETE ... / INSERT ... (这些操作在执行前都会先"当前读"一下最新数据)

MVCC 的好处

  1. 大大提高了并发性能:读不阻塞写,写也不阻塞读。这是它最最大的优点。

  2. 保证了事务的隔离性 :比如默认的 REPEATABLE READ(可重复读)隔离级别,就是靠 MVCC 来实现的。一个事务内,多次读到的数据内容是一致的(都是同一个快照版本)。

一点小代价

  • 需要额外的存储空间:要存多个数据版本和 Undo Log。

  • 需要维护和清理:旧版本数据最终需要被清理掉(由 Purge 线程操作),否则会占用太多空间。

2.事务

好的,我们再用一个通俗易懂的比喻来讲讲 MySQL 是如何实现事务的。这次我们把它想象成一个 "银行转账操作"

银行转账的故事

假设你要从你的账户(A账户)转 100 元给朋友(B账户)。这个操作包含两步:

  1. 扣款:从你的 A 账户减去 100 元。

  2. 收款:往朋友的 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)会保存。这一切都是为了最终的数据一致性。


总结一下整个过程

还是以"银行转账"事务为例:

  1. 开始事务 (BEGIN):告诉 MySQL,接下来的一系列操作是一个整体。

  2. 记录 Undo Log:MySQL 悄悄记下 A 和 B 账户的原始余额,为你准备好"后悔药"。

  3. 执行修改

    • 给 A 账户的行加上行锁,防止别人修改。

    • 修改内存中的数据页(A账户-100)。

    • 将修改记录到 Redo Log BufferUndo Log 中。

    • 释放 A 账户的锁,给 B 账户加锁,然后重复类似过程(B账户+100)。

  4. 提交事务 (COMMIT):

    • Redo Log 正式写入磁盘文件(确保持久性)。

    • 将修改后的数据页异步写回磁盘数据文件(这时即使崩溃,也有 Redo Log 兜底)。

    • 释放所有锁。

    • 清理不再需要的 Undo Log。

  5. 如果失败/回滚 (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
};

可见性判断算法

  1. 如果 DB_TRX_ID < up_limit_id可见(事务在快照前已提交)

  2. 如果 DB_TRX_ID >= low_limit_id不可见(事务在快照后开始)

  3. 如果 DB_TRX_ID 在活跃事务列表中:不可见(事务未提交)

  4. 否则:可见(事务在快照时已提交)

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 UPDATEUPDATEDELETE → 使用锁,读取最新版本

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 等相关的知识,欢迎关注~

相关推荐
今天过得怎么样2 小时前
彻底搞懂 Spring Boot 中 properties 和 YAML 的区别
后端
qq_12498707532 小时前
基于springboot的幼儿园家校联动小程序的设计与实现(源码+论文+部署+安装)
java·spring boot·后端·spring·微信小程序·小程序
啦啦啦~~~7542 小时前
【最新版】Edge浏览器安装!绿色增强版+禁止Edge更新的软件+彻底卸载Edge软件
数据库·阿里云·电脑·.net·edge浏览器
程序边界2 小时前
金仓数据库助力Oracle迁移:一场国产数据库的逆袭之旅
数据库·oracle
为什么不问问神奇的海螺呢丶2 小时前
oracle RAC开关机步骤
数据库·oracle
后端小张2 小时前
【Java 进阶】深入理解Redis:从基础应用到进阶实践全解析
java·开发语言·数据库·spring boot·redis·spring·缓存
TDengine (老段)2 小时前
TDengine IDMP 1.0.9.0 上线:数据建模、分析运行与可视化能力更新一览
大数据·数据库·物联网·ai·时序数据库·tdengine·涛思数据
云老大TG:@yunlaoda3602 小时前
如何使用华为云国际站代理商的BRS进行数据安全保障?
大数据·数据库·华为云·云计算
工具人55552 小时前
strip()方法可以删除字符串中间空格吗
数据库·mysql