隔离性 Isolation 实现机制

隔离性 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
喜欢就支持一下吧
点赞7 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容