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

MVCC 与事务隔离:MySQL 如何实现“读不阻塞写”?

很多开发者每天都在用@Transactional,却从未真正理解事务隔离的底层实现。

  • 当面试官问:“MySQL的RR级别是如何解决幻读的?”或者“MVCC到底是怎么工作的?”如果你只能回答“通过锁”或者“通过多版本”,那还停留在“知其然”的层面。

今天,我们要深入到InnoDB的行记录结构、Undo Log版本链和Read View的生成逻辑,彻底拆解MVCC的“平行宇宙”机制,看看它是如何实现“读不阻塞写”的。

事务隔离的痛点:锁的代价

在没有MVCC之前,数据库要实现隔离性,只能靠

  • 读操作加共享锁(S锁):读的时候,别人不能写。
  • 写操作加排他锁(X锁):写的时候,别人不能读。

这种机制在高并发下是灾难性的。想象一下,电商系统的商品详情页(高频读)和库存扣减(高频写),如果读和写互相阻塞,系统吞吐量会直线下降。

MVCC的出现,就是为了解决这个问题:让读操作不加锁,让写操作不阻塞读。

MVCC的底层解剖:行记录的“隐藏字段”

在InnoDB中,每一行数据(聚簇索引记录)除了我们定义的列,还隐式包含三个隐藏字段

  • DB_TRX_ID(6字节):最近修改该行数据的事务ID。
  • DB_ROLL_PTR(7字节):回滚指针,指向该行数据在Undo Log中的上一个版本。
  • DB_ROW_ID(6字节):行ID,如果没有主键,InnoDB会自动生成。
// InnoDB行记录的简化结构 struct row_t { // 用户定义的列 int id; char name[20]; // 隐藏字段 uint64_t DB_TRX_ID; // 最后一次修改的事务ID uint64_t DB_ROLL_PTR; // 指向Undo Log中的旧版本 uint64_t DB_ROW_ID; // 行ID };

当一个事务修改数据时,InnoDB会:

  1. 将旧版本数据写入Undo Log。
  2. 更新当前行的DB_TRX_ID为当前事务ID。
  3. 更新DB_ROLL_PTR指向Undo Log中的旧版本。

这样,所有版本的数据通过DB_ROLL_PTR串联成一个版本链

Undo Log:后悔药与版本链

Undo Log不仅仅是用来回滚的,它还是MVCC的“历史档案馆”。

版本链的形成

假设初始数据是id=1, name='Alice',事务ID为100。

  1. 事务200执行UPDATE user SET name='Bob' WHERE id=1

    • name='Alice'写入Undo Log。
    • 更新当前行name='Bob'DB_TRX_ID=200DB_ROLL_PTR指向Undo Log中的Alice版本。
  2. 事务300执行UPDATE user SET name='Charlie' WHERE id=1

    • name='Bob'写入Undo Log。
    • 更新当前行name='Charlie'DB_TRX_ID=300DB_ROLL_PTR指向Undo Log中的Bob版本。
当前行: name='Charlie', DB_TRX_ID=300, DB_ROLL_PTR -> Undo Log | Undo Log: name='Bob', DB_TRX_ID=200, DB_ROLL_PTR -> Undo Log | Undo Log: name='Alice', DB_TRX_ID=100, DB_ROLL_PTR -> NULL

Read View:判断版本可见性的“时光机”

这是不是挺难的,没关系、其实很好理解,下面咱们来看一下:

有了版本链,事务如何判断哪个版本对自己是可见的?这就需要一个Read View(读视图)

struct ReadView { vector<uint64_t> m_ids; // 生成Read View时,所有活跃(未提交)事务ID集合 uint64_t min_trx_id; // m_ids中的最小值 uint64_t max_trx_id; // 生成Read View时,下一个将要分配的事务ID uint64_t creator_trx_id; // 创建该Read View的事务ID };

可见性判断规则

当事务访问某行数据时,从最新版本开始,沿着版本链依次检查每个版本的DB_TRX_ID

  1. 如果DB_TRX_ID == creator_trx_id:当前事务自己修改的,可见
  2. 如果DB_TRX_ID < min_trx_id:该版本的事务在Read View生成前已提交,可见
  3. 如果DB_TRX_ID >= max_trx_id:该版本的事务在Read View生成后才启动,不可见
  4. 如果min_trx_id <= DB_TRX_ID < max_trx_id
    • 如果DB_TRX_IDm_ids中:该版本的事务在Read View生成时未提交,不可见
    • 如果DB_TRX_ID不在m_ids中:该版本的事务在Read View生成时已提交,可见

代码模拟:可见性判断

public boolean isVisible(RowVersion version, ReadView readView) { uint64_t trxId = version.getDB_TRX_ID(); if (trxId == readView.getCreatorTrxId()) { return true; // 自己修改的,可见 } if (trxId < readView.getMinTrxId()) { return true; // 已提交,可见 } if (trxId >= readView.getMaxTrxId()) { return false; // 未启动,不可见 } // 在min和max之间,检查是否在活跃事务列表中 return !readView.getMIds().contains(trxId); }

RC与RR:Read View生成时机的差异

READ COMMITTED(RC):每次查询都生成新的Read View

  • 事务A启动,生成Read View 1。
  • 事务B修改数据并提交。
  • 事务A再次查询,生成新的Read View 2,能看到事务B的修改。
  • 结果:不可重复读

REPEATABLE READ(RR):第一次查询生成Read View,后续复用

  • 事务A启动,第一次查询时生成Read View 1。
  • 事务B修改数据并提交。
  • 事务A再次查询,复用Read View 1,看不到事务B的修改。
  • 结果:可重复读

RR级别下的幻读问题

在RR级别下,MVCC只能解决“快照读”的幻读(普通SELECT)。
对于“当前读”(SELECT ... FOR UPDATEUPDATEINSERT),InnoDB使用Next-Key Lock(间隙锁+记录锁)来防止幻读。

当前读 vs 快照读

快照读(Snapshot Read)

  • 普通的SELECT ...语句。
  • 基于MVCC,读取历史版本。
  • 不加锁,不阻塞写。

当前读(Current Read)

  • SELECT ... FOR UPDATEUPDATEINSERTDELETE
  • 读取最新版本,加锁。
  • 阻塞其他事务的写操作。

为什么SELECT ... FOR UPDATE不走MVCC?

因为它需要获取排他锁,必须读取最新数据,否则会导致数据不一致。

总结

MVCC不是真的复制了多份数据,而是利用Undo Log版本链 + Read View动态构建出的“历史视图”。

  • Undo Log:存储历史版本,形成版本链。
  • Read View:判断版本可见性,实现隔离性。
  • 隐藏字段:连接当前数据和历史版本。

最后,送师金句
“MVCC不是真的复制了多份数据,而是利用Undo Log版本链 + Read View动态构建出的‘历史视图’。它是高并发下读写并行的核心秘密。”

http://www.jsqmd.com/news/557000/

相关文章:

  • YimMenu全面使用指南:从功能探索到安全应用的完整路径
  • 当因果图遇到混淆变量:手把手教你用PAG(部分祖先图)解读真实世界数据
  • Druid连接池minIdle和maxActive参数详解:如何避免连接池耗尽问题
  • 基于PLC的间歇反应釜智能温控系统设计与实践【附仿真代码】
  • 创新二维码生成利器:theqrmodule模块实战指南
  • ARKit数字人开发指南:如何用苹果52个BlendShape权重实现自然表情动画
  • 在C++中,什么是类的友元函数,如何使用?
  • 从零到一:用HarmonyOS和ArkTS开发一个宠物社交App(附数据库设计)
  • 聊天记录丢失?用WeChatMsg构建个人数据护城河,让数字资产永久归属自己
  • Windows持久化核心战术:系统服务植入实战教程
  • 给CFD新手的建议:从Python环境到OpenFOAM cavity案例,我的第一个完整模拟踩坑记录
  • Ubuntu 22.04 镜像源切换实战:从备份到极速更新的保姆级指南
  • python vue大学生足球队俱乐部管理系统
  • FanControl:Windows系统终极风扇控制软件完整使用指南
  • YOLOv11涨点改进| Arxiv 2026 | 独家创新首发、注意力改进篇| 引入InfSA无限自注意力模块,使注意力图更聚焦、全局建模更强,含多种改进,助力小目标检测、图像分割、图像分类高效涨点
  • LabelImg终极指南:快速掌握免费图像标注工具的使用技巧
  • 4大维度重塑音乐体验:面向发烧友的foobar2000增强方案
  • 【动静障碍物】基于JPS算法(改进A)全局路径规划与DWA动态窗口局部避障的机器人自主导航混合控制算法附Matlab代码
  • Windows应急响应实战:玄机靶场vulntarget-j-02后门排查全记录(附NTLM哈希爆破脚本)
  • 揭秘AI写教材:低查重技巧与高效工具的完美结合
  • 从API调试到文件加密:Python GMSSL的SM4算法在5个真实场景下的应用代码
  • 20251202马思钊3.23实验课报告
  • 使用Java实现支付宝支付接口的完整对接教程
  • BAAI/bge-m3从零部署:WebUI可视化工具,快速实现语义匹配验证
  • Windows powershell view huge file via command
  • 突破安卓权限壁垒:LAMDA自动化框架的跨设备流媒体解析技术全解
  • python+vue电影推荐系统python协同过滤
  • VisionPro+C#实战:告别.vpp文件,用CogFrameGrabbers类动态抓取工业相机(附完整WinForm源码)
  • 硬件设计避坑指南:反相降压-升压电路5个易错点实测复盘
  • 东方博宜OJ 1928:采购礼品 ← 有依赖的背包 + 并查集