MySQL事务隔离级别与MVCC实现原理
2024.11.13
7 min
3.0k 字
// 目录 · contents
引言
事务是数据库区别于文件系统的核心特性之一。在高并发场景下,如何在保证数据一致性的同时提供良好的并发性能,是数据库设计的核心挑战。MySQL InnoDB引擎通过MVCC(多版本并发控制)机制,在不加锁的情况下实现了事务的隔离,极大地提升了并发性能。
ACID 特性
事务的四个基本特性(ACID)是理解事务隔离的基础:
graph LR
A[ACID] --> B[Atomicity<br/>原子性]
A --> C[Consistency<br/>一致性]
A --> D[Isolation<br/>隔离性]
A --> E[Durability<br/>持久性]
B --> B1[undo log]
C --> C1[其他三者共同保证]
D --> D1[MVCC + 锁机制]
E --> E1[redo log]
| 特性 | 实现机制 | 说明 |
|---|---|---|
| 原子性(Atomicity) | undo log | 事务中的操作要么全部成功,要么全部回滚 |
| 一致性(Consistency) | 约束 + AID | 事务前后数据库的完整性约束不被破坏 |
| 隔离性(Isolation) | MVCC + 锁 | 并发事务之间互不干扰 |
| 持久性(Durability) | redo log | 事务提交后数据不会丢失 |
事务隔离级别
SQL标准定义了四种隔离级别,它们在并发性能和数据一致性之间做出不同的权衡:
四种隔离级别与并发问题
graph TD
subgraph 隔离级别从低到高
RU[READ UNCOMMITTED<br/>读未提交]
RC[READ COMMITTED<br/>读已提交]
RR[REPEATABLE READ<br/>可重复读]
SE[SERIALIZABLE<br/>串行化]
end
subgraph 并发问题
D1[脏读]
D2[不可重复读]
D3[幻读]
end
RU -.->|存在| D1
RU -.->|存在| D2
RU -.->|存在| D3
RC -.->|存在| D2
RC -.->|存在| D3
RR -.->|部分存在| D3
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不可能 | 可能 | 可能 |
| REPEATABLE READ(InnoDB默认) | 不可能 | 不可能 | 部分解决 |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 |
并发问题演示
1 | |
脏读示例:
1 | |
不可重复读示例:
1 | |
幻读示例:
1 | |
MVCC 实现原理
MVCC(Multi-Version Concurrency Control)的核心思想是:为每行数据维护多个版本,读操作读取的是某个时间点的快照,而不是当前最新数据,从而实现读写不冲突。
隐藏列
InnoDB为每行数据自动添加三个隐藏列:
1 | |
Undo Log 版本链
每次对数据行的修改都会生成一条undo log记录,多条undo
log通过DB_ROLL_PTR串联形成版本链。
graph LR
subgraph 当前数据
A["id=1, name='Dave', age=28<br/>DB_TRX_ID=300<br/>DB_ROLL_PTR → "]
end
subgraph Undo Log
B["id=1, name='Carol', age=25<br/>DB_TRX_ID=200<br/>DB_ROLL_PTR → "]
C["id=1, name='Bob', age=22<br/>DB_TRX_ID=100<br/>DB_ROLL_PTR → "]
D["id=1, name='Alice', age=20<br/>DB_TRX_ID=50<br/>DB_ROLL_PTR = NULL"]
end
A --> B
B --> C
C --> D
版本链的形成过程:
1 | |
ReadView 机制
ReadView是MVCC实现快照读的关键。当事务执行快照读(普通SELECT)时,会生成一个ReadView,用来判断数据版本的可见性。
ReadView包含以下关键字段:
1 | |
可见性判断算法
flowchart TD
A[获取数据行的 DB_TRX_ID] --> B{trx_id == creator_trx_id?}
B -->|是| C[可见 - 自己修改的数据]
B -->|否| D{trx_id < min_trx_id?}
D -->|是| E[可见 - 在ReadView创建前已提交]
D -->|否| F{trx_id >= max_trx_id?}
F -->|是| G[不可见 - 在ReadView创建后开始的事务]
F -->|否| H{trx_id 在 m_ids 中?}
H -->|是| I[不可见 - 该事务还未提交]
H -->|否| J[可见 - 该事务已经提交]
I --> K[沿版本链找上一个版本<br/>重复判断]
G --> K
RC 与 RR 的 ReadView 差异
两种隔离级别的核心区别在于生成ReadView的时机:
- READ COMMITTED:每次执行SELECT都会重新生成一个ReadView
- REPEATABLE READ:只在事务中第一次执行SELECT时生成ReadView,后续复用
1 | |
间隙锁(Gap Lock)与幻读解决
在RR隔离级别下,InnoDB通过间隙锁和临键锁(Next-Key Lock)在当前读场景下解决幻读问题。
锁的类型
graph TD
A[InnoDB行级锁] --> B[Record Lock<br/>记录锁]
A --> C[Gap Lock<br/>间隙锁]
A --> D[Next-Key Lock<br/>临键锁]
B --> B1[锁定索引记录本身]
C --> C1[锁定索引记录之间的间隙]
D --> D1[Record Lock + Gap Lock<br/>锁定记录及其前面的间隙]
Next-Key Lock 示例
假设表中有记录 id: 1, 5, 10, 15, 20,索引间隙为:
1 | |
1 | |
等值查询的加锁规则
1 | |
快照读与当前读
理解MVCC,必须区分两种读取方式:
1 | |
graph TD
A[SELECT语句] --> B{是否加锁?}
B -->|否| C[快照读<br/>MVCC ReadView]
B -->|是| D[当前读<br/>加锁读取最新版本]
D --> E{FOR UPDATE?}
E -->|是| F[排他锁 X Lock]
E -->|否| G{FOR SHARE?}
G -->|是| H[共享锁 S Lock]
I[INSERT/UPDATE/DELETE] --> D
事务相关的实践建议
死锁检测与处理
1 | |
减少锁冲突的最佳实践
1 | |
长事务的危害
1 | |
长事务的危害: 1. Undo log膨胀:长事务会阻止undo log的回收,导致存储空间持续增长 2. 锁占用时间长:增加锁冲突和死锁的概率 3. MVCC版本链过长:影响查询性能,需要遍历更多版本
总结
MySQL InnoDB的事务隔离机制是一个精巧的工程设计:
- MVCC通过undo log版本链和ReadView机制,实现了快照读的无锁并发,极大提升了读写并发性能
- RC和RR的核心区别在于ReadView的生成时机——RC每次SELECT生成,RR只在第一次SELECT生成
- 间隙锁和临键锁在当前读场景下解决幻读问题,但也增加了死锁的风险
- 实际使用中,需要合理设计事务,避免长事务,按固定顺序访问资源以减少死锁
- InnoDB默认的RR隔离级别在大多数场景下提供了良好的一致性与并发性平衡,但在某些高并发场景下,RC级别可能更合适(减少间隙锁冲突)
$ echo "comments" · 评论