文章目录
- 前言
- 一、事务并发带来的三大问题
-
- [1.1 脏读](#1.1 脏读)
- [1.2 不可重复读](#1.2 不可重复读)
- [1.3 幻读](#1.3 幻读)
- [1.4 三种问题的对比](#1.4 三种问题的对比)
- 二、四种隔离级别详解
-
- [2.1 读未提交](#2.1 读未提交)
- [2.2 读已提交](#2.2 读已提交)
- [2.3 可重复读](#2.3 可重复读)
- [2.4 串行化](#2.4 串行化)
- [2.5 隔离级别对比表](#2.5 隔离级别对比表)
- [三、MySQL InnoDB的实现机制](#三、MySQL InnoDB的实现机制)
-
- [3.1 MVCC与Read View](#3.1 MVCC与Read View)
- [3.2 锁机制](#3.2 锁机制)
- [3.3 InnoDB如何解决幻读](#3.3 InnoDB如何解决幻读)
- 四、MySQL默认隔离级别:可重复读
-
- [4.1 为什么MySQL选择可重复读](#4.1 为什么MySQL选择可重复读)
- [4.2 如何查看和修改隔离级别](#4.2 如何查看和修改隔离级别)
- [五、读已提交 vs 可重复读](#五、读已提交 vs 可重复读)
-
- [5.1 核心区别](#5.1 核心区别)
- [5.2 选择建议](#5.2 选择建议)
- 六、总结与要点
-
- [6.1 核心要点回顾](#6.1 核心要点回顾)
- [6.2 常见问题](#6.2 常见问题)
- [6.3 进阶知识点](#6.3 进阶知识点)
- 写在最后:
前言
"明明查询了两次,结果却不一样"------这是并发事务中经常遇到的问题。事务隔离级别正是为了解决这类问题而设计的,它定义了事务之间如何相互隔离,以及哪些数据不一致现象可以被容忍。
本文将深入剖析事务隔离级别的方方面面:
- 四种隔离级别:从低到高的完整演进
- 三大并发问题:脏读、不可重复读、幻读的本质
- MySQL实现:InnoDB如何通过MVCC和锁机制实现不同隔离级别
- 默认级别:为什么MySQL选择可重复读作为默认隔离级别
一、事务并发带来的三大问题
在了解隔离级别之前,首先要理解并发事务可能导致的三种数据不一致现象。
1.1 脏读
定义:一个事务读取了另一个事务未提交的数据。
场景示例:
- 事务A将订单金额从100改为200,但尚未提交
- 事务B读取到金额为200
- 事务A发生回滚,金额恢复为100
- 事务B读到的200就是"脏数据",实际上从未存在过
危害:脏读会导致业务逻辑基于不存在的数据做出错误判断。
1.2 不可重复读
定义:一个事务内两次读取同一数据,得到不同结果。
场景示例:
- 事务A第一次查询订单金额,得到100
- 事务B将金额修改为200并提交
- 事务A第二次查询,得到200
同一事务内,两次读取结果不一致
危害:不可重复读会破坏事务的稳定性,可能导致基于第一次读取结果做出的决策在第二次读取时失效。
1.3 幻读
定义:一个事务内两次查询同一范围的数据,第二次查询出现了第一次没有的"幻影行"。
场景示例:
- 事务A查询状态为"待支付"的订单,得到10条记录
- 事务B新增一条"待支付"的订单并提交
- 事务A再次查询"待支付"订单,得到11条记录
- 多出来的一条就是"幻影行"
危害:幻读会影响范围查询的准确性,在需要锁定范围数据的场景下尤为严重。
1.4 三种问题的对比
| 问题类型 | 关注点 | 本质 | 影响范围 |
|---|---|---|---|
| 脏读 | 未提交数据 | 违反了事务的隔离性 | 单行数据 |
| 不可重复读 | 已提交的修改 | 同一行数据被修改 | 单行数据 |
| 幻读 | 已提交的新增/删除 | 数据集合的变化 | 范围数据 |
二、四种隔离级别详解
SQL标准定义了四种隔离级别,级别越高,数据一致性越强,但并发性能也越低。
2.1 读未提交
- 特点:事务可以读取其他事务未提交的修改,完全不隔离。
- 解决的问题:无。
- 可能的问题:脏读、不可重复读、幻读都可能发生。
- 实现方式:直接读取最新数据,不加任何读锁,也不使用MVCC。
- 适用场景:几乎没有实际应用场景,除非对数据一致性完全不在意。
2.2 读已提交
- 特点:只能读取其他事务已提交的数据。
- 解决的问题:脏读。
- 可能的问题:不可重复读、幻读仍可能发生。
- 实现方式:
- 每条语句执行时都生成一个新的Read View
- 写操作加行锁,防止并发修改
- 没有间隙锁,只有行锁
- 适用场景:大多数关系型数据库(如Oracle、SQL Server)的默认隔离级别,适合对一致性要求不特别严格的场景。
2.3 可重复读
- 特点:同一事务内多次读取同一数据,结果保持一致。
- 解决的问题:脏读、不可重复读。
- 可能的问题:幻读在标准SQL中仍可能发生,但InnoDB通过间隙锁解决了。
- 实现方式:
- 事务开始时生成一个Read View,整个事务期间复用
- 写操作加行锁
- 在MySQL InnoDB中,还会加间隙锁或临键锁防止幻读
- 适用场景:MySQL的默认隔离级别,适合大多数业务场景。
2.4 串行化
- 特点:事务串行执行,完全隔离。
- 解决的问题:脏读、不可重复读、幻读全部解决。
- 实现方式:
- 所有读操作都加锁(SELECT转SELECT ... LOCK IN SHARE MODE)
- 实际相当于将所有并发事务强制串行化
- 适用场景:对一致性要求极高、并发量很低的场景,如金融对账等。
2.5 隔离级别对比表
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 并发性能 | 实现复杂度 |
|---|---|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 | 最高 | 最低 |
| 读已提交 | 解决 | 可能 | 可能 | 高 | 低 |
| 可重复读 | 解决 | 解决 | InnoDB解决 | 中 | 高 |
| 串行化 | 解决 | 解决 | 解决 | 最低 | 中 |
三、MySQL InnoDB的实现机制
3.1 MVCC与Read View
InnoDB通过MVCC实现读已提交和可重复读级别下的快照读(普通SELECT)。
Read View的生成时机:
- 读已提交:事务中的每条SELECT语句都会生成一个新的Read View
- 可重复读:事务中的第一条SELECT语句生成Read View,后续所有查询都复用这个Read View
Read View的核心内容:
- 当前活跃事务ID列表
- 最小活跃事务ID
- 最大已分配事务ID
可见性判断规则:
- 如果数据行的DB_TRX_ID小于最小活跃ID,说明是已提交事务,可见
- 如果数据行的DB_TRX_ID等于当前事务ID,说明是自己修改的,可见
- 如果数据行的DB_TRX_ID在活跃事务列表中,说明是未提交事务,不可见
- 其他情况,沿着undo log版本链找更早的版本
3.2 锁机制
InnoDB使用多种锁机制来控制并发访问:
| 锁类型 | 作用范围 | 说明 |
|---|---|---|
| 行锁 | 单行记录 | 锁定特定行,防止并发修改 |
| 间隙锁 | 两个索引之间的间隙 | 锁定范围,防止在这个范围内插入新数据 |
| 临键锁 | 行锁+间隙锁 | 锁定一个左开右闭的区间,是InnoDB默认的行锁算法 |
不同隔离级别的锁行为:
| 隔离级别 | 快照读 | 当前读 | 间隙锁 |
|---|---|---|---|
| 读未提交 | 无锁 | 行锁 | 无 |
| 读已提交 | MVCC | 行锁 | 无 |
| 可重复读 | MVCC | 临键锁 | 有 |
| 串行化 | 共享锁 | 排他锁 | 有 |
3.3 InnoDB如何解决幻读
InnoDB在可重复读级别下,通过组合使用MVCC和锁机制,彻底解决了幻读问题。
对于快照读(普通SELECT):
- 整个事务使用同一个Read View
- 新增的数据行,其DB_TRX_ID必然大于当前事务的最小活跃ID,因此不可见
- 从MVCC层面杜绝了幻读
对于当前读(SELECT ... FOR UPDATE / UPDATE / DELETE):
- 需要读取最新数据,不能使用快照
- 使用临键锁锁定扫描到的范围
- 其他事务无法在这个范围内插入新数据
- 从锁层面杜绝了幻读
这种设计使得MySQL的可重复读隔离级别实际上达到了串行化在数据一致性上的效果,同时保持了较高的并发性能。
四、MySQL默认隔离级别:可重复读
4.1 为什么MySQL选择可重复读
与其他数据库(如Oracle、SQL Server)默认使用读已提交不同,MySQL默认使用可重复读,主要有以下原因:
| 原因 | 说明 |
|---|---|
| 历史原因 | MySQL早期binlog格式为statement时,必须在可重复读级别下才能保证主从一致性 |
| 间隙锁支持 | 间隙锁只在可重复读级别生效,这是防止幻读的关键 |
| 业务兼容性 | 许多业务逻辑隐式依赖可重复读的语义 |
4.2 如何查看和修改隔离级别
sql
-- 查看当前会话隔离级别
SELECT @@transaction_isolation;
-- 查看全局隔离级别
SELECT @@global.transaction_isolation;
-- 设置当前会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
五、读已提交 vs 可重复读
5.1 核心区别
| 对比维度 | 读已提交 | 可重复读 |
|---|---|---|
| Read View生成 | 每语句生成 | 每事务生成 |
| 不可重复读 | 可能发生 | 不会发生 |
| 幻读 | 可能发生 | InnoDB下不会发生 |
| 间隙锁 | 无 | 有 |
| 锁竞争 | 较少 | 较多 |
| 并发性能 | 更高 | 较高 |
5.2 选择建议
| 业务场景 | 推荐隔离级别 | 理由 |
|---|---|---|
| 普通OLTP业务 | 可重复读 | 保持默认,简单可靠 |
| 高并发更新 | 读已提交 | 减少间隙锁竞争,提升并发 |
| 金融对账 | 串行化 | 数据一致性优先 |
| 报表统计 | 读已提交 | 允许数据变化,追求性能 |
六、总结与要点
6.1 核心要点回顾
- 四种隔离级别:读未提交、读已提交、可重复读、串行化,依次增强。
- 三大问题:脏读(未提交数据)、不可重复读(同一行被修改)、幻读(集合变化)。
- MySQL默认:可重复读(Repeatable Read)。
- InnoDB实现:
- MVCC实现快照读,避免脏读和不可重复读
- 间隙锁+MVCC共同解决幻读
- 读已提交每语句生成ReadView,可重复读每事务生成
6.2 常见问题
| 问题 | 回答要点 |
|---|---|
| MySQL默认隔离级别是什么? | 可重复读,通过MVCC+间隙锁解决幻读 |
| 读已提交和可重复读的区别? | ReadView生成时机不同,间隙锁有无不同 |
| InnoDB如何解决幻读? | 快照读用MVCC,当前读用间隙锁 |
| 为什么MySQL用可重复读做默认? | 历史原因(binlog格式)+ 间隙锁支持 |
| 什么时候适合用读已提交? | 高并发更新场景,减少间隙锁竞争 |
6.3 进阶知识点
- 间隙锁只在可重复读生效:这是MySQL的设计决策,读已提交级别没有间隙锁
- Next-Key Lock:行锁+间隙锁的组合,锁定一个左开右闭的区间
- RR下的幻读测试:可以通过测试验证快照读无幻读,当前读有间隙锁保护
- 隔离级别与binlog格式:statement格式binlog必须在RR下才能保证主从一致
写在最后:
事务隔离级别是数据库并发控制的核心,理解它不仅能帮助我们正确使用数据库,还能在设计分布式系统时提供思路。MySQL通过MVCC和锁机制的巧妙结合,在可重复读级别下实现了与串行化相当的数据一致性,同时保持了较高的并发性能,这也是它成为最受欢迎的开源数据库的重要原因之一。