隔离性 Isolation 实现机制
什么是隔离性
隔离性保证多个事务并发执行时,一个事务的中间状态对其他事务不可见。
-- 事务 A:正在转账
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 还没提交
-- 事务 B:查询余额
SELECT balance FROM accounts WHERE id = 1;
-- 应该看到旧值还是 -100 之后的值?
-- 答案由隔离性决定
InnoDB 通过两种机制共同实现隔离性:
MVCC(多版本并发控制):控制读操作看到的版本
Lock(锁机制): 控制写操作的并发访问
MVCC:多版本并发控制
核心概念
MVCC 的核心思想是:保留数据的多个版本,每个事务看到自己”应该看到”的版本。
-- 表数据
CREATE TABLE user (id INT, name VARCHAR(50));
-- 原始数据:id=1, name='张三'
-- 时间线:
-- T1: 事务 A 开始
-- T2: 事务 A 修改 name='李四'(未提交)
-- T3: 事务 B 开始
-- T4: 事务 B 查询 id=1(应该看到 '张三' 还是 '李四'?)
-- MVCC 的答案:事务 B 看到 '张三'(原始版本)
-- 事务 B 有自己的 Read View,"看不到"未提交的修改
隐藏列
InnoDB 中每行数据有 3 个隐藏列:
┌──────────────────────────────────────────┐
│ 行数据 │
│ ┌─────────┬─────────┬────────────────┐ │
│ │ 业务列 │ DB_TRX_ID │ DB_ROLL_PTR │ │
│ │ (name等) │ 事务ID │ undo log 指针 │ │
│ └─────────┴─────────┴────────────────┘ │
└──────────────────────────────────────────┘
1. DB_TRX_ID:最近修改该行的事务 ID
2. DB_ROLL_PTR:指向 undo log 的指针(用于构建旧版本)
3. DB_ROW_ID:隐式自增 ID(如果没有主键时使用)
Read View(读视图)
每个事务在第一次读时创建一个 Read View,决定”能看到哪些版本”。
Read View 包含:
├─ creator_trx_id:创建这个 Read View 的事务 ID
├─ m_ids:当前活跃(未提交)的事务 ID 列表
├─ min_trx_id:活跃事务中的最小 ID
└─ max_trx_id:下一个要分配的事务 ID
判断规则:
1. 行的 DB_TRX_ID == creator_trx_id → 自己改的,可见
2. 行的 DB_TRX_ID < min_trx_id → 已提交的旧版本,可见
3. 行的 DB_TRX_ID >= max_trx_id → 将来的事务,不可见
4. trx_id ∈ m_ids → 活跃事务的修改,不可见(需去 undo 链找旧版本)
可见性判断流程
SELECT id=1时:
行版本链
│
当前版本: '李四' (trx_id=100)
│ ← DB_ROLL_PTR
旧版本: '张三' (trx_id=50)
│
更旧版本: ...
事务 B 的 Read View:
creator_trx_id = 200
m_ids = [100, 101, 200]
min_trx_id = 100
max_trx_id = 201
判断流程:
① 当前版本 trx_id=100
→ 100 ∈ m_ids → 活跃事务的修改 → 不可见
② 沿着 undo 链找下一个版本
→ 旧版本 trx_id=50
→ 50 < min_trx_id(100) → 已提交的旧版本 → 可见!
③ 返回 '张三'
锁机制
行级锁
-- 隐式加锁(InnoDB 自动加)
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 自动对 id=1 的行加 X 锁(排他锁)
DELETE FROM accounts WHERE id = 1;
-- 自动对 id=1 的行加 X 锁
-- 显式加锁
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- X 锁
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE; -- S 锁(共享锁)
锁的类型
行级锁:
├─ 共享锁(S Lock):读读不冲突
│ SELECT ... LOCK IN SHARE MODE
│
├─ 排他锁(X Lock):读写都冲突
│ SELECT ... FOR UPDATE
│ UPDATE / DELETE / INSERT
│
├─ 间隙锁(Gap Lock):防止幻读
│
├─ 临键锁(Next-Key Lock):行锁 + 间隙锁
│
└─ 插入意向锁(Insert Intention Lock):间隙插入专用
表级锁:
├─ 意向共享锁(IS):表示事务准备加 S 锁
├─ 意向排他锁(IX):表示事务准备加 X 锁
└─ 自增锁(AUTO-INC Lock)
死锁
-- 事务 A
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 锁 id=1
-- 等待事务 B 释放 id=2
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 等待锁 id=2
-- 事务 B
START TRANSACTION;
UPDATE accounts SET balance = balance - 50 WHERE id = 2; -- 锁 id=2
-- 等待事务 A 释放 id=1
UPDATE accounts SET balance = balance + 50 WHERE id = 1; -- 等待锁 id=1
-- 死锁发生!
-- InnoDB 检测到死锁,选择回滚代价较小的事务
-- 抛出 ERROR 1213 (40001): Deadlock found
隔离级别与实现的关系
| 隔离级别 | 实现机制 | 现象 |
|---|---|---|
| READ UNCOMMITTED | 不创建 Read View,直接读最新版本 | 脏读 |
| READ COMMITTED | 每次 SELECT 都创建 Read View | 不可重复读 |
| REPEATABLE READ | 事务开始后只创建一次 Read View | 幻读(InnoDB 用间隙锁解决) |
| SERIALIZABLE | 所有 SELECT 隐式转为 LOCK IN SHARE MODE | 无并发问题 |
面试要点
- 隔离性两大实现机制:MVCC(控制可见性) + 锁(控制并发写)
- MVCC 核心:隐藏列(DB_TRX_ID + DB_ROLL_PTR)+ Read View 判断可见性
- Read View 是关键:不同隔离级别区别在于 Read View 的创建时机
- 行锁:InnoDB 在 UPDATE/DELETE 时自动加 X 锁
- 间隙锁:REPEATABLE READ 下防止幻读,也是死锁的常见原因
- 死锁:InnoDB 自动检测并回滚一个事务
一句话总结:MySQL 通过 MVCC 实现"读不阻塞写、写不阻塞读"的高并发,同时用行锁+间隙锁防止写冲突和幻读——这是隔离性的两套武器。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END


暂无评论内容