MySQL隔离级别实现原理
MySQL 如何实现不同隔离级别
MySQL 通过 多版本并发控制(MVCC) 和 锁机制 的组合实现了不同的事务隔离级别。不同的隔离级别采用不同的并发控制策略,从而在数据一致性和系统性能之间取得平衡。
隔离级别实现机制总览
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现机制 |
|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 直接读取最新数据(无版本控制,无锁) |
| READ COMMITTED | 不可能 | 可能 | 可能 | 每次查询生成新 ReadView |
| REPEATABLE READ | 不可能 | 不可能 | 可能 | 事务开始时生成 ReadView + Next-Key Lock |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 | 所有读操作加共享锁,写操作加排他锁 |
MVCC(多版本并发控制)
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;
形成的版本链:
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 UPDATESELECT ... LOCK IN SHARE MODEUPDATE / 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_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:强制串行化 |
实现原理总结:
总结
MySQL 通过 MVCC 和锁机制的协同工作实现了不同的隔离级别:
- MVCC 提供高效的快照读,避免读写冲突
- ReadView 控制版本可见性,实现不同隔离级别的语义
- 锁机制 保护当前读和写操作,防止并发冲突
- Next-Key Lock 在 RR 级别下防止幻读
理解这些机制,能够帮助我们:
- 根据业务需求选择合适的隔离级别
- 优化查询性能(优先使用快照读)
- 避免死锁和锁等待问题
- 设计更可靠的并发控制方案