MySQL隔离级别实现原理
MySQL 如何实现不同隔离级别
MySQL 通过 多版本并发控制(MVCC) 和 锁机制 的组合实现了不同的事务隔离级别。不同的隔离级别采用不同的并发控制策略,从而在数据一致性和系统性能之间取得平衡。
隔离级别实现机制总览
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现机制 |
|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 直接读取最新数据(无版本控制,无锁) |
| READ COMMITTED | 不可能 | 可能 | 可能 | 每次查询生成新 ReadView |
| REPEATABLE READ | 不可能 | 不可能 | 可能 | 事务开始时生成 ReadView + Next-Key Lock |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 | 所有读操作加共享锁,写操作加排他锁 |
graph TB
A["隔离级别实现"] --> B["MVCC<br/>多版本并发控制"]
A --> C["锁机制<br/>Lock"]
B --> B1["Undo Log<br/>版本链"]
B --> B2["ReadView<br/>可见性判断"]
C --> C1["Record Lock<br/>行锁"]
C --> C2["Gap Lock<br/>间隙锁"]
C --> C3["Next-Key Lock<br/>行锁+间隙锁"]
C --> C4["表锁/意向锁"]
style A fill:#2196F3,stroke:#1976D2,rx:10,ry:10
style B fill:#4CAF50,stroke:#388E3C,rx:10,ry:10
style C fill:#FF9800,stroke:#F57C00,rx:10,ry:10
style B1 fill:#E91E63,stroke:#C2185B,rx:10,ry:10
style B2 fill:#E91E63,stroke:#C2185B,rx:10,ry:10
style C1 fill:#9C27B0,stroke:#7B1FA2,rx:10,ry:10
style C2 fill:#9C27B0,stroke:#7B1FA2,rx:10,ry:10
style C3 fill:#9C27B0,stroke:#7B1FA2,rx:10,ry:10
style C4 fill:#9C27B0,stroke:#7B1FA2,rx:10,ry:10MVCC(多版本并发控制)
MVCC(Multi-Version Concurrency Control)是 InnoDB 实现高并发的核心机制。它通过保存数据的多个历史版本,使得读操作不需要加锁,从而大幅提升并发性能。
MVCC 的应用场景
在数据库中,对数据的操作主要有两种:读 和 写。在并发场景下,会出现以下三种情况:
- 读-读并发:多个事务同时读取数据,不会出现问题
- 写-写并发:通过加锁机制处理,保证数据一致性
- 读-写并发:通过 MVCC 机制解决
MVCC 的核心价值在于:读操作不加锁,写操作也不阻塞读操作,极大提升了系统的并发性能。
快照读 vs 当前读
要理解 MVCC,必须首先明确两个概念:快照读 和 当前读。
快照读(Snapshot Read):
读取的是快照数据,即快照生成那一刻的数据。普通的 SELECT 语句在不加锁情况下就是快照读。
-- 快照读示例
SELECT * FROM orders WHERE customer_id = 1001;当前读(Current Read):
读取的是最新数据,需要加锁。包括加锁的 SELECT、增删改操作。
-- 当前读示例
SELECT * FROM orders WHERE customer_id = 1001 LOCK IN SHARE MODE;
SELECT * FROM orders WHERE customer_id = 1001 FOR UPDATE;
INSERT INTO orders (...) VALUES (...);
UPDATE orders SET status = 'PAID' WHERE order_id = 5001;
DELETE FROM orders WHERE order_id = 5002;两者的关系:
- 快照读是 MVCC 实现的基础,通过读取历史版本实现无锁并发
- 当前读是悲观锁实现的基础,通过加锁保证数据一致性
MVCC 核心组成
MVCC 依赖两个关键组件:Undo Log 和 ReadView。
1. Undo Log(版本链)
Undo Log 记录数据修改前的旧版本,形成一个版本链。每条记录都包含以下隐藏字段:
- DB_TRX_ID:最后修改该行的事务 ID
- DB_ROLL_PTR:指向 Undo Log 中上一个版本的指针
- DB_ROW_ID:隐藏的自增 ID(如果没有主键)
版本链示例:
假设有一张商品表:
CREATE TABLE product (
id INT PRIMARY KEY,
name VARCHAR(100),
price DECIMAL(10, 2)
);
INSERT INTO product (id, name, price) VALUES (1, '笔记本电脑', 5000.00);多个事务依次修改价格:
-- 事务 100:修改价格为 4800
UPDATE product SET price = 4800 WHERE id = 1;
-- 事务 200:修改价格为 4500
UPDATE product SET price = 4500 WHERE id = 1;
-- 事务 300:修改价格为 4200
UPDATE product SET price = 4200 WHERE id = 1;形成的版本链:
graph LR
A["当前版本<br/>price=4200<br/>TRX_ID=300"] --> B["旧版本<br/>price=4500<br/>TRX_ID=200"]
B --> C["更旧版本<br/>price=4800<br/>TRX_ID=100"]
C --> D["最初版本<br/>price=5000<br/>TRX_ID=NULL"]
style A fill:#4CAF50,stroke:#388E3C,rx:10,ry:10
style B fill:#2196F3,stroke:#1976D2,rx:10,ry:10
style C fill:#2196F3,stroke:#1976D2,rx:10,ry:10
style D fill:#607D8B,stroke:#455A64,rx:10,ry:102. ReadView(读视图)
ReadView 是事务在某个时刻对数据库状态的快照,用于判断版本链中哪些版本对当前事务可见。它是 MVCC 实现的核心,决定了事务能看到哪些数据版本。
ReadView 的作用:
ReadView 主要解决可见性问题,即告诉事务应该看到哪个快照,不应该看到哪个快照。不同的隔离级别下,ReadView 的生成时机不同:
- RC(读已提交):每次查询时重新创建 ReadView,以反映最新提交的更改
- RR(可重复读):事务第一次查询时创建 ReadView,整个事务期间保持不变
ReadView 包含的关键信息:
在 InnoDB 的实现中(MySQL 5.7/8.0 源码),ReadView 包含以下核心字段:
- trx_ids(m_ids):生成 ReadView 时所有活跃(未提交)的读写事务 ID 列表
- up_limit_id(min_trx_id):活跃事务中最小的事务 ID(最低水位)
- low_limit_id(max_trx_id):系统应该分配给下一个事务的 ID 值(最高水位)
- creator_trx_id:创建这个 ReadView 的事务 ID
注意: trx_ids 包含了 [up_limit_id, low_limit_id) 范围内的活跃事务 ID,但不一定连续。例如:trx_ids = [5, 7, 8, 11],表示事务 5、7、8、11 正在活跃
可见性判断规则:
对于版本链中的每个版本,其 DB_TRX_ID 记为 trx_id,判断流程如下:
graph TB
A["检查版本的 trx_id"] --> B{"trx_id < min_trx_id?"}
B -->|是| C["✓ 可见<br/>该版本在当前事务开始前已提交"]
B -->|否| D{"trx_id >= max_trx_id?"}
D -->|是| E["✗ 不可见<br/>该版本在当前事务开始后才创建"]
D -->|否| F{"trx_id 在 m_ids 中?"}
F -->|是| G["✗ 不可见<br/>该版本由未提交事务创建"]
F -->|否| H["✓ 可见<br/>该版本由已提交事务创建"]
style C fill:#4CAF50,stroke:#388E3C,rx:10,ry:10
style E fill:#F44336,stroke:#D32F2F,rx:10,ry:10
style G fill:#F44336,stroke:#D32F2F,rx:10,ry:10
style H fill:#4CAF50,stroke:#388E3C,rx:10,ry:10详细判断示例:
假设有一个 ReadView:
trx_ids = [5, 6, 8] -- 活跃事务列表
up_limit_id = 5 -- 最小活跃事务 ID
low_limit_id = 8 -- 下一个将分配的事务 ID
creator_trx_id = 7 -- 当前事务 ID对于不同的版本,判断可见性:
-- 情况 1:trx_id = 3
-- 3 < 5(up_limit_id)→ 可见
-- 该事务在当前事务开始前已提交
-- 情况 2:trx_id = 6
-- 5 <= 6 < 8 且 6 在 trx_ids 中 → 不可见
-- 事务 6 正在活跃,未提交
-- 情况 3:trx_id = 7
-- 7 == creator_trx_id → 可见(例外情况)
-- 是当前事务自己的修改,肯定可见
-- 情况 4:trx_id = 9
-- 9 >= 8(low_limit_id)→ 不可见
-- 该事务在当前事务开始后才创建核心原则: 一个事务只能看到在它开始之前已经提交的事务的结果,而未提交的结果都是不可见的(自己的除外)
不同隔离级别下的 MVCC 行为
READ UNCOMMITTED(RU)
实现方式: 直接读取最新版本的数据,不使用 MVCC 和 ReadView。
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- 事务 A
BEGIN;
SELECT price FROM product WHERE id = 1;
-- 直接读取最新值,即使是未提交的修改特点: 没有任何隔离保护,性能最高但数据一致性最差。
READ COMMITTED(RC)
实现方式: 每次 SELECT 语句执行时都生成新的 ReadView。
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 事务 A
BEGIN;
SELECT price FROM product WHERE id = 1;
-- 第一次查询,生成 ReadView_1,读取已提交的最新版本
-- 事务 B 修改并提交
-- UPDATE product SET price = 4000 WHERE id = 1; COMMIT;
SELECT price FROM product WHERE id = 1;
-- 第二次查询,生成 ReadView_2,读取事务 B 提交后的新值
COMMIT;工作流程:
sequenceDiagram
participant T1 as 事务 A (RC)
participant RV as ReadView
participant DB as 数据库
participant T2 as 事务 B
T1->>RV: 第一次 SELECT
RV->>RV: 生成 ReadView_1
RV->>DB: 读取符合 ReadView_1 的版本
DB-->>T1: 返回 price=4200
T2->>DB: UPDATE price=4000
T2->>DB: COMMIT
T1->>RV: 第二次 SELECT
RV->>RV: 生成 ReadView_2(新的)
RV->>DB: 读取符合 ReadView_2 的版本
DB-->>T1: 返回 price=4000(读到新提交的值)特点: 避免脏读,但会出现不可重复读和幻读。
REPEATABLE READ(RR)
实现方式: 事务第一次执行 SELECT 时生成 ReadView,后续所有快照读都使用这个 ReadView。
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 事务 A
BEGIN;
SELECT price FROM product WHERE id = 1;
-- 第一次查询,生成 ReadView,读取 price=4200
-- 事务 B 修改并提交
-- UPDATE product SET price = 4000 WHERE id = 1; COMMIT;
SELECT price FROM product WHERE id = 1;
-- 第二次查询,仍使用之前的 ReadView,读取 price=4200
COMMIT;工作流程:
sequenceDiagram
participant T1 as 事务 A (RR)
participant RV as ReadView
participant DB as 数据库
participant T2 as 事务 B
T1->>T1: BEGIN
T1->>RV: 第一次 SELECT
RV->>RV: 生成 ReadView(只生成一次)
RV->>DB: 读取符合 ReadView 的版本
DB-->>T1: 返回 price=4200
T2->>DB: UPDATE price=4000
T2->>DB: COMMIT
T1->>RV: 第二次 SELECT
RV->>RV: 复用之前的 ReadView
RV->>DB: 读取符合 ReadView 的版本
DB-->>T1: 返回 price=4200(仍是旧值)特点: 避免脏读和不可重复读,快照读场景下避免幻读。
锁机制与幻读处理
MVCC 只能解决快照读(普通 SELECT)的并发问题。对于当前读(加锁的 SELECT、UPDATE、DELETE、INSERT),需要通过锁机制来保证隔离性。
快照读 vs 当前读
| 读类型 | SQL 示例 | 是否加锁 | 读取版本 |
|---|---|---|---|
| 快照读 | SELECT * FROM table WHERE ... | 否 | 基于 ReadView 读取历史版本 |
| 当前读 | SELECT ... FOR UPDATE SELECT ... LOCK IN SHARE MODE UPDATE / DELETE / INSERT | 是 | 读取最新版本,加锁 |
示例代码:
-- 快照读(不加锁)
SELECT * FROM orders WHERE order_id = 1001;
-- 当前读(加排他锁)
SELECT * FROM orders WHERE order_id = 1001 FOR UPDATE;
-- 当前读(加共享锁)
SELECT * FROM orders WHERE order_id = 1001 LOCK IN SHARE MODE;
-- 当前读(加排他锁)
UPDATE orders SET status = 'PAID' WHERE order_id = 1001;
DELETE FROM orders WHERE order_id = 1001;
INSERT INTO orders (order_id, amount) VALUES (1001, 500);Next-Key Lock(间隙锁 + 行锁)
在 REPEATABLE READ 级别下,InnoDB 使用 Next-Key Lock 防止当前读场景下的幻读。
Next-Key Lock 组成:
- Record Lock(行锁):锁定索引记录本身
- Gap Lock(间隙锁):锁定索引记录之间的间隙
graph LR
A["索引记录 10"] -.->|Gap Lock| B["间隙"]
B -.-> C["索引记录 20"]
C -.->|Gap Lock| D["间隙"]
D -.-> E["索引记录 30"]
C -.->|Record Lock| C
style A fill:#E0E0E0,stroke:#9E9E9E,rx:10,ry:10
style C fill:#4CAF50,stroke:#388E3C,rx:10,ry:10
style E fill:#E0E0E0,stroke:#9E9E9E,rx:10,ry:10
style B fill:#FF9800,stroke:#F57C00,rx:10,ry:10
style D fill:#FF9800,stroke:#F57C00,rx:10,ry:10示例场景:
假设订单表有索引 (user_id),当前记录:
| order_id | user_id | amount |
|---|---|---|
| 101 | 10 | 100 |
| 102 | 20 | 200 |
| 103 | 30 | 300 |
-- 事务 A(RR 隔离级别)
BEGIN;
SELECT * FROM orders WHERE user_id > 15 AND user_id < 25 FOR UPDATE;
-- 锁定范围:(10, 20] 的 Next-Key Lock + (20, 30) 的 Gap Lock
-- 即锁定 user_id 在 (10, 30) 范围内的所有记录和间隙
-- 事务 B 尝试插入
INSERT INTO orders (order_id, user_id, amount) VALUES (104, 18, 150);
-- 阻塞!因为 user_id=18 在锁定的间隙内
-- 事务 B 尝试插入
INSERT INTO orders (order_id, user_id, amount) VALUES (105, 35, 400);
-- 成功!user_id=35 不在锁定范围内SERIALIZABLE(串行化)
实现方式: 所有读操作自动加共享锁(LOCK IN SHARE MODE),写操作加排他锁。
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 事务 A
BEGIN;
SELECT * FROM orders WHERE user_id = 1001;
-- 自动转换为:SELECT ... LOCK IN SHARE MODE
-- 事务 B 尝试修改
UPDATE orders SET amount = 600 WHERE user_id = 1001;
-- 阻塞!等待事务 A 释放共享锁特点: 完全串行化执行,避免所有并发问题,但性能最差。
InnoDB 如何解决三种读现象
| 读现象 | 解决方式 | 使用机制 |
|---|---|---|
| 脏读 | RC 及以上级别 | 每次查询生成新 ReadView,只读取已提交版本 |
| 不可重复读 | RR 及以上级别 | 事务开始时生成 ReadView,后续复用同一视图 |
| 幻读 | MVCC(快照读)+ Next-Key Lock(当前读) | RR:快照读避免幻读,当前读用间隙锁防止插入 SERIALIZABLE:强制串行化 |
实现原理总结:
graph TB
A["解决脏读"] --> B["ReadView 判断可见性<br/>只读已提交事务的版本"]
C["解决不可重复读"] --> D["RR 级别下 ReadView 固定<br/>读取一致的快照版本"]
E["解决幻读"] --> F["快照读:MVCC"]
E --> G["当前读:Next-Key Lock"]
style A fill:#4CAF50,stroke:#388E3C,rx:10,ry:10
style C fill:#2196F3,stroke:#1976D2,rx:10,ry:10
style E fill:#9C27B0,stroke:#7B1FA2,rx:10,ry:10
style B fill:#FF9800,stroke:#F57C00,rx:10,ry:10
style D fill:#FF9800,stroke:#F57C00,rx:10,ry:10
style F fill:#FF9800,stroke:#F57C00,rx:10,ry:10
style G fill:#FF9800,stroke:#F57C00,rx:10,ry:10总结
MySQL 通过 MVCC 和锁机制的协同工作实现了不同的隔离级别:
- MVCC 提供高效的快照读,避免读写冲突
- ReadView 控制版本可见性,实现不同隔离级别的语义
- 锁机制 保护当前读和写操作,防止并发冲突
- Next-Key Lock 在 RR 级别下防止幻读
理解这些机制,能够帮助我们:
- 根据业务需求选择合适的隔离级别
- 优化查询性能(优先使用快照读)
- 避免死锁和锁等待问题
- 设计更可靠的并发控制方案
更新: 2025-12-04 17:37:41
原文: https://www.yuque.com/u22210564/zoxfmt/doc-01-mysql-11-mysql-14