AIGC标识 MySQL 可重复读到底防不防幻读?2 个事务同屏跑给你看

同一个可重复读事务,普通 SELECT 连续两次都读到 3 行。

换成 SELECT ... FOR UPDATE,同一时刻读到 4 行。

事务 B 再插第 5 行,被卡住了。

这不是页面算错。

MySQL 的 RR 有两套读法:快照读和当前读。

我把两套读法做成一个单文件网页,左右两个事务同屏操作。

14 条自检全过,18 组场景和一份独立 Python 状态机逐项对上。

图 1:MySQL 事务隔离级别可视化实验台首屏。顶部是四种隔离级别按钮,默认选中可重复读 RR;中间左右两栏分别是事务 A 和事务 B,每栏都有 BEGIN、SELECT、UPDATE、INSERT、SELECT FOR UPDATE、COMMIT、ROLLBACK 按钮;下方是 users 表、行版本号、锁状态和事件时间线,底部是 14 条自检结果

一、为什么值得看:RR 防的是“快照里的幻读”

SQL 标准里,RR 只要求挡住脏读和不可重复读,幻读是可选。

MySQL 的 InnoDB 默认用 RR,又往前做了一步。

普通 SELECT 走快照读,整个事务看同一份快照。

别人后来插入的行,快照里没有,所以你连续查两次都是 3 行。

SELECT ... FOR UPDATE 走当前读,读最新已提交版本。

这时它能看见新插入的第 4 行。

看见还不算完,它还会加记录锁和间隙锁。

间隙锁的作用,是让别的事务无法在这个区间里插入第 5 行。

所以“RR 防不防幻读”不能一句话回答。

问的是普通 SELECT,答案是不出现幻读。

问的是当前读,答案是能看见新行,但并发插入会被挡住。

读法 读什么 新插入的行 并发插入
普通 SELECT,快照读 事务开始时的快照 看不到 不拦
SELECT ... FOR UPDATE,当前读 最新已提交加本事务修改 看得到 间隙锁拦住

二、这个页面能做什么

页面顶部是四种隔离级别。

中间左右两栏是事务 A 和事务 B。

每栏都能 BEGIN、SELECT、UPDATE、INSERT、SELECT ... FOR UPDATE、COMMIT、ROLLBACK。

下面三块分别是 users 表、行版本号、锁状态和事件时间线。

右上角有四个一键场景,分别演示脏读、不可重复读、RC 幻读、RR 快照读与当前读。

还有一条“运行自检”,逐条跑 14 个断言。

整个页面一个 HTML 文件,不引 CDN,不联网。

三、四个场景,数字怎么走

先看脏读。

隔离级别切到 RU。

A 把 id=1 的 age 从 30 改成 99,还没提交。

B 执行 SELECT,读到 99。

表里这一行标着“未提交(A)”,可 B 的快照里其实还是 30。

这就是脏读:读到了别人没提交的值。

图 2:读未提交场景。事务 A 更新 id=1 的 age 为 99 但未提交,事务 B 的 SELECT 读到 age=99,users 表里这一行标着“未提交(A)”,版本号升到 v2;事务 A 的锁状态显示记录锁 id=1

RC 下,脏读没了。

A 先读 id=1,得到 30。

B 改成 50 并提交。

A 再读,得到 50。

同一个事务,两次读不一样,这是不可重复读。

图 3:读已提交场景。事务 A 第一次 SELECT 读到 age=30,事务 B 把 age 改成 50 并提交,事务 A 第二次 SELECT 读到 age=50;右侧时间线按顺序记录 10 次到 50 的变化

RC 下的范围查询也会变。

A 第一次 SELECT *,3 行。

B 插入 Dave,id=4,提交。

A 再 SELECT *,4 行。

这是幻读。

RR 下,普通 SELECT 不再变。

A 第一次 SELECT *,3 行。

B 插入 Dave 并提交。

A 再 SELECT *,还是 3 行。

因为 A 一直读 BEGIN 时的快照 v1。

接着 A 执行 SELECT ... FOR UPDATE,范围 [1,10]。

这次读到 4 行,Dave 出现了。

同时锁状态里多了 4 个记录锁和一个间隙锁 [1,10]。

if (iso === 'RU') rows = readRU(t, id);
else if (iso === 'RC' || iso === 'SER') rows = readRC(t, id);
else rows = readSnap(t, id);

图 4:RR 快照读与当前读对比。事务 A 的快照版本 v1 只有 3 行,普通 SELECT 两次都读到 3 行;执行 SELECT FOR UPDATE 后当前读到 4 行,Dave 出现在结果里;锁状态显示记录锁 id=1 到 id=4,以及间隙锁 [1,10]

B 再 BEGIN,插入 Eve。

页面算出新 id 是 5,正好落在 [1,10] 里。

INSERT 被挡住,日志给出中文提示:

“插入位置 id=5 落在事务 A 的间隙锁 [1,10] 内,INSERT 阻塞等待锁释放”。

表里没有第 5 行,B 的状态还是“进行中”。

const gap = findGapLock(other, newId);
if (gap) return finish(t, blocked(`插入位置 id=${newId} 落在事务 ${other} 的间隙锁 [${gap.low},${gap.high}] 内,INSERT 阻塞等待锁释放`), sql);

图 5:间隙锁失败路径。事务 B 插入 id=5 的 Eve,日志里先显示 INSERT 语句,再给出红色提示“插入位置 id=5 落在事务 A 的间隙锁 [1,10] 内,INSERT 阻塞等待锁释放”;users 表仍只有 4 行,事务 A 的间隙锁 [1,10] 还在

四、验证与踩坑

我在本地起了静态服务,把 5 个场景逐个点了一遍。

python -m http.server 8080

页面自检 14/14 通过。

这 14 条是页面自己写的,不能自己证明自己。

我另写了一份 Python 状态机,按同一套规则重算 18 组场景。

18/18 和页面一致。

对比项包括读到的 age、行 id、版本号、是否阻塞、是否报错。

场景 页面读到 Python 读到
RU 脏读 99 99
RC 不可重复读 30 然后 50 30 然后 50
RR 可重复读 30 然后 30 30 然后 30
RC 幻读 3 行然后 4 行 3 行然后 4 行
RR 快照读 3 行然后 3 行 3 行然后 3 行
RR 当前读 4 行 4 行
SER 当前读 50 50

这一步抓出了一个真问题。

SERIALIZABLE 下,普通 SELECT 最初复用了 RR 的快照读。

A 先 BEGIN,B 改成 50 并提交,A 再 SELECT,页面还显示 30。

按 InnoDB 的规则,串行化下普通 SELECT 会转成加共享锁的当前读,应该看到 50。

我把 SER 分支改成 readRC,并补了两条自检。

一条验证 SER 当前读能看到新值,一条验证共享锁能挡住 B 的 UPDATE。

自检从 12 条变成 14 条。

第二个小问题是被阻塞的 INSERT 只显示原因,不显示 SQL。

补上 SQL 后,截图 5 才能一眼看出是哪条语句被挡。

失败路径也试了。

没 BEGIN 就 COMMIT,提示“事务未开始,无法 COMMIT”。

没 BEGIN 就 SELECT,提示“事务未开始,请先执行 BEGIN”。

事务进行中重复 BEGIN,提示“事务已在进行中,请先 COMMIT 或 ROLLBACK”。

五、结果与边界

这个页面能回答三件事。

普通 SELECT 和当前读分别看到什么。

RC 和 RR 的差别到底落在哪一行。

间隙锁为什么会挡住 INSERT。

它不能替代真库。

页面没有连 MySQL,跑的是按 InnoDB 规则写的状态机。

undo log 清理、锁等待超时、死锁检测、二级索引上的 next-key 锁,这些都没建模。

页面里的间隙锁是一个连续区间 [1,10]。

真实 InnoDB 的 next-key 锁是按索引记录一段一段加的,间隙不一定连续。

自动递增 id 也做了简化,按当前最大 id 加一。

这些边界不影响“快照读对当前读”的结论,但拿它当线上诊断工具会不够。

六、使用码道体会

码道有三种使用方式:WebUI、TUI 和桌面 IDE。

这次用的是 WebUI。

我把状态模型、四种隔离级别、四个场景、锁状态、自检都写进需求。

它生成了一个 571 行的单文件 index.html。

结构基本对,省事的是表数据、版本号、锁状态、时间线都做进了同一屏。

但 AI 写的自检会跟着 AI 自己的错误走。

SER 分支就是例子:页面能跑,14 条里最初也没有一条能抓住它。

所以我现在的做法是,需求里要求页面自检,交付后再写一份独立实现复算。

页面自检负责回归,独立复算负责抓第一版的错。

七、准备环境:打开码道 Web

浏览器打开码道 Web 版: https://devcloud.cn-north-4.huaweicloud.com/chat?source=dmzntgwlb&sourcead=dmzntgwlb1&invitecode=INV-IXhxk5bwWx_qKSP1FcgSuiOEf6ruDXtjvLFiCH9nMs5dZGLapaSSylwt-cKRj5LoT3MlxikWxy8VhqplfvtQjefQOq31Tbp3R3wYP3K535Cn4XTaUzqqExKPYTWvvw4LijV14IS1torXdTGp7oyXb6SR5VxgYTg50jM ,登录后就能在对话窗口输入需求,不需要装软件。

八、总结

RR 下普通 SELECT 连续两次读到 3 行,不是幻读被彻底消灭。

它读的是事务开始时的快照。

当前读能看见别人后来提交的第 4 行。

间隙锁再把第 5 行挡在门外。

三件事分开看,RR 的“防幻读”就清楚了。

页面和源码在 Demo Park:https://atomgit.com/deli007/demo_park/tree/main/codearts-mysql-isolation-lab

posted @ 2026-11-05 08:11  新科技观察员  阅读(3)  评论(0)    收藏  举报