本篇是我的数据库学习记录,完整梳理了 MySQL 事务的四大隔离级别,从并发事务引发的三大问题,到 MVCC 与 Read View 的底层实现原理,一次性把这个数据库核心知识点讲透。内容均为自己学习过程中的整理与复盘,如有疏漏欢迎大家指正。
一、事务的核心特性:ACID
事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成,它的核心价值是保证一系列数据库操作「要么全部成功,要么全部失败」。而要实现事务的完整能力,必须严格遵守四大特性(ACID):
- 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会停留在中间环节。事务执行中发生错误,会被回滚到事务开始前的状态,就像这个事务从未执行过一样。InnoDB 引擎通过undo log(回滚日志) 保证原子性。
- 一致性(Consistency):事务操作前后,数据满足完整性约束,数据库始终保持一致性状态。比如 A、B 两个账户总余额为 1400 元,无论两者之间如何转账,转账结束后总余额依然要为 1400 元,这就是一致性的核心要求。一致性由原子性、隔离性、持久性共同保证。
- 隔离性(Isolation):数据库允许多个并发事务同时对数据进行读写和修改,隔离性可以防止多个事务并发执行时因交叉执行导致的数据不一致,保证每个事务在独立的、不受其他事务干扰的数据空间中运行。本文的核心 ------ 事务隔离级别,正是隔离性的具体落地实现,InnoDB 引擎通过MVCC(多版本并发控制) 与锁机制保证隔离性。
- 持久性(Durability):事务提交完成后,对数据的修改就是永久的,即便系统发生故障、数据库宕机,修改后的数据也不会丢失。InnoDB 引擎通过redo log(重做日志) 保证持久性。
在四大特性中,隔离性是业务开发中最常接触、也是面试中最高频的考点。要理解隔离级别,首先要搞清楚:如果没有隔离性,并发事务会引发哪些数据问题。
二、并发事务引发的三大数据问题
当多个事务同时操作数据库的同一份数据时,如果没有有效的隔离机制,会依次出现脏读、不可重复读、幻读三类问题,其严重程度逐级递增。
1. 脏读
定义:一个事务读到了另一个未提交事务修改过的数据,就发生了脏读现象。场景示例:
- 事务 A 读取到某个账户余额为 100元,将其更新为 200 元,但尚未提交事务;
- 此时事务 B 读取账户余额,拿到了事务 A 更新后的 200 元;
- 后续事务 A 发生回滚,余额恢复为 100 元,事务 B 之前读到的 200 元就成了无效的「脏数据」。
脏读的核心问题是读取到了未提交的临时数据,而这类数据随时可能因回滚失效,会直接导致后续业务逻辑基于错误数据执行。
2. 不可重复读
定义:在同一个事务内,多次读取同一个数据,前后两次读到的结果不一致,就发生了不可重复读现象。场景示例:
- 事务 A 启动后,第一次读取账户余额为 100 元,此时未做其他操作;
- 事务 B 启动后,将账户余额更新为 200 元,并提交了事务;
- 事务 A 再次读取账户余额,拿到了 200 元,同一个事务内两次读取同一条数据,结果完全不同。
不可重复读的核心问题是,同一个事务内无法保证对同一条数据的读取结果可重复,违背了事务隔离的基本诉求。
3. 幻读
定义:在同一个事务内,多次查询符合某个条件的记录数量,前后两次查询到的记录数量不一致,就发生了幻读现象。场景示例:
- 事务 B 启动后,第一次查询「余额大于 100 元的账户」,得到 5 条记录;
- 事务 A 启动后,插入了一条余额为 200 元的账户记录,并提交了事务;
- 事务 B 再次执行相同的查询语句,得到 6 条记录,同一个事务内相同的查询条件,却多出来一条记录,如同产生了幻觉。
幻读的核心问题是针对范围查询的结果集一致性被破坏,而非单条数据的修改,这也是它与不可重复读的核心区别。
三、SQL 标准的四大事务隔离级别
为了解决上述并发事务带来的三类问题,SQL 标准定义了四种隔离级别,隔离级别从低到高依次为:读未提交、读提交、可重复读、串行化。隔离级别越高,数据一致性越强,但数据库的并发性能越低,业务需要根据自身场景做平衡选择。
表格
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | 可能发生 | 可能发生 | 可能发生 |
| 读提交 | 不可能发生 | 可能发生 | 可能发生 |
| 可重复读 | 不可能发生 | 不可能发生 | 可能发生(SQL 标准) |
| 串行化 | 不可能发生 | 不可能发生 | 不可能发生 |
下面我们通过一个统一的并发场景,详解每一种隔离级别的行为表现:
场景前提:账户余额表中有一条记录,我的账户余额为 100 元。事务 A 仅负责查询余额,事务 B 负责将余额修改为 200 元,两个事务按时间顺序执行。
1. 读未提交(read uncommitted)
核心定义:一个事务还没提交时,它做的变更就能被其他事务看到。
- 事务 B 修改余额为 200 元后,即使未提交事务,事务 A 也能立刻读到 200 元的余额;
- 后续无论事务 B 是否提交,事务 A 在事务内的所有查询,都会拿到最新的变更数据。
特点:隔离级别最低,并发性能最高,但无法解决任何并发事务问题,实际业务中极少使用。
2. 读提交(read committed)
核心定义:一个事务提交之后,它做的变更才能被其他事务看到。
- 事务 B 修改余额但未提交时,事务 A 查询到的余额依然是 100 元,避免了脏读;
- 事务 B 提交事务后,事务 A 再次查询,就能读到 200 元的最新余额,同一个事务内两次读取结果不同,依然存在不可重复读。
特点:解决了脏读问题,但无法解决不可重复读和幻读,是 Oracle、PostgreSQL 等数据库的默认隔离级别。
3. 可重复读(repeatable read)
核心定义:一个事务执行过程中看到的数据,始终和这个事务启动时看到的数据保持一致。
- 事务 A 启动后,无论事务 B 是否修改数据、是否提交事务,事务 A 在整个事务内的所有查询,拿到的余额始终是事务启动时的 100 元,彻底避免了脏读和不可重复读;
- 只有事务 A 自身提交事务后,再次查询才能读到事务 B 修改后的 200元。
特点:SQL 标准中,该级别依然可能发生幻读,但 MySQL InnoDB 引擎对其做了优化,通过 MVCC 和 next-key lock 机制,很大程度上避免了幻读现象,这也是 InnoDB 将其设为默认隔离级别的核心原因。
4. 串行化(serializable)
核心定义:对记录加上读写锁,多个事务对同一条记录进行读写操作时,一旦发生读写冲突,后访问的事务必须等前一个事务执行完成,才能继续执行,彻底杜绝了事务的并发执行。
- 事务 A 启动后执行了查询操作,会对该记录加读锁;
- 此时事务 B 想要修改这条记录,会因写锁与读锁冲突被阻塞,直到事务 A 提交事务释放锁后,事务 B 才能继续执行;
- 从事务 A 的视角,整个事务内的所有查询结果完全一致,彻底杜绝了脏读、不可重复读和幻读。
特点:数据一致性最高,但并发性能极差,仅适用于对数据一致性要求极高、且几乎没有并发访问的业务场景,实际开发中极少使用。
四、隔离级别的核心实现:MVCC 与 Read View 机制
对于读未提交,直接读取数据的最新版本即可;对于串行化,通过加读写锁强制事务串行执行即可。而业务中最常用的读提交 和可重复读 ,核心都是通过MVCC(多版本并发控制) 实现的,两者的核心差异,在于 MVCC 中 Read View 的生成时机不同。
1. MVCC 的基础:数据版本链
MVCC 的核心思想,是通过维护数据的多个历史版本,让不同事务的读写操作不用加锁互斥,从而极大提升数据库的并发性能。而数据版本链的实现,依赖于 InnoDB 聚簇索引记录中的两个隐藏列,以及 undo log 回滚日志。
InnoDB 的聚簇索引记录中,包含两个与事务相关的隐藏列:
- trx_id:当一个事务对某条聚簇索引记录进行修改时,会把该事务的事务 ID 记录在 trx_id 隐藏列中,标识这条数据的最新修改者。
- roll_pointer:每次对某条记录进行修改时,会把旧版本的记录写入 undo log 中,这个隐藏列是一个指针,指向修改前的旧版本记录,通过它可以串联起这条数据的所有历史版本,形成版本链。
比如事务 A(事务 ID=51)将余额从 100 元修改为 200 元后,数据的版本链如下:最新版本(余额 200 元,trx_id=51)→ 旧版本(余额 100 元,trx_id=50)→ 更早的历史版本
2. 数据可见性的核心:Read View
Read View 可以理解为事务启动时(或查询时)数据库的一个「数据快照」,它定义了当前事务能看到哪些数据版本、不能看到哪些数据版本,是 MVCC 中判断数据可见性的核心依据。
Read View 包含四个核心字段:
表格
| 字段名 | 核心含义 |
|---|---|
| m_ids | 创建 Read View 时,数据库中「启动了但还未提交的活跃事务」的事务 ID 列表 |
| min_trx_id | 活跃事务列表 m_ids 中的最小事务 ID |
| max_trx_id | 创建 Read View 时,数据库中应该分配给下一个事务的 ID 值(全局最大事务 ID+1) |
| creator_trx_id | 创建这个 Read View 的当前事务的事务 ID |
有了 Read View 和数据版本链,事务查询数据时,会按照以下规则判断某个数据版本是否可见:
- 如果数据版本的 trx_id < min_trx_id:说明该版本是在 Read View 创建前就已经提交的事务生成的,对当前事务可见。
- 如果数据版本的 trx_id ≥ max_trx_id:说明该版本是在 Read View 创建后才启动的事务生成的,对当前事务不可见。
- 如果数据版本的 trx_id 在 min_trx_id 和 max_trx_id 之间:
- 若 trx_id 在 m_ids 列表中:说明生成该版本的事务还未提交,对当前事务不可见;
- 若 trx_id 不在 m_ids 列表中:说明生成该版本的事务已经提交,对当前事务可见。
如果当前最新版本不可见,就会通过 roll_pointer 指针沿着版本链向下查找,直到找到第一个对当前事务可见的版本,作为查询结果返回。
3. 读提交与可重复读的核心差异
读提交和可重复读的底层实现,核心差异就在于Read View 的生成时机不同,这也是两者隔离能力不同的根本原因。
(1)可重复读隔离级别
Read View 生成规则:事务启动时生成一个 Read View,整个事务生命周期内,所有查询都复用这个 Read View。
正是因为整个事务都使用同一个快照,所以无论其他事务是否修改数据、是否提交,当前事务的可见性规则始终不变,自然就能保证同一个事务内,多次读取同一条数据的结果完全一致,彻底解决了不可重复读问题。
同时,对于普通的 select 快照读,因为始终使用事务启动时的快照,即使其他事务插入了新数据,当前事务也无法看到,从而很大程度上避免了幻读问题。
(2)读提交隔离级别
Read View 生成规则:事务内的每一次 select 语句执行前,都会重新生成一个新的 Read View。
这就意味着,事务内的每次查询,都会获取最新的数据库快照。如果两次查询之间,有其他事务修改了数据并提交,新的 Read View 就会将这个已提交的事务纳入可见范围,从而导致同一个事务内两次读取同一条数据的结果不同,这就是不可重复读产生的根本原因。
五、InnoDB 对幻读的特殊处理
SQL 标准中,可重复读隔离级别依然允许幻读的发生,但 MySQL InnoDB 引擎在可重复读级别下,通过两套机制,极大程度上避免了幻读现象:
- 针对快照读(普通 select 语句):通过 MVCC 机制解决幻读。因为可重复读级别下,整个事务复用同一个 Read View 快照,即使中途有其他事务插入了符合查询条件的新数据,当前事务也无法看到这条新数据,自然就避免了幻读。
- 针对当前读(select ... for update、insert、update、delete 语句):通过 next-key lock(临键锁,记录锁 + 间隙锁)解决幻读。执行当前读语句时,InnoDB 会对查询的范围加上临键锁,不仅锁住符合条件的现有记录,还会锁住范围之间的间隙,阻止其他事务在这个范围内插入新数据,从根源上避免了幻读的发生。
需要特别注意的是,InnoDB 的可重复读级别只是很大程度上避免了幻读,并非完全杜绝,如果事务内交替使用快照读和当前读,依然有可能触发幻读,这也是业务开发中需要注意的细节。
六、总结
事务隔离级别是数据库并发控制的核心,其本质是在数据一致性和并发性能之间做平衡:
- 读未提交隔离级别最低,并发性能最好,但无法解决任何并发问题,实际业务极少使用;
- 读提交解决了脏读问题,保留了较好的并发性能,适合读多写少、对数据一致性要求不极致的业务场景;
- 可重复读是 MySQL InnoDB 的默认隔离级别,解决了脏读和不可重复读问题,同时通过 MVCC 和临键锁极大程度避免了幻读,兼顾了一致性和并发性能,是绝大多数业务场景的最优选择;
- 串行化彻底杜绝了所有并发问题,但并发性能极差,仅适用于对数据一致性要求极高的低并发场景。
而业务中最常用的读提交和可重复读,底层核心都是基于 MVCC 多版本并发控制实现,两者的根本差异,就在于 Read View 快照的生成时机不同。理解了 Read View 的可见性规则,就真正掌握了 MySQL 事务隔离级别的核心原理。