一、MVCC 到底是干嘛的?
MVCC 全称 Multi-Version Concurrency Control(多版本并发控制),核心作用就一个:让多个人同时读写同一条数据,不用互相锁着等,读写不冲突、不阻塞,大家都能顺畅操作。
举个最直白的例子:有人在修改一条数据(写操作),你同时去读这条数据(读操作),不用等他改完,你能正常读到数据,他也能正常修改,互不干扰——这就是MVCC的功劳。
二、核心比喻:用“图书阅览室”理解MVCC全貌
把MySQL数据库想象成一个「图书阅览室」,帮你快速理解MVCC的所有核心组件:
「书」= 数据库里的每一行数据;
「写操作」= 有人要修改书里的内容(比如把“1”改成“3”);
「读操作」= 有人要翻看这本书;
「undo log」= 阅览室的「档案柜」,专门存放“书的旧版本”(修改前先复印一份存档);
「Read View」= 你进阅览室时,管理员给你发的「阅读权限清单」(规定你能看哪些版本的书);
「事务」= 你在阅览室的「阅读/修改时长」(从进来到离开,期间你看到的书的版本是固定的)。
没有MVCC的情况:有人改书,就把阅览室锁起来,别人不能进、不能看,效率极低;
有MVCC的情况:改书的人改新书,看书的人读档案柜里的旧书,互不打扰,效率拉满。
三、MVCC 核心原理:3个关键组件
MVCC能实现“读写不冲突”,全靠3个核心组件协同工作,结合比喻一步步讲透,不跳任何步骤。
1. 组件1:每行数据的“隐藏3字段”
InnoDB引擎会在每一行数据背后,偷偷藏3个我们看不到的隐藏字段,这是MVCC的基础,相当于给每本书做了“版本标记”:
「DB_TRX_ID」:哪个事务最后修改了这行数据(相当于“这本书最后被谁修改过”,有唯一的事务ID);
「DB_ROLL_PTR」:版本指针,指向undo log里的“旧版本数据”(相当于“这本书的旧复印件,存放在档案柜的哪个位置”);
「DB_ROW_ID」:隐藏主键,没有手动设置主键时,MySQL会自动用这个字段当主键(无关核心逻辑,了解即可)。
2. 组件2:undo log(MVCC的“旧版本档案柜”)
之前讲三大日志时,我们知道undo log是“回滚日志”,但它还有一个更重要的作用——给MVCC提供“历史版本数据”,相当于阅览室的档案柜,专门存旧书复印件。
undo log的写入时机:执行增删改操作前,先把修改前的旧数据写入undo log(比如要把“1”改成“3”,先把“1”抄一份存进档案柜,再改书);
undo log的回收:事务提交后,不会立即删除,而是“延迟回收”(标记为“可回收”,等没有事务再需要这份旧版本时,后台自动清理,避免有人还在看旧书时,档案被删);
与MVCC的关联:undo log里的每一条旧数据,都是一行数据的“历史版本”,通过DB_ROLL_PTR指针,能串联起所有历史版本(比如从“3”找到“1”,再找到更早的版本)。
3. 组件3:Read View
当你开启一个事务,第一次执行查询时,MySQL会瞬间给你拍一张“快照”,这张快照就是Read View,相当于管理员给你的“阅读权限清单”——规定了你能看哪些版本的书,不能看哪些。
Read View里只有4个关键信息:
「m_ids」:当前正在阅览室里、还没离开(未提交)的人(事务)列表;
「min_trx_id」:这些未提交的人(事务)里,ID最小的那个;
「max_trx_id」:下一个要进入阅览室的人(事务)的ID(相当于“下一个读者的编号”);
「creator_trx_id」:你自己的事务ID(相当于“你的读者编号”)。
核心作用:Read View决定了你能看到哪一个版本的数据——不是最新的,而是“你有权限看的、最适合的版本”。
四、MVCC 核心逻辑:怎么判断“我能读哪个版本”?
当你查询某一行数据时,MySQL会拿着这行数据的「DB_TRX_ID」(最后修改它的事务ID),对照你的Read View(阅读权限清单),按4条简单规则判断,全程不用复杂计算:
规则1:自己改的,能看
如果这行数据的DB_TRX_ID(最后修改者),和你的creator_trx_id(你自己的事务ID)一样 → 这是你自己改的,当然能看(自己改的书,自己肯定能看)。
规则2:早于你进入的、已提交的,能看
如果这行数据的DB_TRX_ID,比Read View里的min_trx_id(当前未提交事务的最小ID)还小 → 说明修改这行数据的人,在你进入阅览室之前就已经离开(提交事务)了,这是“历史稳定版本”,能看(别人早就改完走了,你看他改后的版本没问题)。
规则3:晚于你进入的、未提交的,不能看
如果这行数据的DB_TRX_ID,大于或等于Read View里的max_trx_id(下一个事务ID) → 说明修改这行数据的人,是在你之后进入阅览室的,而且还没离开(未提交),你不能看(别人还在改,你不能看半成品)。
规则4:和你同时在、未提交的,不能看
如果这行数据的DB_TRX_ID,在min_trx_id和max_trx_id之间,而且在m_ids(当前未提交事务列表)里 → 说明修改这行数据的人,和你同时在阅览室(未提交),你不能看(别人还没改完,你看不到他的修改)。
关键补充:读不到怎么办?
如果按上面的规则,判断当前版本不能看,MySQL会顺着这行数据的「DB_ROLL_PTR」指针,去undo log(档案柜)里找“上一个旧版本”,再用上面的规则重新判断,直到找到一个“你能看的版本”为止。
五、经典场景演示
用“读旧数据、数据覆盖别人修改的数据”场景,一步步演示MVCC的工作过程,看完就懂为什么会读旧数据,以及为什么会覆盖别人修改后的数据。
场景:初始数据 num=1,两个事务同时操作
1.时刻1:你开启事务A(creator_trx_id=100),执行查询:SELECT num FROM t WHERE id=1;
MySQL给你生成Read View:m_ids=[], min_trx_id=101, max_trx_id=101, creator_trx_id=100;
判断:当前数据num=1的DB_TRX_ID=0(初始无修改),小于min_trx_id=101,能看;
你读到:num=1(旧版本)。
2.时刻2:别人开启事务B(creator_trx_id=101),执行修改:UPDATE t SET num=3 WHERE id=1;
先写undo log:把num=1的旧数据存入undo log,DB_ROLL_PTR指向这条旧数据;
内存中把num改成3,数据行的DB_TRX_ID更新为101;
事务B提交(相当于“改书的人离开阅览室”)。
3.时刻3:你在事务A中,再次执行查询:SELECT num FROM t WHERE id=1;
你依然用“时刻1生成的Read View”(MySQL默认隔离级别下,事务内Read View全程不变);
判断:当前数据num=3的DB_TRX_ID=101,等于max_trx_id=101,不能看;
顺着DB_ROLL_PTR去undo log找旧版本,找到num=1(DB_TRX_ID=0),能看;
你读到的还是:num=1(旧数据)——这就是“读不到最新数据”的原因。
4.时刻4:你在事务A中,执行修改:UPDATE t SET num=1+1 WHERE id=1;
你基于读到的旧数据1,计算出2,执行更新;
此时,事务B已经提交,数据实际是3,但你看不到;
更新后,数据被你改成2,覆盖了别人的3——这就是“数据覆盖”的问题。
六、核心疑问解答
疑问1:读不到最新数据,岂不是数据有误?
结论:不算错误,这是MySQL“事务隔离”的正常行为,是故意设计的。
解释:MVCC的核心目的是“保证同一个事务内,看到的数据一致、不混乱”,而不是“保证读到最新数据”。就像你手里拿着一份9点的报纸(Read View),不管之后出了多少晚报(别人的修改),你手里的报纸始终是9点的——宁可读旧数据,也不能让你在同一个事务里,前后读到不一样的数据(比如第一次读1,第二次读3,算错账)。
补充:如果需要读到最新数据,不要用长事务,读完就提交,每次查询都会生成新的Read View,就能看到别人提交的最新修改。
疑问2:(重点)读旧数据后修改,会覆盖别人的新数据,怎么避免?
结论:MVCC只保证“读安全、读一致”,不保证“写安全”,单纯靠MVCC先读后改,一定会出现覆盖,需要额外加机制。
3种小白也能上手的解决方案(真实业务常用):
1.方案1:SELECT ... FOR UPDATE(加行锁,最常用)
读数据时,直接给数据上锁:SELECT num FROM t WHERE id=1 FOR UPDATE; ,别人要改这条数据,必须等你事务结束,你读的一定是最新数据,不会基于旧值修改,避免覆盖。
2.方案2:原子更新SQL(不用先读后改)
不要先读num=1,再计算2,直接让MySQL自己计算:UPDATE t SET num = num + 1 WHERE id=1; ,这是原子操作,不会被打断,MySQL会自动处理并发,绝对安全。
3.方案3:乐观锁(版本号机制)
给表加一个version字段(版本号),更新时判断版本是否一致:UPDATE t SET num=2, version=version+1 WHERE id=1 AND version=旧版本号; ,如果别人改了数据,版本号会变,更新失败,你重试即可。
疑问3:MVCC和undo log的关联,到底是什么?
结论:undo log是MVCC的“历史版本仓库”,没有undo log,MVCC就无法提供旧版本数据,也就无法实现“读写不冲突”。
简单说:MVCC负责“判断你能看哪个版本”,undo log负责“提供这个版本的数据”,两者缺一不可——没有undo log,MySQL找不到旧版本,只能给你上锁,就失去了MVCC的意义。
七、MVCC 与事务隔离级别
MVCC主要支持MySQL的2种事务隔离级别,行为不同,对应不同的业务场景,结合Read View的生成时机就能看懂:
1. REPEATABLE READ(可重复读,MySQL默认)
核心特点:事务开始时,生成一次Read View,全程用到底。
效果:不管别人怎么修改、怎么提交,你在同一个事务里,读到的始终是事务开始时的旧版本(就像手里的报纸全程不变),实现“可重复读”,避免“不可重复读”,但会一直读旧数据。
2. READ COMMITTED(读已提交)
核心特点:每次执行SELECT时,都会生成新的Read View。
效果:别人提交了新修改,你下次查询就能看到最新数据,不会一直读旧数据,但可能出现“不可重复读”(同一个事务里,两次查询结果不一样)。
3. SERIALIZABLE(串行化)
不使用MVCC,直接给数据上锁,读写排队执行,完全避免并发问题,但性能极低,适合对数据一致性要求极高、并发量极低的场景(如银行对账)。
八、极简总结
1.MVCC = 多版本并发控制,核心是“读写不冲突、不阻塞”;
2.3个核心组件:数据隐藏字段(版本标记)、undo log(旧版本仓库)、Read View(阅读权限);
3.读旧数据不是错误,是事务隔离的正常行为,保证事务内数据一致;
4.MVCC不保证写安全,先读后改会覆盖别人数据,需用加锁、原子更新、乐观锁解决;
5.undo log不仅用于回滚,更是MVCC的核心,没有undo log就没有MVCC。