【Mysql系列】Mysql 多级隔离级别揭秘
目录
一、什么是隔离级别
1.1、为什么复合操作需要事务?
1.2、事务的 ACID 特性如何保障操作可靠性?
1.3、隔离性通过隔离级别来控制
二、为什么用多级隔离级别
2.1、事务并发执行时可能引发以下问题
2.1.1、脏读(Dirty Read)- 读未提交
2.1.2、不可重复读(Non-Repeatable Read)- 读已更新
2.1.3、幻读(Phantom Read)- 读已新增已删除
2.2、SQL 标准定义的四级隔离级别(从低到高)
三、Mysql-innodb RC隔离级别解决幻读底层流程揭秘
3.1、模拟RC和RR隔离的事务情况
3.2、MVCC实现揭秘
3.2.1、数据版本链结构化表格(从新到旧排序)基于UNDO_LOG
3.2.2、版本链核心机制解析
3.2.2.1、版本链如何形成?
3.2.2.2、版本链的核心作用:支撑 MVCC 并发读
3.2.2.3、版本链的生命周期(undo log 回收)
3.2.3、如何根据undo_log中的版本链进行定位确定读取哪个版本数据呢?
3.2.3.1、核心概念定义
3.2.3.2、核心分工
3.2.3.3、当前读与快照读的本质区别
3.2.3.4、ReadView:可见性判断的 “裁判”
3.3、拿上述模拟RC与RR的事务例子进行说明
3.3.1、例子
3.3.2、RC隔离级别 ReadView 以及规则判断
3.3.3、RR隔离级别 ReadView 以及规则判断
3.3.4、RR存在幻读的特例
一、什么是隔离级别
在 MySQL 中,当需要执行多条 SQL 语句(例如 “先查询数据状态再进行更新”、“跨表关联修改数据” 等场景)时,单条 SQL 的独立执行无法保证整体操作的可靠性。此时需引入事务机制,通过 ACID 特性屏蔽并发业务的干扰,确保操作的原子性与完整性:
1.1、为什么复合操作需要事务?
假设业务场景为 “查询账户余额后进行转账”,若不使用事务:
- 事务 A 查询余额为 1000 元,准备转账 500 元;
- 此时事务 B 同时修改该账户余额为 0 并提交;
- 事务 A 继续执行转账操作,最终余额变为 - 500 元,导致数据错误。
事务的作用:将多条 SQL 语句封装为一个不可分割的整体,要么全部成功,要么全部回滚,避免中间状态被其他业务干扰。MySQL 的事务机制通过 ACID 特性,为多条 SQL 的复合操作提供了 “原子性保护罩”:既屏蔽了并发业务的干扰,又确保了数据在复杂操作中的一致性,是解决 “查询 - 修改” 类业务场景可靠性问题的核心方案。
1.2、事务的 ACID 特性如何保障操作可靠性?
特性 | 含义及作用 | 示例场景 |
---|---|---|
原子性(Atomicity) | 事务中的操作要么全部完成,要么全部回滚,不存在部分成功的状态。 | 转账操作中,扣款与到账必须同时成功,否则回滚至初始状态。 |
一致性(Consistency) | 事务执行前后,数据从一个合法状态变为另一个合法状态,避免逻辑错误。 | 转账后,转出账户减少的金额必须等于转入账户增加的金额,总余额不变。 |
隔离性(Isolation) | 多个事务并发执行时,相互之间不可见、不干扰,通过隔离级别避免脏读、幻读等问题。 | 事务 A 查询余额时,不受事务 B 未提交的修改影响,确保读取的数据是已提交的合法版本。 |
持久性(Durability) | 事务提交后,数据修改永久保存,即使数据库崩溃也能恢复。 | 转账成功后,即使服务器断电,重启后数据依然有效。 |
1.3、隔离性通过隔离级别来控制
隔离性(Isolation) 正是通过设置多级隔离级别来控制不同事务之间的干扰程度。多级隔离级别的设计本质是在 “数据一致性” 和 “并发性能” 之间寻找平衡,不同级别对应不同的并发控制策略,同时来针对解决脏读、幻读等问题。
二、为什么用多级隔离级别
2.1、事务并发执行时可能引发以下问题
2.1.1、脏读(Dirty Read)- 读未提交
事务 A 读取到事务 B 未提交的修改,若 B 回滚,A 读取的数据即为无效。
核心逻辑:
- 事务 B 的更新未提交时,事务 A 若能读到这个 “临时数据”,就会引发脏读;
- 事务 B 回滚后,事务 A 之前读的 “
李四
” 实际不存在,数据一致性被破坏
时间点 | 事务 A 操作 | 事务 B 操作 | stu 表(id=1)数据状态 | 关键问题(脏读风险) |
---|---|---|---|---|
T1 | -(未开始查询) | -(未开始更新) | name='李牛牛' | - |
T2 | -(等待) | update stu set name='李四' where id=1 (执行更新,但未提交) | name='李四' (事务 B 未提交的临时状态) | 事务 B 修改了数据,但还没确认(未提交) |
T3 | select * from stu where id=1 (查询数据) | -(等待) | 读事务 B 未提交的name='李四' | 脏读发生:事务 A 读到了事务 B 未提交的临时数据 |
T4 | -(等待) | rollback (回滚事务,撤销 T2 的修改) | 回滚为name='李牛牛' | 事务 B 的修改被撤销,事务 A 之前读的数据 “无效” 了 |
T5 | -(后续查询 / 业务逻辑) | -(结束) | name='李牛牛' | 若事务 A 基于 T3 的脏数据做业务,会出问题 |
2.1.2、不可重复读(Non-Repeatable Read)- 读已更新
事务 A 多次读取同一数据时,事务 B 在期间提交了修改,导致 A 前后读取结果不一致。
核心逻辑:
- 事务 A 在同一事务内,先读
李牛牛
,后读李四
; - 差异源于事务 B 在事务 A 执行期间提交了修改,破坏了 “同一事务内数据可重复读” 的预期。
时间点 | 事务 A 操作 | 事务 B 操作 | stu 表(id=1)数据状态 | 关键现象(不可重复读) |
---|---|---|---|---|
T1 | -(未开始) | -(未开始) | name='李牛牛' | - |
T2 | select * from stu where id=1 (第一次查询) | -(等待) | name='李牛牛' | 事务 A 拿到初始数据 |
T3 | 处理其他数据(耗时操作) | -(等待) | name='李牛牛' | 数据暂时未变 |
T4 | -(等待) | update stu set name='李四' where id=1 (执行更新) | name='李四' (事务 B 修改后) | 事务 B 修改了数据,但未提交 |
T5 | -(等待) | commit (提交事务 B) | name='李四' (已提交) | 事务 B 的修改生效,数据永久改变 |
T6 | select * from stu where id=1 (第二次查询) | -(结束) | name='李四' | 不可重复读发生:同事务内两次查询结果不同 |
2.1.3、幻读(Phantom Read)- 读已新增已删除
事务 A 按条件查询数据时,事务 B 插入了符合条件的新数据,导致 A 再次查询时结果集 “幻觉” 般新增记录。
核心逻辑:
1.事务 A 先查询 “无数据”→执行删除→再查询 “有数据(李牛牛
)”;
2.差异源于事务 B 在事务 A 执行期间插入并提交了新数据,让事务 A 产生 “数据幻觉”。
时间点 | 事务 A 操作 | 事务 B 操作 | stu 表数据状态 | 关键现象(幻读) |
---|---|---|---|---|
T1 | -(未开始) | -(未开始) | 空表(假设初始无数据) | - |
T2 | select * from stu (第一次查询) | -(等待) | 空表 | 事务 A 看到 “没有数据” |
T3 | delete from stu (删除操作,实际无数据可删) | -(等待) | 空表 | 逻辑上 “删除所有数据”,但无实际效果 |
T4 | -(等待) | insert into stu value(2, '李牛牛') (插入数据) | 插入后:id=2, name='李牛牛' (事务 B 未提交) | 事务 B 新增数据,但未提交 |
T5 | -(等待) | commit (提交事务 B) | 数据变为:id=2, name='李牛牛' (已提交) | 事务 B 的插入生效,数据永久存在 |
T6 | select * from stu (第二次查询) | -(结束) | id=2, name='李牛牛' | 幻读发生:同事务内前后查询数据量 / 内容突变 |
多级隔离级别的存在,正是为了通过不同策略解决上述问题,同时避免过度锁定导致性能下降。
2.2、SQL 标准定义的四级隔离级别(从低到高)
隔离级别 | 解决的问题 | 并发性能影响 | MySQL 默认级别(InnoDB) |
---|---|---|---|
读未提交(Read Uncommitted) | 不解决任何问题,可能出现脏读、不可重复读、幻读 | 最高(几乎无锁定) | 一般不使用 |
读已提交(Read Committed, RC) | 解决脏读,仍可能出现不可重复读、幻读 | 较高(行级锁,提交后释放) | 是(大多数业务场景) |
可重复读(Repeatable Read, RR) | 解决脏读、不可重复读,仍可能出现幻读(Innodb除外) | 中等(行级锁 + 间隙锁,防止数据修改) | 可配置使用 |
可串行化(Serializable) | 解决所有并发问题(脏读、不可重复读、幻读) | 最低(完全串行化执行) | 仅特殊场景使用 |
三、Mysql-innodb RC隔离级别解决幻读底层流程揭秘
在 MySQL 的 InnoDB 存储引擎里,RC(读已提交)和 RR(可重复读)这两种隔离级别,借助 MVCC(多版本并发控制)机制来处理并发事务。
3.1、模拟RC和RR隔离的事务情况
时间点 | 事务 A(trx_id=1) | 事务 B(trx_id=2) | 事务 C(trx_id=3) | 事务 D(查询事务) | stu 表(id=1)数据版本变化 | 关键现象(RR vs RC) |
---|---|---|---|---|---|---|
T1 | begin update name=' 李牛牛 ' where id = 1 | -(未开始) | -(未开始) | -(未开始) | 事务 A 修改后:版本 1(trx_id=1,name=' 李牛牛 ') | - |
T2 | commit | -(未开始) | -(未开始) | -(未开始) | 版本 1 提交,成为 “已提交版本” | - |
T3 | -(结束) | begin update name=' 李四 ' where id = 1 | -(未开始) | -(未开始) | 事务 B 修改后:版本 2(trx_id=2,name=' 李四 ') | 版本 2 未提交,仅事务 B 可见 |
T4 | -(结束) | 处理其他数据(耗时操作) | -(未开始) | select * from stu where id = 1 | RR和RC读版本1(李牛牛 ) | |
T5 | -(结束) | commit | -(未开始) | 处理其他数据(耗时操作) | 版本 2 提交,覆盖版本 1,成为新 “已提交版本” | - |
T6 | -(结束) | -(结束) | begin update name=' 王五' where id = 1 | 处理其他数据(耗时操作) | 事务 C 修改后:版本 3(trx_id=3,name=' 王五 ') | 版本 3 未提交,仅事务 C 可见 |
T7 | -(结束) | -(结束) | 处理其他数据(耗时操作) | select * from stu where id = 1(第二次查询) | RC读版本2(李四) RR读版本1(李牛牛)-解决不可重复度 | |
T8 | -(结束) | -(结束) | commit | (等待) | 版本 3 提交,覆盖版本 2,成为最新 “已提交版本” | |
T8 | -(结束) | -(结束) | -(结束) | (等待) | 无新修改,版本 3 仍为最新 |
3.2、MVCC实现揭秘
MVCC 的核心逻辑是通过维护数据的多个版本,让不同事务在并发访问时,能基于各自可见的版本读写数据,以此在保证一定数据一致性的同时,提升并发性能 ,平衡了数据一致性需求与系统并发处理能力。InnoDB 的数据版本链使用的是回滚日志(UNDO_LOG)是 MVCC 的底层支撑,通过TRX_ID
和DB_ROLL_PTR
串联历史版本,让读写操作在并发时互不阻塞;而 隔离级别的差异 本质是 “读操作遍历版本链时的规则不同”(快照是否固定、遍历范围如何),最终导致 “可重复读” 或 “不可重复读” 的结果。
3.2.1、数据版本链结构化表格(从新到旧排序)基于UNDO_LOG
版本层级 (新→旧) | id | 姓名 | TRX_ID (修改事务 ID) | DB_ROLL_PTR (回滚指针,指向上一版本) | 版本来源 |
---|---|---|---|---|---|
版本 3 (最新) | 1 | 王五 | 3 | 0x6446123 (指向版本 2) | 事务 3 修改并提交 |
版本 2 | 1 | 李四 | 2 | 0x6346413 (指向版本 1) | 事务 2 修改并提交 |
版本 1 | 1 | 李牛牛 | 1 | 0x77108a12 (指向版本 0) | 事务 1 修改并提交 |
版本 0 (最旧) | 1 | 李牛牛 | null | null | 初始数据(无事务修改) |
3.2.2、版本链核心机制解析
3.2.2.1、版本链如何形成?
-
每次事务修改数据时,InnoDB 会:
① 复制当前行的旧版本到 undo log(回滚日志);
② 生成新版本,记录自身TRX_ID
(修改事务的 ID);
③ 通过DB_ROLL_PTR
将新版本与旧版本串联,形成单向链表。 -
示例中,数据从 “
李牛牛
”→“李牛牛
”→“李四”→“王五” 的演变,对应版本 0→1→2→3 的链式结构。
3.2.2.2、版本链的核心作用:支撑 MVCC 并发读
InnoDB 的 MVCC(多版本并发控制) 通过版本链实现 “读不阻塞写,写不阻塞读”:
- 读操作(SELECT):根据当前事务的隔离级别和一致性快照,从版本链的最新版本开始,向前遍历,跳过以下版本:
✖️ 事务未提交的版本(TRX_ID
对应事务未提交);
✖️ 事务 ID 大于当前快照可见范围的版本(仅 RR 级别会严格校验)。 - 写操作(UPDATE/DELETE):直接生成新的版本,追加到版本链末端(旧版本保留在 undo log,供回滚或 MVCC 读使用)。
3.2.2.3、版本链的生命周期(undo log 回收)
- 版本链中的旧版本不会永久保留,InnoDB 会通过 purge 线程 回收 “不再被任何事务需要的旧版本”(如:当所有活跃事务的快照都不包含某旧版本时,该版本会被清理)。
- 这解释了为什么长事务会导致 undo log 膨胀(长事务的快照保留时间长,旧版本无法及时回收)。
3.2.3、如何根据undo_log中的版本链进行定位确定读取哪个版本数据呢?
在 MySQL 的 InnoDB 存储引擎中,MVCC 通过 版本链(undo log) 和 ReadView 机制 实现高并发下的数据一致性。当事务执行 快照读(如普通 SELECT
)时,会根据 隔离级别 生成 ReadView(RC 级别每次查询生成新 ReadView,RR 级别事务启动时生成一次),并按规则遍历版本链,直至找到可见版本或链尾。而 当前读(如 SELECT ... FOR UPDATE
)则直接读取最新版本并加锁,不依赖 ReadView。通过这种方式,MVCC 将数据可见性判断转化为高效的内存操作,既避免了锁冲突,又通过隔离级别灵活控制一致性,实现了性能与正确性的平衡。
3.2.3.1、核心概念定义
概念 | 本质 | 典型操作示例 | 核心特点 |
---|---|---|---|
当前读 | 读取数据的最新版本,并对数据加锁(共享锁 / 排他锁),保证并发一致性。 | SELECT ... FOR UPDATE (排他锁)SELECT ... LOCK IN SHARE MODE (共享锁)INSERT/UPDATE/DELETE (隐式加锁) | ① 读最新数据;② 加锁,阻塞其他事务修改 |
快照读 | 读取数据的历史版本(通过 MVCC 的版本链),无需加锁(逻辑无锁)。 | 普通 SELECT * FROM table (未加锁时) | ① 读历史版本;② 无锁,高并发友好 |
ReadView | 一个 “可见性视图”,用于判断哪个历史版本对当前事务可见的规则集合。 | 由 InnoDB 自动生成(随事务 / 查询启动) | 包含活跃事务 ID、版本范围等元数据 |
3.2.3.2、核心分工
概念 | 作用 | 依赖关系 |
---|---|---|
MVCC | 多版本并发控制,通过维护数据的历史版本链(undo log),实现读写并发无锁化。 | 是快照读的底层支撑,当前读也依赖其版本链(但行为不同)。 |
快照读 | 读取历史版本(非最新数据),无锁,高并发友好。 | 必须依赖 ReadView 判断哪个历史版本可见。负责高并发场景。 |
当前读 | 读取最新版本,并加锁(共享 / 排他锁),保证写操作原子性。 | 不依赖 ReadView(加锁,直接读最新),直接访问最新数据并加锁。负责强一致场景(如写操作、临界资源读取)。 |
ReadView | 一个 “可见性规则集合”,记录生成时刻的活跃事务 ID 等信息,指导快照读选择历史版本。 | 是快照读的可见性判断依据,决定从版本链中选哪个版本。 |
3.2.3.3、当前读与快照读的本质区别
特性 | 快照读(Snapshot Read) | 当前读(Current Read) |
---|---|---|
依赖 ReadView | ✅ 必须通过 ReadView 选择历史版本 | ❌ 直接读取最新版本(忽略 ReadView) |
加锁机制 | ❌ 无锁(MVCC 实现读 - 写并发) | ✅ 加锁(S/X 锁,阻塞其他事务修改) |
典型操作 | SELECT * FROM table (无锁查询) | SELECT ... FOR UPDATE INSERT/UPDATE/DELETE |
一致性保证 | 由 ReadView 和隔离级别控制(RC/RR 的差异) | 强一致性(读取时锁定最新数据) |
3.2.3.4、ReadView:可见性判断的 “裁判”
ReadView 是一个动态生成的内存结构,包含四个核心字段:
m_ids
:当前活跃事务 ID 集合(未提交的事务);min_trx_id
:m_ids
中的最小事务 ID;max_trx_id
:下一个待分配的事务 ID(未来事务的起始 ID);creator_trx_id
:当前事务自身的 ID。
当事务执行快照读时,InnoDB 会按以下步骤遍历版本链:
获取当前 ReadView(根据隔离级别生成,RC 每次查询生成,RR 事务启动时生成);
从最新版本开始,检查每个版本的trx_id
是否满足以下条件:
条件 | 结果 |
---|---|
trx_id < min_trx_id | ✅ 版本可见(事务已提交) |
trx_id ≥ max_trx_id | ❌ 版本不可见(事务在 ReadView 生成后启动) |
min_trx_id ≤ trx_id < max_trx_id 且 trx_id ∈ m_ids | ❌ 版本不可见(事务未提交) |
min_trx_id ≤ trx_id < max_trx_id 且 trx_id ∉ m_ids | ✅ 版本可见(事务已提交) |
递归检查:若当前版本不可见,则通过DB_ROLL_PTR
回溯到上一版本,重复上述判断,直到找到可见版本或遍历到链尾。
3.3、拿上述模拟RC与RR的事务例子进行说明
我们把上面的例子挪下来。
3.3.1、例子
时间点 | 事务 A(trx_id=1) | 事务 B(trx_id=2) | 事务 C(trx_id=3) | 事务 D(查询事务) | stu 表(id=1)数据版本变化 | 关键现象(RR vs RC) |
---|---|---|---|---|---|---|
T1 | begin update name=' 李牛牛 ' where id = 1 | -(未开始) | -(未开始) | -(未开始) | 事务 A 修改后:版本 1(trx_id=1,name=' 李牛牛 ') | - |
T2 | commit | -(未开始) | -(未开始) | -(未开始) | 版本 1 提交,成为 “已提交版本” | - |
T3 | -(结束) | begin update name=' 李四 ' where id = 1 | -(未开始) | -(未开始) | 事务 B 修改后:版本 2(trx_id=2,name=' 李四 ') | 版本 2 未提交,仅事务 B 可见 |
T4 | -(结束) | 处理其他数据(耗时操作) | -(未开始) | select * from stu where id = 1 | RR和RC读版本1(李牛牛 ) | |
T5 | -(结束) | commit | -(未开始) | 处理其他数据(耗时操作) | 版本 2 提交,覆盖版本 1,成为新 “已提交版本” | - |
T6 | -(结束) | -(结束) | begin update name=' 王五' where id = 1 | 处理其他数据(耗时操作) | 事务 C 修改后:版本 3(trx_id=3,name=' 王五 ') | 版本 3 未提交,仅事务 C 可见 |
T7 | -(结束) | -(结束) | 处理其他数据(耗时操作) | select * from stu where id = 1(第二次查询) | RC读版本2(李四) RR读版本1(李牛牛)-解决不可重复度 | |
T8 | -(结束) | -(结束) | commit | (等待) | 版本 3 提交,覆盖版本 2,成为最新 “已提交版本” | |
T8 | -(结束) | -(结束) | -(结束) | (等待) | 无新修改,版本 3 仍为最新 |
3.3.2、RC隔离级别 ReadView 以及规则判断
在读已提交(RC)隔离级别下,每一次执行快照读(select语句)时,都会生成一个ReadView,在上述例子中事务4,在事务3与事务2提交过程中 进行了2次查询,也就是生成了2次ReadView。
快照读时机 | ReadView的值 | 版本链数据访问规则 (每一个版本进行遍历判断找到对应版本) |
---|---|---|
第一次快照读,是事务2还未提交 | m_ids=[2,3,4](当前活跃事务 ID 集合(未提交的事务) min_trx_id =2( max_trx_id = 5(下一个待分配的事务 ID(未来事务的起始 ID) creator_trx_id =4(当前事务自身的 ID) | 1、当前事务id == creator_trx_id(4)? (成立说明数据就是自己这个事务更改的) 2、trx_id<min_trx_id(2)? (成立说明数据已经提交了,可以访问,只有版本链中的1满足,其他不满足) 3、trx_id>max_trx_id(5)? (成立、并判断版本是否在m_ids中,如果在说明事务没有提交) 4、min_trx_id(2)<=trx_id<=max_trx_id(5) (如果成立,说明这个版本事务没有被提交不能访问,不存在可以访问) (满足一个即可返回,顺序互斥判断)综上判断最终找到版本1,返回李牛牛 |
第二次快照读,是事务2已提交,事务3未提交 | m_ids=[3,4](当前活跃事务 ID 集合(未提交的事务) min_trx_id =3( max_trx_id = 5(下一个待分配的事务 ID(未来事务的起始 ID) creator_trx_id =4(当前事务自身的 ID) | 1、当前事务id == creator_trx_id(4)? (成立说明数据就是自己这个事务更改的) 2、trx_id<min_trx_id(3)? (成立说明数据已经提交了,可以访问,遍历到版本链中的2满足) 3、trx_id>max_trx_id(5)? (成立说明该事务是在ReadView生成以后才开始,不容许访问。不成立,可以访问) 4、min_trx_id(3)<=trx_id<=max_trx_id(5) (如果成立,说明这个版本事务没有被提交不能访问,不存在可以访问) (满足一个即可返回,顺序互斥判断)综上判断最终找到版本2,返回 |
3.3.3、RR隔离级别 ReadView 以及规则判断
在可重复读(RR)隔离级别下,仅在第一次执行快照读(select语句)时,都会生成一个ReadView,后面快照读复用,在上述例子中事务4,在事务3与事务2提交过程中 进行了2次查询,也就是生成了1次ReadView。
快照读时机 | ReadView的值 | 版本链数据访问规则 (每一个版本进行遍历判断找到对应版本) |
---|---|---|
第一次快照读,是事务2还未提交 | m_ids=[2,3,4](当前活跃事务 ID 集合(未提交的事务) min_trx_id =2( max_trx_id = 5(下一个待分配的事务 ID(未来事务的起始 ID) creator_trx_id =4(当前事务自身的 ID) | 1、当前事务id == creator_trx_id(4)? (成立说明数据就是自己这个事务更改的) 2、trx_id<min_trx_id(2)? (成立说明数据已经提交了,可以访问,只有版本链中的1满足,其他不满足) 3、trx_id>max_trx_id(5)? (成立、并判断版本是否在m_ids中,如果在说明事务没有提交) 4、min_trx_id(2)<=trx_id<=max_trx_id(5) (如果成立,说明这个版本事务没有被提交不能访问,不存在可以访问) (满足一个即可返回,顺序互斥判断)综上判断最终找到版本1,返回李牛牛 |
第二次快照读,是事务2已提交,事务3未提交 | 复用第一的快照读 | 1、当前事务id == creator_trx_id(4)? (成立说明数据就是自己这个事务更改的) 2、trx_id<min_trx_id(2)? (成立说明数据已经提交了,可以访问,只有版本链中的1满足,其他不满足) 3、trx_id>max_trx_id(5)? (成立、并判断版本是否在m_ids中,如果在说明事务没有提交) 4、min_trx_id(2)<=trx_id<=max_trx_id(5) (如果成立,说明这个版本事务没有被提交不能访问,不存在可以访问) (满足一个即可返回,顺序互斥判断)综上判断最终找到版本1,返回李牛牛 |
3.3.4、RR存在幻读的特例
在可重复读(RR)隔离级别下,仅在第一次执行快照读(select语句)时,都会生成一个ReadView,后面快照读复用。因为复用了 所以没有幻读的问题。
特例:当两次快照读之间存在当前读(也就是说加了锁) ,ReadView会从新生成,导致产生幻读。