Skip to content

MySQL隔离级别实现原理

MySQL 如何实现不同隔离级别

MySQL 通过 多版本并发控制(MVCC)锁机制 的组合实现了不同的事务隔离级别。不同的隔离级别采用不同的并发控制策略,从而在数据一致性和系统性能之间取得平衡。

隔离级别实现机制总览

隔离级别脏读不可重复读幻读实现机制
READ UNCOMMITTED可能可能可能直接读取最新数据(无版本控制,无锁)
READ COMMITTED不可能可能可能每次查询生成新 ReadView
REPEATABLE READ不可能不可能可能事务开始时生成 ReadView + Next-Key Lock
SERIALIZABLE不可能不可能不可能所有读操作加共享锁,写操作加排他锁
mermaid
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:10

MVCC(多版本并发控制)

MVCC(Multi-Version Concurrency Control)是 InnoDB 实现高并发的核心机制。它通过保存数据的多个历史版本,使得读操作不需要加锁,从而大幅提升并发性能。

MVCC 的应用场景

在数据库中,对数据的操作主要有两种:。在并发场景下,会出现以下三种情况:

  1. 读-读并发:多个事务同时读取数据,不会出现问题
  2. 写-写并发:通过加锁机制处理,保证数据一致性
  3. 读-写并发:通过 MVCC 机制解决

MVCC 的核心价值在于:读操作不加锁,写操作也不阻塞读操作,极大提升了系统的并发性能。

快照读 vs 当前读

要理解 MVCC,必须首先明确两个概念:快照读当前读

快照读(Snapshot Read):

读取的是快照数据,即快照生成那一刻的数据。普通的 SELECT 语句在不加锁情况下就是快照读。

sql
-- 快照读示例
SELECT * FROM orders WHERE customer_id = 1001;

当前读(Current Read):

读取的是最新数据,需要加锁。包括加锁的 SELECT、增删改操作。

sql
-- 当前读示例
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 LogReadView

1. Undo Log(版本链)

Undo Log 记录数据修改前的旧版本,形成一个版本链。每条记录都包含以下隐藏字段:

  • DB_TRX_ID:最后修改该行的事务 ID
  • DB_ROLL_PTR:指向 Undo Log 中上一个版本的指针
  • DB_ROW_ID:隐藏的自增 ID(如果没有主键)

版本链示例:

假设有一张商品表:

sql
CREATE TABLE product (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    price DECIMAL(10, 2)
);

INSERT INTO product (id, name, price) VALUES (1, '笔记本电脑', 5000.00);

多个事务依次修改价格:

sql
-- 事务 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;

形成的版本链:

mermaid
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:10

2. 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,判断流程如下:

mermaid
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:

sql
trx_ids = [5, 6, 8]  -- 活跃事务列表
up_limit_id = 5      -- 最小活跃事务 ID
low_limit_id = 8     -- 下一个将分配的事务 ID
creator_trx_id = 7   -- 当前事务 ID

对于不同的版本,判断可见性:

sql
-- 情况 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。

sql
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

-- 事务 A
BEGIN;
SELECT price FROM product WHERE id = 1;
-- 直接读取最新值,即使是未提交的修改

特点: 没有任何隔离保护,性能最高但数据一致性最差。

READ COMMITTED(RC)

实现方式: 每次 SELECT 语句执行时都生成新的 ReadView。

sql
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;

工作流程:

mermaid
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。

sql
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;

工作流程:

mermaid
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读取最新版本,加锁

示例代码:

sql
-- 快照读(不加锁)
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(间隙锁):锁定索引记录之间的间隙
mermaid
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_iduser_idamount
10110100
10220200
10330300
sql
-- 事务 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),写操作加排他锁。

sql
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:强制串行化

实现原理总结:

mermaid
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 和锁机制的协同工作实现了不同的隔离级别:

  1. MVCC 提供高效的快照读,避免读写冲突
  2. ReadView 控制版本可见性,实现不同隔离级别的语义
  3. 锁机制 保护当前读和写操作,防止并发冲突
  4. Next-Key Lock 在 RR 级别下防止幻读

理解这些机制,能够帮助我们:

  • 根据业务需求选择合适的隔离级别
  • 优化查询性能(优先使用快照读)
  • 避免死锁和锁等待问题
  • 设计更可靠的并发控制方案

更新: 2025-12-04 17:37:41
原文: https://www.yuque.com/u22210564/zoxfmt/doc-01-mysql-11-mysql-14

Java 后端面试知识库