做后端开发和数据库运维的同学,面试也好、日常排查也好,大概率都跟MVCC这三个字母打过照面。MySQL的MVCC(Multi-Version Concurrency Control,多版本并发控制)是InnoDB存储引擎用来处理读写并发互不阻塞的核心机制,也是理解事务隔离级别、锁行为、undo log清理策略的总入口。这篇博文我会从一个实践者的角度,把MVCC的核心原理、ReadView可见性判断、RR与RC的差异,以及本地可复现的验证实验一次讲透,适合对InnoDB有一定基础但总觉得“好像懂了又说不太清”的人,也适合准备数据库面试的朋友当复习提纲。
我会按照“问题从哪来 → 底层机制长什么样 → 隔离级别怎么配合 → 实际动手验证 → 常见误区和排查技巧”这条线来讲,尽量不绕弯子,尽量把关键结论都落到可验证的SQL上。
1. 并发控制的前置问题:为什么数据库非要搞出MVCC
1.1 没有MVCC时,事务并发会踩哪些坑
两个事务同时操作同一行数据时,最自然的做法是用锁把这一行锁住,一个事务读,另一个事务就必须等着。但仔细想想,这种做法对“读多写少”的业务很不友好。一个普通电商详情页,可能一秒有上千次查询,期间只有几次价格调整,如果每次查询都被写操作堵住,页面延迟马上就会被打上去。
数据库事务要解决的并发问题主要有三个:脏读(读到别人未提交的数据)、不可重复读(同一事务内两次读同一行,结果不一样)、幻读(同一事务内两次范围查询,行数不一样)。在读已提交(RC)和可重复读(RR)这两个隔离级别下,脏读和不可重复读也是不能出现的。如果全靠加锁去保证,要么是读的时候也加共享锁,导致读写互相阻塞;要么是让写操作排队等读完成,并发度会低得没法看。
MVCC的思路不是“堵”,而是“绕”。它把每一行数据的多个历史版本都保存下来,读操作直接读取符合自己事务视角的旧版本,写操作往版本链上追加新版本。读写各走各的版本,互不等待。这就好比同一个文档,别人在改最新稿,你手里是一份改了也不会影响别人的快照稿,大家各看各的。
1.2 锁和MVCC的分工并不重叠
MVCC不是用来替代锁的。InnoDB的并发控制实际上是“MVCC + 锁”的组合拳:
- 读读并发:任何时候都不冲突,MVCC管不到也不需要管。
- 读写并发:查询走快照读,读旧版本,不需要等写锁释放;写操作继续往版本链上追加新版本。这部分是MVCC的核心价值。
- 写写并发:MVCC解决不了,两个事务同时改一行,必须靠行锁互斥。后到的事务只能等先到的事务提交或回滚。
很多初学者以为MVCC能解决一切并发冲突,这是后面要重点纠正的误区。简单记一句话:MVCC解决读写阻塞,行锁解决写写互斥,间隙锁和Next-Key Lock解决幻读。各管一段,谁也替代不了谁。
1.3 用协作办公类比理解“多版本”思想
拿多人协作编辑一份在线文档举例。最常见的三种策略是:实时协同编辑很多人同时落笔,那就要做非常细粒度的冲突合并;悲观排他编辑,一个人打开编辑时其他人只能只读甚至完全进不去,安全但低效;再来就是“分支快照”模式,我把文档复制出一个分支来写,改完再合并回去,其他人不受影响,但合并时会发现自己这版可能落后了。
InnoDB的MVCC更像是第三种思路的数据库版本。事务开始后,它通过ReadView拿到一个“我当前应该看到哪个版本”的视角,读数据时沿着版本链找到符合视角的版本返回,而不是傻等别人写完。等写事务提交后,版本链上新增的版本也不会立刻销毁,而是保留一段时间,给那些“视角还停留在过去”的事务继续读。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. MVCC的三个底层支柱:隐藏列、undo log、ReadView
2.1 每行数据上那三列看不见的元数据
InnoDB的聚簇索引记录里,除了业务字段,还固定藏着几个隐藏列,平时SELECT不会返回,但它们是MVCC的地基:
- DB_TRX_ID:最近一次对该行执行写操作的事务ID。
- DB_ROLL_PTR:回滚指针,指向该行在undo log中的上一个版本,版本链就是靠它串起来的。
- DB_ROW_ID:隐藏主键。如果表没有显式主键,InnoDB会用它来生成聚簇索引;有主键时这个字段不会额外占用可见空间。
举个例子。初始时事务T1插入了一行数据,balance=100,这行上的DB_TRX_ID记着T1的ID。接着事务T2执行了UPDATE,把balance改成200,InnoDB不会直接覆盖物理行,而是先把当前行的旧版本(100)写到undo log,再把新值(200)写到当前行,同时把DB_TRX_ID改成T2的ID,DB_ROLL_PTR指向刚刚写入的旧版本。此时版本链是:
| 版本位置 | balance | 写入事务 | 说明 |
|---|---|---|---|
| 当前行 | 200 | T2 | 最新版本,物理存储在聚簇索引 |
| undo log record | 100 | T1 | 旧版本,通过DB_ROLL_PTR关联 |
如果T3又把这行改成300,情况会继续向上叠加:当前行持有300和T3的ID,回滚指针指向200那个undo记录,而200的undo记录又通过它自己的回滚指针指向100的undo记录。一条朝旧版本方向延伸的链表就这样形成了。读取的时候如果发现当前版本不合适,就往回找,直到找到自己可见的版本。
这里有个概念要提前说清:INSERT操作产生的undo log在事务提交后通常可以立刻清理,因为新插入的行对别的事务来说从来就是不可见的,不需要保留旧版本供回滚读取;真正需要长期保留的是UPDATE和DELETE产生的undo log,因为它们承载着版本链的旧版本,可能还有更早启动的事务等着按旧版本读取。
2.2 undo log不只是用来回滚,更是版本链的载体
提起undo log,多数人第一反应是“事务回滚时要靠它恢复数据”。没错,这是它的基本职责。但在MVCC机制里,它的另一个身份更关键:版本链上的每个旧版本,物理上都存在于undo log中。
以UPDATE为例,一个UPDATE其实可以拆成两步看:先标记旧记录删除,再插入新记录。实际操作中InnoDB做了大量优化,但我建议你在理解阶段就按“旧版本进入undo log,新版本写入聚簇索引”来记,这样看锁和MVCC都顺很多。DELETE也不神秘,它只是给记录打删除标记并把值记录到undo log,真正的物理清除要等purge线程来回收。
版本链的读取方向永远是“从当前最新版本往旧版本方向”。读请求到达时,如果当前行版本对自己不可见,就沿着DB_ROLL_PTR跳到undo log里的旧版本,再判断一次,直到找到可见版本为止。这条链路可能很长,也可能很短,取决于ReadView和活跃事务的远近关系。链上每个节点都记录了对应的trx_id,判断时用的就是它。
2.3 ReadView:快照读的“可见性规则”
MVCC最核心的判断动作发生在“生成ReadView”这一刻。ReadView本质上是一个事务在某个时间点对全部事务状态的快照,里面有几个关键信息:
- m_ids:生成ReadView时,当前正在活跃(未提交)的事务ID列表。
- min_trx_id:m_ids中的最小事务ID。
- max_trx_id:生成ReadView时,InnoDB下一个将要分配的事务ID。注意它不是当前最大事务ID,而是“还没被分配出去”的下限,所以m_ids里的事务ID肯定都小于它。
- creator_trx_id:生成这个ReadView的事务自己的ID。
判断某行版本的DB_TRX_ID是否可见,规则只有四条,按顺序执行:
| 条件 | 结论 | 原因 |
|---|---|---|
| trx_id == creator_trx_id | 可见 | 自己修改的数据当然自己要能读到 |
| trx_id < min_trx_id | 可见 | 该版本在ReadView生成前已提交 |
| trx_id >= max_trx_id | 不可见 | 该版本是在ReadView生成后启动的事务生成的 |
| min_trx_id <= trx_id < max_trx_id | 需要查m_ids | 如果trx_id在m_ids中,说明事务未提交,不可见;不在列表里,说明事务已提交,可见 |
下面举个具体的判断例子。假设生成ReadView时,事务ID是连续分配的,当前活跃事务是100、200、300,其中当前事务是200。那ReadView就是m_ids=[100,200,300],min_trx_id=100,max_trx_id=301,creator_trx_id=200。某个被读取的行版本B,DB_TRX_ID分别是80、150、250、500时:
| 行版本事务ID | 判断结果 | 具体原因 |
|---|---|---|
| 80 | 可见 | 80 < min_trx_id(100),ReadView生成前已提交 |
| 150 | 不可见 | 150在m_ids中,事务尚未提交 |
| 250 | 可见 | 250不在m_ids中,说明事务已提交 |
| 500 | 不可见 | 500 >= max_trx_id(301),ReadView生成后才分配的事务 |
这四条规则不需要死记硬背,你可以把它当成一个“前台在前台看到的世界”:比自己小的编号基本是前辈,比自己大的编号可能是后辈,还在后台没退场的(活跃里的)就当没看见。
2.4 快照读和当前读:两种完全不同的读取路径
普通SELECT(不加FOR UPDATE、不加LOCK IN SHARE MODE)就是快照读,走MVCC版本链,不加锁。INSERT、UPDATE、DELETE和SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE属于当前读,直接读最新已提交版本,并且对涉及记录加锁。DELETE和UPDATE之所以是当前读,是因为不先锁定最新版本,就没法安全地在其上做修改。
这里有个实践里很重要的推论:在同一个RR事务里,先执行普通SELECT拿到历史快照,再执行UPDATE,UPDATE不会基于快照里的旧值去改,而是基于最新已提交版本去改。这个特征会直接导致“先快照读后更新”和“先更新后快照读”看到的数据不一致,后面实验部分我会专门演示。
3. 隔离级别与MVCC的配合:RC和RR差别在哪
3.1 不同隔离级别下ReadView的生成时机完全不同
MVCC本身不限制生成ReadView的次数,关键在于生成时机和复用策略是由事务隔离级别决定的。
在读已提交(RC)级别下,事务内每一次普通SELECT都会重新生成一个新的ReadView。这意味着每次读都能看到在该时刻之前已提交的最新事务。所以RC级别天然不会出现脏读,因为已经提交的事务才可能进入新ReadView的可见范围,但可能出现不可重复读:同一次事务里,第一次SELECT生成ReadView时别的事务还没提交,第二个SELECT时事务提交了,新ReadView把它纳入了可见范围,两次结果自然就不同。
在可重复读(RR)级别下,只有事务内第一次执行快照读时才生成ReadView,之后整个事务的所有快照读都复用同一个ReadView。这样后面别的事务即使提交了,也不会影响自己的判断,因为ReadView里那批活跃事务列表没变过。这就是“可重复读”三个字的来源,不是锁把行锁住了,而是读的视角被钉在了过去。
3.2 用时间线演示“同一条SQL两个结果”
假设初始余额是100,事务A先开始但什么都不做,事务B把余额改成200并提交。如果事务A在事务B提交前做过一次快照读,那么RR下事务A之后无论读多少次,看到的都是100;RC下事务A如果之后再做快照读,看到的是200。
如果事务A在事务B提交之前没有做过任何快照读,情况又不一样。RR下事务A第一次做快照读发生在事务B提交之后,ReadView是在此时生成的,那时事务B已经不在活跃事务列表里,所以事务A直接看到200。这个细节很多人会忽略,也是“RR的可重复读是不是永远从事务开始那一刻算起”的经典争论点。准确说法是:RR的快照点在第一次快照读那一刻,不是事务开始那一刻。真正常见实现里,事务开始后第一条一致性读才建立快照。
3.3 RR不是完全靠MVCC消灭幻读
RR级别下,普通的快照读已经不会出现幻读了,因为整个事务看到的行数被同一个ReadView固定。但如果事务里执行的是当前读,比如SELECT ... FOR UPDATE,那读的是最新已提交数据,仍然可能发现别的并发事务刚刚插入的行,也就是产生幻读现象。
InnoDB对这种情况的兜底方案是加锁,准确说是Next-Key Lock,它是行锁和间隙锁的组合:既锁住命中的记录,也锁住记录之间的间隙,防止别的事务在范围内插入新行。当前读只有在唯一索引等值且命中的情况下,才会退化成单行记录锁;普通索引范围查询和等值未命中,都会涉及间隙锁或Next-Key Lock。所以对RR级别更严谨的表述是:快照读下用MVCC避免幻读,当前读下用Next-Key Lock避免幻读。两者结合,RR整体上才能做到对幻读的良好控制。
3.4 RC级别下为什么经常遇到间隙锁消失了
RC级别下,InnoDB基本只使用记录锁,没有间隙锁和Next-Key Lock,这也是RC并发度通常比RR高的原因之一。RC下出现幻读是允许的,所以不需要间隙锁去锁区间。代价是可能出现“一个UPDATE范围里,另一事务并发插入了一行,导致更新行数比预期多”的情况,这在做数据修正和批处理时要特别小心。
对读写分离或分库分表中间件场景,很多人喜欢把线上库设成RC,这样能减少间隙锁引发的死锁概率,对高并发写入更友好。但它放弃的是RR下的可重复读保证。业务上如果同一事务内有多次查询并要求结果绝对一致,就该在代码层补齐策略,比如改成查询后加锁,或无then重查。
4. 动手验证:在本地MySQL里观察MVCC的真实行为
4.1 准备测试表与必要的环境开关
先用下面这组SQL准备一个最简测试环境。注意要把默认隔离级别明确成RR,方便后面对照。
sql复制CREATE DATABASE IF NOT EXISTS mvcc_demo;
USE mvcc_demo;
CREATE TABLE account (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(32) NOT NULL,
balance INT NOT NULL
) ENGINE=InnoDB;
INSERT INTO account(id, name, balance)
VALUES (1, '张三', 100);
-- 查看当前隔离级别,8.0版本字段名是 transaction_isolation
SELECT @@transaction_isolation;
MySQL 8.0以前的版本查看隔离级别要写SELECT @@tx_isolation;,这个细节在不同版本间切换时很容易踩。如果当前是RR,继续往下走;如果是其他级别,执行SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;调整一下。
4.2 实验一:RR下两次快照读为什么读到旧数据
打开两个终端会话,按下面顺序执行:
会话A:
sql复制USE mvcc_demo;
START TRANSACTION;
SELECT * FROM account WHERE id = 1;
此时事务A已经生成ReadView,但还没做任何写操作。接着切到会话B:
会话B:
sql复制USE mvcc_demo;
START TRANSACTION;
UPDATE account SET balance = 200 WHERE id = 1;
COMMIT;
会话B可以正常更新并提交,整个过程不会被会话A的读阻塞。等到会话B提交后,再回到会话A执行:
会话A:
sql复制SELECT * FROM account WHERE id = 1;
RR下你会看到结果仍然是balance=100。原因是事务A的ReadView在第一次SELECT时已经生成,会话B当时还活跃,它的新版本(trx_id=事务B的ID)不在ReadView可见范围。后续即使会话B提交,ReadView里的m_ids并不会更新,所以事务A始终看不到B的新版本,只能沿版本链回退到100那个旧版本。
4.3 实验二:同一流程换成RC,结果立刻不同
把两个会话都改成RC级别,重新跑一遍上面的完整流程:
sql复制SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
会话A先开启事务,并做一次普通SELECT,读到balance=100;会话B执行UPDATE并提交;会话A再次执行同样的SELECT,这次读到的是200。
差别就出在ReadView生成时机上。RC下每次快照读都重新生成ReadView,第二次SELECT时事务B早就提交了,不在活跃事务列表里,新版本自然可见。用这个实验可以非常直观地跟面试官解释“不可重复读在RC下为什么存在、在RR下为什么不存在”。
4.4 实验三:同一事务内,先快照读再当前读的怪异表现
这个实验能帮你彻底理解快照读和当前读的差异,同时也是网上很多“RR下有幻读”“RR失效”争论的真正来源。重置balance为100后,按顺序执行:
会话A:
sql复制START TRANSACTION;
SELECT * FROM account WHERE id = 1;
-- 读到 100
会话B:
sql复制UPDATE account SET balance = 200 WHERE id = 1;
COMMIT;
会话A再执行以下两条,注意顺序:
sql复制-- 普通快照读,仍然是100
SELECT * FROM account WHERE id = 1;
-- 当前读,结果变成了200
SELECT * FROM account WHERE id = 1 FOR UPDATE;
普通SELECT走了ReadView,固定看到快照版本100。而FOR UPDATE是当前读,它不读取版本链里的历史版本,直接读最新已提交版本200,并给这行加锁。这个实验也解释了为什么RR下“先快照读,再UPDATE一个被别的事务修改过的行”时,UPDATE影响行数与预期不一致:因为UPDATE是当前读,拿到的不是快照里的旧值,而是最新值。
4.5 实验四:自己事务里改过的数据,为什么等下还能读到
这个实验用来验证ReadView判断规则里“trx_id == creator_trx_id时可见”这条。
会话A:
sql复制START TRANSACTION;
UPDATE account SET balance = 300 WHERE id = 1;
SELECT * FROM account WHERE id = 1;
事务A此时没有提交,但普通SELECT能读到balance=300,因为版本链头部的DB_TRX_ID等于当前事务自己的ID,ReadView必然放行。与此同时,会话B如果执行SELECT * FROM account WHERE id = 1;,读到的是100(假设balance还是100且会话B的ReadView晚于事务A的写操作生成,且事务A仍活跃),因为它无法看到事务A未提交的版本。这个现象也说明MVCC不会让一个事务读到别人未提交的修改,脏读在RC和RR下都不会出现。
我在做这个实验时发现一个容易误判的点:如果会话B的普通SELECT发生在事务A执行UPDATE之前,并且在RR下先建立了自己的ReadView,那么即使事务A之后提交了,会话B也始终看不到A的修改。这会让初学者误以为“事务B看不到A是因为隔离级别是RR”,实际上真正的原因在于B的事务内第一次SELECT时ReadView早就把A排除在外了。搞清楚这一点,很多线上诡异的“数据看不到”问题就能定位。
4.6 补充验证:用系统表观察活跃事务的变化
如果你想观察“事务ID在什么时候分配”“老事务为什么会影响undo log清理”,可以顺手查两个系统视图:
sql复制-- 查看当前InnoDB事务列表
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id
FROM information_schema.innodb_trx;
-- 查看undo log相关信息
SHOW ENGINE INNODB STATUS;
SHOW ENGINE INNODB STATUS的输出里,TRANSACTIONS段落会列出当前活跃事务以及它们持有的锁,在排查长事务和锁等待时非常有用。注意这个输出内容比较长,不要被前面的缓冲池信息带偏重点,直接找TRANSACTIONS结尾的段落看事务列表就行。
在实际项目里,如果发现undo log膨胀很快,一个高频原因是存在长时间不提交的RR事务。因为RR事务固定了一个ReadView,版本链上早于这个ReadView的旧版本都不能被purge线程清理,undo log只能越积越多。排查时优先看information_schema.innodb_trx里有没有跑了几个小时的空闲事务,发现后尽快提交或回滚。
5. 实战死角:MVCC常见误区与高频排查思路
5.1 误区一:以为MVCC能替代锁,高并发写还会死锁
网上很多教程把MVCC讲得像一个万能并发方案,导致部分同学写代码时放松了对锁的警惕。实际遇到死锁时,如果把原因归到MVCC头上,大概率方向就错了。死锁本质是多个事务以不同顺序持有锁并互相等待,这属于锁管理器的职责范围,MVCC并不参与写锁的仲裁。
最典型的发生场景是批量更新时两个事务按不同顺序更新同一组记录。比如事务A先更新id=1再更新id=2,事务B先更新id=2再更新id=1,两边都持有第一条记录的行锁再等对方释放,MySQL检测到死锁后会回滚其中一方。规避手段是按固定顺序更新,比如统一按主键从小到大排序,或者缩短事务持有锁的时间。
5.2 误区二:以为RR下的普通SELECT永远读不到最新数据
RR确实复用了同一个ReadView,但前提是事务里已经发生过快照读。如果一个RR事务开启后一直没执行任何普通SELECT,而是直接执行UPDATE或SELECT ... FOR UPDATE这类当前读,它读取的始终是最新已提交版本。等这个事务后续再做普通SELECT时,ReadView是第一次普通SELECT那次才生成的,这时别的事务如果早已提交,快照读也能看到较新的数据。
所以“RR事务开始得早,就一定看不到后提交的数据”这种说法不严谨。说到底,RR的ReadView生成点并不在事务开始语句,而在第一条一致性读语句。这一点和“可重复读”的名称容易让人产生歧义。
5.3 误区三:把explain的结果和MVCC行为扯在一起
很多初学者查执行计划时会疑惑:为什么EXPLAIN输出的rows是一个估算值,却看不到跟MVCC相关的内容,比如走版本链还是走当前读。这里要明确一点:EXPLAIN只展示执行计划和成本估算,不展示事务的可见性判断过程。普通SELECT和SELECT ... FOR UPDATE的EXPLAIN结果可能几乎一样,它们真正的差异发生在执行阶段——前者走版本链找合适版本,后者锁定最新版本并加锁。别指望从EXPLAIN里看出MVCC行为。
如果要在线上判断一个慢查询是因为版本链过长导致的,还是因为索引列历史版本太多导致的,更靠谱的做法是查看SHOW ENGINE INNODB STATUS里的history list length。这个值代表undo log中未被purge清理的历史版本数量,正常情况下应该在几百到几千波动,如果持续几十万甚至更高,基本可以判断有长事务拖住了purge线程。
5.4 高并发场景下,MVCC带来的四大“隐性成本”
MVCC让读写互不阻塞,但不是没有代价。第一,每个读请求都可能需要沿版本链回溯,版本链越长,读放大越明显;第二,旧版本在undo log中滞留会增加存储和磁盘I/O压力;第三,长事务会长期占用ReadView,阻碍purge线程回收undo log,导致undo log异常膨胀;第四,大批量更新时,高版本密度会让purge线程跟不上版本产生速度,造成瞬时undo log暴涨。
在线业务里处理这些问题,我的基本策略是:把RR事务控制在极短时间,不在事务里做远程调用或耗时计算;大批量更新尽量拆成小批提交,避免一次性生成海量undo log;针对history list length配置监控,超过阈值马上排查长事务。这些措施比单纯调大缓冲池更管用。
5.5 MySQL面试中关于MVCC的高频问法整理
结合最近大家在面试和热词里反复搜的内容,我总结几个高频问题,每个问题都能用本文某一部分回答:
| 面试问题 | 答题要点 |
|---|---|
| MVCC是怎么实现的? | 隐藏列 + undo log版本链 + ReadView可见性判断 |
| 为什么RC会出现不可重复读,RR不会? | ReadView生成时机:RC每次快照读生成一次,RR复用第一次的ReadView |
| MVCC能解决幻读吗? | 快照读可以,当前读要靠Next-Key Lock,RR下两者结合才完整 |
| ReadView判断规则有哪些? | 自己改的可见;小于min_trx_id的可见;大于等于max的不可见;区间内查是否活跃 |
| 事务回滚怎么实现? | 通过undo log,版本链本身就是回滚的数据基础 |
| 长事务对MVCC有什么影响? | 固定ReadView,阻塞undo log清理,导致history list length飙高 |
关于第一个问题,我建议回答时不要只背术语,而是顺手画一条版本链,从当前行指向undo log里的旧版本,讲清楚DB_TRX_ID和DB_ROLL_PTR怎么配合。面试官想听到的是你真理解这个结构,不是只会说“多版本控制”四个字。
5.6 最后两个平时容易忽略的操作细节
做实验时如果把会话A开着不提交,然后直接在会话B里执行DDL,比如ALTER TABLE给表加列,会大概率碰上Waiting for table metadata lock,因为A的快照读持有表元数据锁。这不是MVCC本身的问题,而是MDL锁与长事务叠加的经典场景,处理方式就是找到阻塞的会话并提交或KILL。排查命令是:
sql复制SELECT * FROM information_schema.innodb_trx\G
SELECT * FROM performance_schema.metadata_locks\G
另外,生产环境调隔离级别时要注意连接池里的连接可能仍然保留旧会话级别设置。单纯执行SET GLOBAL transaction_isolation='READ-COMMITTED';只对之后新建的连接生效,已存在的连接池连接不会感知变化。稳妥做法是在连接初始化时显式执行SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;,或者确认连接池的配置项已经同步修改,否则你会发现线上好多连接仍在RR下运行,排查半天也找不到原因。
我个人在实际操作中的体会是,MVCC看起来概念不多,但一旦把它和隔离级别、当前读音、间隙锁、长事务这几个点串起来,整个InnoDB并发控制体系就通了一大半。很多线上诡异问题,比如同一个事务里查不到刚同步过来的数据、批量更新行数不对、undo log只增不减,追根溯源都能落到ReadView的生成时机和版本链保留策略上。建议你拿到一张测试表,把上面几个实验亲手跑一遍,比单纯背十遍原理都管用。
