MySQL可重复读的级别下,幻读本质问题
当前读,是自己的事务插入了数据或者修改数据之后会出现幻读,还是其他事务在本事务执行之间插入或者修改了数据出现了幻读?以及,为什么别的或者事务插入了数据,自己的视图会改变?不会整个事务期间都是用开始的时候的ReadView吗?按道理来说,就算别的事务插入了新的数据,那个数据的版本号应该算是在本事务之后的或者是还未提交的事务,不应该被看到呀?
👏 非常棒的问题!
你问的这几句其实正好切中 MySQL 幻读、本事务视图机制(Read View)与当前读区别 的核心。
我们来分层、精准地解释这一切👇
一、先明确:幻读是谁造成的?
✅ 幻读是由“其他事务”在本事务执行过程中插入或删除了符合查询条件的数据造成的。
不是自己改的,是别人改的。
举例👇
时间 | 事务A | 事务B |
---|---|---|
t1 | BEGIN; | |
t2 | SELECT * FROM user WHERE age > 20; (快照读) | |
t3 | INSERT INTO user(age) VALUES(25); COMMIT; | |
t4 | SELECT * FROM user WHERE age > 20; 再次查询 | ← 发现多了一行(id=新插入) |
事务A第二次查到了之前没有的行,这就是幻读。
而这个新行来自事务B的插入操作。
二、为什么“可重复读”能防止这种情况?
(1)可重复读的核心机制是 MVCC + ReadView
当事务A执行第一次快照读(普通 SELECT
)时,InnoDB 会创建一个 Read View(读视图),记录:
- 当前系统中 哪些事务已提交;
- 当前事务自己的ID;
- 哪些事务还没提交(活跃事务列表 active_trx_ids)。
之后,事务A 的每次普通 SELECT
都基于这个 Read View,只能看到:
在创建 Read View 时已经提交的事务的数据。
(2)所以,普通 SELECT
(快照读)不会看到其他事务后来插入的数据
即使事务B在 t3 时插入并提交了新数据,它的事务ID > 事务A 的 Read View 中最大ID,因此:
事务A 看不到它(快照读不会幻读)。
三、那幻读是怎么“出现”的?
关键在于:
不是所有的查询都使用 Read View。
⚠️ 当前读(current read)不使用快照,而是直接读最新已提交数据。
举例理解
-- 事务A
START TRANSACTION;
SELECT * FROM user WHERE age > 20 FOR UPDATE;-- 事务B
INSERT INTO user(age) VALUES(25);
COMMIT;-- 事务A 再执行
SELECT * FROM user WHERE age > 20 FOR UPDATE;
🔹 在第一次 FOR UPDATE
时,InnoDB 会对符合条件的行加锁(记录锁 + 间隙锁)。
🔹 但如果锁范围没有涵盖整个条件范围(或有间隙未锁住),事务B 可能成功插入一条新记录。
🔹 当事务A再次执行 FOR UPDATE
(当前读)时,会读最新数据,就会看到那条插入的新记录 —— 幻读出现。
四、那为什么当前读能看到“新行”?难道 Read View 不起作用了吗?
完全正确:
✅ 当前读(Current Read)不使用 Read View,而是直接去读取最新已提交的数据版本。
- 快照读(Snapshot Read):用 Read View → 读旧版本,不会变。
- 当前读(Current Read):不用 Read View → 读最新版本,可能变。
这就是为什么:
在同一个事务中,快照读看到的数据不会变,但当前读看到的数据可能不同。
五、总结成一句话 🔥
类型 | 是否使用 ReadView | 是否加锁 | 是否可能幻读 | 场景 |
---|---|---|---|---|
快照读(普通SELECT) | ✅ 是 | ❌ 否 | ❌ 不会 | 读操作 |
当前读(SELECT … FOR UPDATE / UPDATE / DELETE) | ❌ 否 | ✅ 是 | ✅ 可能(若锁不全) | 写操作 |
所以——
“幻读”一定是由于 其他事务插入/删除 引起的;
而且只会发生在 当前读 中,因为它不受 Read View 限制。
在这样的一种情况下,一开始该条记录并不在我的readview中,之后我更新这条记录(此时已经提交了),因为我读是看不到这条数据的,更新之后,trx等于我自己的事务id了,之后我就能看到该条数据了,这也是一个幻读情况吧
您描述的这个情况,确实是 MySQL 可重复读(Repeatable Read)隔离级别下一种典型的幻读现象。它精准地指出了“快照读”和“当前读”混合使用时可能出现的边界情况。
为了更清晰地理解整个过程,我们可以先通过一个表格来回顾“快照读”与“当前读”的核心区别,这正是理解该问题的关键。
🔍 场景逐步分析
您描述的情况完全可以被称为“幻读”,因为它符合幻读的定义:在同一个事务中,连续两次查询看到了之前不存在的数据行(幻影行)。我们来分解一下这个过程的每一步:
- 初始状态:您的事务(事务A)开启,并创建了 ReadView。此时,另一个事务(事务B)插入了一条新记录(比如 id=5)并提交。由于事务B的提交发生在您的事务开始之后,其事务ID大于您 ReadView 中的 max_trx_id,因此这条新记录对您的事务不可见。
- 第一次快照读:您执行 SELECT * FROM table WHERE …。这是一次快照读,基于 ReadView 进行判断,自然看不到事务B插入的 id=5 这条记录。结果集是空的或是固定的N条。
- 执行更新(当前读):您执行 UPDATE table SET … WHERE id = 5。UPDATE 操作属于当前读。它不会使用 ReadView,而是直接去查找数据页上 id=5 的最新版本。它找到了事务B插入并已提交的记录。于是,InnoDB 会将这条记录的 trx_id 修改为您当前事务A的ID,并将旧版本写入 undo log。这个操作被称为“数据版本的可见性转换”——一条原本对您不可见的记录,通过您的更新操作,变成了您事务的一部分。
- 第二次读:您再次执行查询。此时,无论是快照读还是当前读,都能看到这条记录了。因为快照读的规则之一是:事务总是可以看到自己在本事务内修改过的数据(即 trx_id 等于当前事务ID的记录)。
所以,在整个事务中,您先是没有看到某条记录,然后通过自己的更新操作又能看到它了,这完全符合“幻读”的特征。
💡 如何避免这类幻读?
MySQL 提供了机制来防止这种情况,核心在于在事务开始时就用“当前读”锁定可能产生幻影行的范围。
• 核心方案:使用加锁读(Locking Read)
在您事务的第一次查询时,如果这是一个关键操作,需要绝对保证数据一致性,就不要使用普通的 SELECT,而是使用 SELECT … FOR UPDATE。
START TRANSACTION;
- 使用当前读并加锁,锁定 id>=5 的间隙,防止其他事务插入
SELECT * FROM table WHERE id >= 5 FOR UPDATE;
– … 其他操作
COMMIT;
• 工作原理:SELECT … FOR UPDATE 会对查询匹配的记录以及记录之间的间隙(Gap) 加锁(这种锁叫 Next-Key Lock)。这就像您提前宣告了“id>=5 的这个范围被我占用了”,其他事务试图在这个范围内插入新记录(比如 id=5)的操作会被阻塞,直到您的事务提交。这样就从根本上杜绝了其他事务插入您“看不见但又能更新到”的幻影行。
💎 总结
您的理解是完全正确的。这正是一个经典的幻读场景,其根源在于:
• 快照读:基于 ReadView,提供一致性视图,避免不了写操作带来的幻读。
• 当前读:基于最新数据,用于更新操作,但会“看到”其他事务提交的新数据。
要避免它,需要在事务中正确使用 SELECT … FOR UPDATE 这样的加锁语句,利用 InnoDB 的 Next-Key Locking 机制来锁定数据范围。