当前位置: 首页 > news >正文

MySQL学习之MVCC多版本并发控制

        在数据库并发场景中,如何平衡一致性与性能始终是核心难题。当多个事务同时读写数据时,可能出现脏读、不可重复读、幻读等问题。MySQL 的 InnoDB 存储引擎通过 MVCC(多版本并发控制)机制,在不加锁的情况下巧妙解决了这些问题,成为其高性能并发的关键。

        MVCC(Multi-Version Concurrency Control,多版本并发控制)是 InnoDB 实现事务隔离级别的核心机制。它通过为数据记录保存多个版本,允许读写操作不互相阻塞,从而在高并发场景下提升数据库性能。简单来说:读操作可以访问数据的历史版本,无需等待写操作释放锁;写操作也无需阻塞读操作,二者通过 “版本” 实现隔离。

        在 MVCC 出现前,数据库解决并发问题主要依赖锁机制:读加共享锁(S 锁),写加排他锁(X 锁)。但这种方式会导致 “读阻塞写、写阻塞读”,严重影响并发性能。MVCC 的出现解决了这一痛点,其核心作用包括:

(1)读写不冲突:读操作无需加锁,直接读取历史版本;写操作仅锁定当前版本,不阻塞读。

(2)支持多隔离级别:通过控制 “可见版本” 的规则,实现 REPEATABLE READ(可重复读)、READ COMMITTED(读已提交)等隔离级别(InnoDB 默认 REPEATABLE READ)。

(3)解决并发问题:避免脏读(读取未提交的数据)、不可重复读(同一事务内多次读取结果不一致),配合间隙锁可解决幻读。

        InnoDB 的 MVCC 通过隐藏列undo 日志Read View三大组件协同实现,缺一不可。它们的作用分别是:

        (一)隐藏列:

        InnoDB 为每个数据行添加了 3 个隐藏列(非用户定义),用于标记版本信息:

(1)DB_TRX_ID:6 字节,记录最后一次修改该记录的事务 ID(事务开始时由 InnoDB 分配的唯一 ID)。

(2)DB_ROLL_PTR:7 字节,回滚指针,指向该记录的上一个版本(存储在 undo 日志中)。

(3)DB_ROW_ID:6 字节,若表无主键或唯一索引,InnoDB 会用该列生成聚簇索引(可选)。

例如,一行数据的实际存储结构如下(简化):

id(用户定义)nameDB_TRX_IDDB_ROLL_PTR
1张三100指向 undo 日志版本 1

(二)undo 日志:

        undo 日志(回滚日志)是存储数据历史版本的地方。当事务修改数据时,InnoDB 会先将旧版本数据写入 undo 日志,再更新当前记录的 DB_TRX_ID 和 DB_ROLL_PTR(指向 undo 日志中的旧版本)。undo 日志分为两类:

(1)insert undo:记录插入操作的旧版本,事务提交后可直接删除(插入的数据在事务外不可见)。

(2)update undo:记录更新 / 删除操作的旧版本,需保留至没有事务需要访问这些版本(由 purge 线程清理)。

        通过 undo 日志,InnoDB 可形成一条 “版本链”:当前记录 → DB_ROLL_PTR → 上一版本(undo 日志) → ... → 最初版本。

(三)Read View:

        Read View(读视图)是一个动态生成的 “快照”,用于判断当前事务能看到哪个版本的数据。它包含 4 个核心字段:

(1)m_ids:当前活跃(未提交)的事务 ID 列表。

(2)min_trx_id:m_ids 中的最小事务 ID。

(3)max_trx_id:下一个将被分配的事务 ID(并非 m_ids 中的最大值)。

(4)creator_trx_id:当前事务的 ID。

其有一套可见性判断规则,例如(假设某版本的 DB_TRX_ID 为 trx_id):

(1)若trx_id == creator_trx_id:当前事务修改的版本,可见。

(2)若trx_id < min_trx_id:该版本由已提交事务生成,可见。

(3)若trx_id > max_trx_id:该版本由未来事务生成,不可见。

(4)若min_trx_id ≤ trx_id ≤ max_trx_id,trx_id在m_ids中,则事务未提交,版本不可见。反之,若trx_id不在m_ids中,则代表事务已提交,版本可见。

        通过这套规则,Read View 能精准筛选出当前事务可访问的版本。

        MVCC的主要优点有:
(1)读写并发性能高:读不加锁,写仅锁当前版本,避免 “读写互斥”,适合读多写少场景。
(2)事务隔离性好:通过版本控制天然支持读已提交和可重复读隔离级别,无需复杂锁机制。
(3)减少锁竞争:避免了共享锁(S 锁)的使用,降低死锁概率。
但其也有缺点,例如:
(1)存储开销大:undo 日志需保存多个历史版本,可能占用较多磁盘空间。
(2)性能损耗:版本链遍历和 Read View 判断会增加 CPU 开销;undo 日志的清理(purge 线程)也需额外资源。
(3)幻读问题:MVCC 在可重复读隔离级别下无法完全解决幻读(需配合间隙锁,InnoDB 默认开启)。

        下面,我们通过实际 SQL 操作,观察 MVCC 在不同隔离级别下的表现(以 InnoDB 为例):

-- 创建一个用户表,并插入数据
CREATE TABLE `user` (`id` int PRIMARY KEY,`name` varchar(10) NOT NULL
) ENGINE=InnoDB;INSERT INTO `user` VALUES (1, '张三');

        在可重复读隔离级别(默认级别)下,其运行如下:

时间事务 A(ID=100)

事务 B(ID=200)

T1BEGIN;BEGIN;
T2(未操作)SELECT * FROM user WHERE id=1; -- 结果:name=' 张三 '(DB_TRX_ID=0,初始版本)
T3UPDATE user SET name=' 李四 ' WHERE id=1; -- DB_TRX_ID=100,DB_ROLL_PTR 指向旧版本(未操作)
T4(未提交)SELECT * FROM user WHERE id=1; -- 结果:仍为 ' 张三 '(因可重复读隔离级别的 Read View 在 T1 生成,看不到事务 A 的未提交版本)
T5COMMIT;(未操作)
T6(已提交)

SELECT * FROM user WHERE id=1; -- 结果:仍为 ' 张三 '(可重复读隔离级别的 Read View 不变,看不到事务 A 的提交版本)

        可以看到,在可重复读隔离级别下,事务 B 全程看到的是 T1 时刻的版本,符合要求。而下面将隔离级别换成读已提交级别:

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

        然后运行如下:

时间

事务 A(ID=100)事务 B(ID=200)
T1BEGIN;BEGIN;
T2(未操作)SELECT * FROM user WHERE id=1; -- 结果:' 张三'
T3UPDATE user SET name=' 李四 ' WHERE id=1;(未操作)
T4(未提交)SELECT * FROM user WHERE id=1; -- 结果:' 张三 '(事务 A 未提交,版本不可见)
T5COMMIT;(未操作)
T6(已提交)SELECT * FROM user WHERE id=1; -- 结果:' 李四 '(RC 的 Read View 在 T6 重新生成,看到已提交版本)

        可以看出,在读已提交级别下,事务 B 在 T6 看到了事务 A 提交的新版本,符合其 “读已提交” 特性。

        可以看出在读已提交和可重复读2个级别中,MVCC的主要差异在于在可重复读级别中,事务开始时生成一次 Read View,事务内查询结果一致;而读已提交级别中,每次查询时生成新的 Read View,能看到其他事务已提交的更新。

        MVCC 是 InnoDB 并发控制的灵魂,通过隐藏列、undo 日志和 Read View 的协同,实现了 “读写不阻塞” 的高效并发,理解 MVCC 能帮助我们写出更符合隔离级别的 SQL。

http://www.lryc.cn/news/609232.html

相关文章:

  • 浅谈Python中的os.environ:环境变量交互机制
  • [硬件电路-141]:模拟电路 - 源电路,信号源与电源,能自己产生确定性波形的电路。
  • IO流-数据流
  • LLM的训练:RLHF及其替代方案
  • 2025年6月电子学会青少年软件编程(C语言)等级考试试卷(七级)
  • 当Windows远程桌面出现“身份验证错误。要求的函数不受支持”的问题
  • 电机结构设计与特性曲线分析:基于MATLAB和FEMM的仿真研究
  • 【软考中级网络工程师】知识点之 IS-IS 协议
  • AI Agent 重塑产业发展新格局
  • SpringAI的使用
  • 图像张量中的通道维度
  • 【C 学习】04.1-数字化基础
  • Spring Boot 整合 Minio 实现高效文件存储解决方案(本地和线上)
  • Monaco Editor 开发流程详解
  • Flutter Dart类的使用
  • Redisson高并发实战:守护Netty IO线程的关键指南
  • 一加Ace5无法连接ColorOS助手解决(安卓设备ADB模式无法连接)
  • 【MySQL】MySQL 中的数据排序是怎么实现的?
  • FreeRTOS源码分析三:列表数据结构
  • 深度学习-读写模型网络文件
  • 03.一键编译安装Redis脚本
  • 07.config 命令实现动态修改配置和慢查询
  • ThinkPHP8.x控制器和模型的使用方法
  • VUE-第二季-01
  • 【实习总结】Qt通过Qt Linguist(语言家)实现多语言支持
  • Python-初学openCV——图像预处理(六)
  • 机器学习之决策树(二)
  • solidworks打开step报【警告!可用的窗口资源极低】的解决方法
  • 《C 语言内存函数深度剖析:从原理到实战(memcpy/memmove/memset/memcmp 全解析)》
  • 使用ACK Serverless容器化部署大语言模型FastChat