跳到主要内容

MySQL隔离级别实现原理

MySQL 如何实现不同隔离级别

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

隔离级别实现机制总览

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

MVCC(多版本并发控制)

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

MVCC 的应用场景

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

  1. 读-读并发:多个事务同时读取数据,不会出现问题
  2. 写-写并发:通过加锁机制处理,保证数据一致性
  3. 读-写并发:通过 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 LogReadView

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;

形成的版本链:

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

详细判断示例:

假设有一个 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;

工作流程:

特点: 避免脏读,但会出现不可重复读和幻读。

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;

工作流程:

特点: 避免脏读和不可重复读,快照读场景下避免幻读。

锁机制与幻读处理

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(间隙锁):锁定索引记录之间的间隙

示例场景:

假设订单表有索引 (user_id),当前记录:

order_iduser_idamount
10110100
10220200
10330300
-- 事务 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:强制串行化

实现原理总结:

总结

MySQL 通过 MVCC 和锁机制的协同工作实现了不同的隔离级别:

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

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

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