尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

我明明没查到这行,却把它改了:RR 下的幻读到底解决了没有

我明明没查到这行,却把它改了:RR 下的幻读到底解决了没有 关于 MySQL 隔离级别几乎每篇八股文都会给你同一张表读未提交、读已提交、可重复读、串行化横着四列打勾打叉。表的最后一格通常写着一句话InnoDB 在可重复读RR下通过 MVCC 临键锁基本解决了幻读。大部分人背到这里就停了。可这个「基本」是什么意思哪些情况解决了哪些没有如果面试官顺着这两个字往下追问一句背表格的人立刻就没词了。这篇文章不再重复那张表。我们直接开一个 MySQL 8.0 容器用两个会话跑几段真实的 SQL把那个「基本」里藏着的东西逼出来。所有输出都是实机跑出来的环境是mysql:8.0.46默认隔离级别就是REPEATABLE-READ你可以完整复现。一、先看一个自相矛盾的事务先建一张最简单的表CREATETABLEt(idINTPRIMARYKEY,nameVARCHAR(20),ageINT)ENGINEInnoDB;INSERTINTOtVALUES(1,lin,18),(2,zhou,20);现在开两个会话。事务 A 在 RR 下反复查询age 19的人中间事务 B 插入一行age 25并提交。时刻事务 ARR事务 BT1BEGIN;SELECT * FROM t WHERE age 19;T2INSERT INTO t VALUES (3,chen,25);T3SELECT * FROM t WHERE age 19;T4UPDATE t SET age age 100 WHERE age 19;T5SELECT * FROM t WHERE age 19;跑出来的结果是这样的A1: first snapshot read ---------------- | id | name | age | ---------------- | 2 | zhou | 20 | ---------------- A2: snapshot read again, after B committed ---------------- | id | name | age | ---------------- | 2 | zhou | 20 | ---------------- A3: run a current read -- UPDATE t SET age age 100 WHERE age 19 A4: snapshot read after the UPDATE ---------------- | id | name | age | ---------------- | 2 | zhou | 120 | | 3 | chen | 125 | ----------------请仔细看这段输出里最别扭的地方。A2 是符合预期的B 已经提交了id 3但 A 看不到它这正是 RR 该有的样子 —— 同一个事务里两次查询结果一致。真正诡异的是 A4。id 3出现了而且它的age已经不是 B 写进去的 25而是125。也就是说事务 A 从头到尾没有任何一次查询「看见」过这一行却结结实实地把它改了一遍。二、为什么会这样两种读两套规则要解释这个矛盾得先接受一件事InnoDB 里的「读」不是一种操作是两种。普通的SELECT叫快照读。它不加锁读的是 MVCC 生成的一致性视图 —— 你可以理解为事务开始时给整个数据库拍了一张照片之后无论别人怎么改你看的都是这张照片。而UPDATE、DELETE、SELECT ... FOR UPDATE这类语句叫当前读。它们必须读到最新的已提交数据否则就会出大问题想象一下如果UPDATE也基于旧照片去改数据那别人刚提交的修改就会被你悄悄覆盖掉。所以当前读绕开快照直接读最新版本并且加锁。现在回头看那段输出因果链就清楚了A2 是快照读B 插入的行trx_id不在 A 的视图范围内不可见A3 的UPDATE是当前读它看到的是最新数据于是锁住并修改了id 3关键的一步 —— 这行被 A 修改之后它的trx_id变成了A 自己的事务 ID而「自己改过的行对自己一定可见」是 MVCC 的基本规则所以 A4 的快照读里它现身了。一句话记住幻行不是被 A「看见」的是被 A自己改出来的。当前读像一只伸进照片外面的手把外面的东西抓了进来。三、再看一个更干净的对照只读不改上面那个例子里UPDATE同时做了两件事读最新版 改数据容易让人混淆。我们把「改」去掉只保留当前读看看会怎样。这次事务 E 中间执行的是SELECT ... FOR UPDATE它加锁、读最新数据但一个字节都不改E2: snapshot read after F committed ---------------- | id | name | age | ---------------- | 2 | zhou | 20 | ---------------- E3: current read via FOR UPDATE (no data change) ---------------- | id | name | age | ---------------- | 2 | zhou | 20 | | 9 | sun | 40 | ---------------- E4: plain snapshot read again ---------------- | id | name | age | ---------------- | 2 | zhou | 20 | ----------------这段输出值得裱起来。同一个事务里三条查询条件完全相同的语句结果是「一行、两行、一行」。id 9在FOR UPDATE里出现紧接着的普通SELECT里又消失了。因为这次没有人修改它它的trx_id还是事务 F 的对 E 的视图依然不可见。对比第一个实验正好印证了前面的推理幻行能否留在快照里取决于它有没有被你自己改过。四、顺带一个坑快照到底什么时候建立很多人以为BEGIN一执行照片就拍好了。实际上不是。在 RR 下一致性视图是在事务里第一条快照读语句执行时才建立的。也就是说BEGIN之后如果你迟迟不读这段空窗期里别人提交的数据你照样看得见BEGIN;-- 什么都没发生-- 此时事务 D 插入 id4 并提交SELECT*FROMtWHEREage19;-- 第一次读照片在这一刻才拍C: first read happens AFTER D committed ---------------- | id | name | age | ---------------- | 2 | zhou | 120 | | 3 | chen | 125 | | 4 | wu | 30 | ----------------id 4是在BEGIN之后才提交的C 依然读到了它。如果你确实需要「进事务的那一刻就冻住」得用另一条语句STARTTRANSACTIONWITHCONSISTENTSNAPSHOT;同样的时序下换成它后来提交的那行就再也读不到了。这个差别在做数据一致性快照、对账、导出的场景里是会出事的。容易踩的地方「我都BEGIN了怎么还会读到新数据」—— 因为冻结的起点是第一次快照读不是BEGIN。五、回到那句「基本挡住」现在可以给开头那两个字一个准确的解释了纯快照读的事务RR 下确实观察不到幻读。这部分是 MVCC 的功劳八股说得没错。一旦事务里混入当前读UPDATE、DELETE、FOR UPDATE、LOCK IN SHARE MODE幻行就可能显形。这就是「基本」二字让掉的部分。临键锁next-key lock解决的是另一半问题它锁住索引记录之间的间隙阻止别人往你扫描的范围里插数据。注意它靠的是加锁和 MVCC 是两套独立机制别混为一谈。实践上怎么办如果一段业务逻辑里既要查、又要基于查询结果改第一次查询就用FOR UPDATE把范围锁住别指望快照读的结果在后面还成立需要严格串行语义的场景对账、扣减库存的强一致路径老老实实上SERIALIZABLE或者用唯一索引、乐观锁把约束做在数据层不要把「RR 解决了幻读」当成可以放心并发的许可证。一句话回顾RR 下的快照读确实不会幻读但当前读会绕过快照读到最新数据被你自己改过的幻行从此就留在了你的视野里 —— 这才是「基本挡住」那两个字的全部含义。
返回列表