MVCC 可重复读 真的解决了幻读?

2 个回答

  • 澜夜师兄
    勇敢一点吧!我们不会比今年更年轻了。
    没有彻底解决幻读。如果要**彻底避免幻读**,需要使用 **`SERIALIZABLE`** 隔离级别。 #### **1. 什么是幻读?** 幻读(Phantom Read)指的是**同一个事务内,多次查询相同的范围数据时,第二次查询时发现了第一次没有的数据**,通常发生在事务的并发执行过程中。 例如: 1. 事务A 先执行 `SELECT * FROM users WHERE age > 10 AND age < 30;` 2. 事务B 插入 `INSERT INTO users (age, name) VALUES (20, 'Alice');` 并提交。 3. 事务A 再次执行相同的查询,发现 `users` 表中多了一条 `age=20` 的记录,造成幻读。 #### **2. RR(REPEATABLE READ)是否解决了幻读?** **InnoDB 在 RR 级别下使用 MVCC 和间隙锁(Gap Lock)部分解决了幻读,但并不能彻底避免所有幻读。** ##### **(1)快照读(Snapshot Read)** - **MVCC 解决了快照读的幻读** - 在 RR 隔离级别下,事务在第一次 `SELECT` 查询时会生成一个**一致性视图**(Snapshot),之后的**所有查询都会基于这个快照**,即使其他事务插入新数据,也不会影响当前事务的快照读结果。 - 这意味着,在 **RR 级别的快照读下,幻读不会发生。** 示例: ```sql -- 事务A(开启事务,进行快照读) START TRANSACTION; SELECT * FROM users WHERE age > 10 AND age < 30; -- 事务B(插入新数据并提交) INSERT INTO users (age, name) VALUES (20, 'Alice'); COMMIT; -- 事务A(再次查询,仍然读取原来的快照,不会发现 Alice) SELECT * FROM users WHERE age > 10 AND age < 30; ``` ✅ **不会发生幻读**,因为 `事务A` 只会看到事务开始时的快照。 ##### **(2)当前读(Current Read)** - **间隙锁(Gap Lock)+ Next-Key Lock 解决部分当前读的幻读** - **当前读** 指的是 `SELECT ... FOR UPDATE`、`UPDATE`、`DELETE` 这些操作,因为它们需要读取最新的数据,因此会加锁。 - 在 **RR 级别下,间隙锁(Gap Lock)可以阻止新记录插入,避免部分幻读。** 示例: ```sql -- 事务A(当前读,加锁查询) START TRANSACTION; SELECT * FROM users WHERE age > 10 AND age < 30 FOR UPDATE; -- 事务B(尝试插入一条符合范围的记录) INSERT INTO users (age, name) VALUES (20, 'Alice'); -- 阻塞 -- 事务A 提交后,事务B 才能执行插入 COMMIT; ``` ✅ **不会发生幻读**,因为间隙锁锁住了 `10 < age < 30` 这个区间,使事务B的插入被阻塞,直到事务A提交。 #### **3. RR 仍然无法避免的幻读情况** 虽然 MVCC 解决了**快照读的幻读**,间隙锁解决了**部分当前读的幻读**,但在某些场景下,RR 仍然不能完全避免幻读。例如: ##### **(1)事务中既有快照读,又有当前读** 如果事务在执行期间,**先进行快照读(普通 `SELECT`),再进行当前读(`SELECT ... FOR UPDATE` 或 `UPDATE`)**,就可能发生幻读。 示例: ```sql -- 事务A(快照读) START TRANSACTION; SELECT * FROM users WHERE age > 10 AND age < 30; -- 事务B(插入新数据) INSERT INTO users (age, name) VALUES (20, 'Alice'); COMMIT; -- 事务A(当前读) SELECT * FROM users WHERE age > 10 AND age < 30 FOR UPDATE; ``` 🔴 **会发生幻读**,因为前面的快照读没有加锁,后面的当前读会看到 `Alice` 这条新数据。 ##### **(2)更新操作(UPDATE 语句)** 在 RR 级别下,`UPDATE` 语句是 **当前读**,会看到已提交的最新数据,因此可能导致幻读。 示例: ```sql -- 事务A(查询 age 在 10 到 30 之间的记录) START TRANSACTION; SELECT * FROM users WHERE age > 10 AND age < 30; -- 事务B(插入新数据) INSERT INTO users (age, name) VALUES (20, 'Alice'); COMMIT; -- 事务A(更新 age=20 这条记录) UPDATE users SET name = 'Bob' WHERE age = 20; ``` 🔴 **会发生幻读**,因为 `UPDATE` 是当前读,它会看到事务B插入的 `Alice`,导致事务A出现幻读。 #### **4. 如何彻底解决幻读?** 如果要**彻底避免幻读**,需要使用 **`SERIALIZABLE`** 隔离级别,它会**强制事务串行执行**,使并发事务互斥,避免任何幻读的情况。 ```sql SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; ``` 但 `SERIALIZABLE` 会导致性能下降,通常不建议使用。 #### **5. 总结** | **读类型** | **是否会发生幻读?** | **解决方案** | |------------|------------------|------------| | **快照读(普通 `SELECT`)** | ❌ 不会发生 | MVCC 解决 | | **当前读(`SELECT ... FOR UPDATE`)** | ❌ 不会发生 | 间隙锁(Gap Lock) | | **事务中先快照读,再当前读** | ✅ 可能发生 | 避免事务中混用快照读和当前读 | | **更新操作(`UPDATE`)** | ✅ 可能发生 | 需要 `SERIALIZABLE` 级别 | - **快照读(普通 `SELECT`)不会发生幻读**,因为 InnoDB 的 **MVCC** 机制确保了事务内的**一致性视图**。 - **当前读(`SELECT ... FOR UPDATE`)通常不会发生幻读**,因为 **间隙锁** 阻止了新数据插入。 - **如果事务内既有快照读,又有当前读,或使用 `UPDATE` 操作,可能仍然会发生幻读。** - **想要彻底避免幻读,需要使用 `SERIALIZABLE`,但会降低并发性能。** **结论:** ✅ **InnoDB 的 RR 通过 MVCC + 间隙锁 解决了大部分幻读问题,但在特定情况下(如事务中混用快照读和当前读)仍然可能出现幻读。要彻底避免幻读,需要使用 SERIALIZABLE 级别。**
    编辑于 2024-12-01 12:22 •IP属地 美国
    举报
    评论
    收藏

  • ketty
    不必太张扬,是花自然香。
    ### next-key锁+MVCC 在隔离级别为RR时,MySQL已经可以避免脏读和重复读,但还是无法避免幻读,MySQL采用next-key锁与MVCC(多版本并发控制)来避免幻读. 那么next-key锁是如何避免幻读的呢,我们不妨先想想什么时候可能出现幻读? 即插入到我们查询范围内的空隙中的时候.举个例子,假设我们有一查询语句SELECT * FROM child WHERE id > 100,此时如果另一个事务向100之后插入一条记录的话,此事务再select就会出现幻读了.再比如SELECT * FROM child WHERE id BETWEEN 80 AND 100,此时如果向[80,100]之间插入数据就会出现幻读. > To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row locking with gap locking. InnoDB performs row-level locking in such a way that when it searches or scans a table index, it sets shared or exclusive locks on the index records it encounters. Thus, the row-level locks are actually index-record locks. In addition, a next-key lock on an index record also affects the “gap” before that index record. That is, a next-key lock is an index-record lock plus a gap lock on the gap preceding the index record. If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order. > 为了防止幻像,InnoDB使用一种名为 next-key 锁定的算法,该算法将 index-row 锁定与间隙锁定相结合。 InnoDB以这样的方式执行 row-level 锁定:当它搜索或扫描 table 索引时,它会在它遇到的索引记录上设置共享锁或独占锁。因此,row-level 锁实际上是 index-record 锁。另外,索引 record 上的 next-key 锁也会影响索引 record 之前的“间隙”。也就是说,next-key 锁是一个 index-record 锁加上在 index record 之前的间隙上的间隙锁。如果一个 session 在索引中的 record R上有共享或独占锁定,则另一个 session 不能在R之前的R之前的间隙中插入新的索引 record。 文档中不但解释清楚了如何避免幻读,还对next_key下了一个定义,即next-key 锁是一个 index-record 锁加上在 index record 之前的间隙上的间隙锁. 这里出现幻读的条件无非有三种,next_key锁都可以很好的解决幻读 1. where id > n; 会在实际存在的最后一个值上加(N_max, positive infinity)这个gap锁 其他的值加next_key锁 2. BETWEEN lhs AND rhs; 在扫描每一个值时对其加index-record 锁,然后在对这个index的前面的间隙加gap锁 3. where id < n; 会在实际存在的最小值上加 (negative infinity, N_min],其他的值加next_key锁 其实也很好想,当我们select的时候,因为MVCC的特性使得我们根本不需要锁,因为MVCC所加的记录删除时间的列会帮我们筛选掉幻读的行,从而在不加锁的情况下避免幻读.但是此时数据仍然是可以加入表的.但是当我们需要对表进行修改的时候就不一样了,此时MVCC显然无法满足要求,我们需要保证在一个区间插入的时候其他会话不在此区间进行插入,所采取的策略就是next_key锁,关于next_key锁,上面讲的很清楚了.
    发布于 2022-03-24 23:41 •IP属地 云南
    举报
    评论
    收藏