事务的各个隔离级别是如何实现的
1. 锁机制 (Locking)
锁是实现隔离性的传统方式,其核心思想是当一个事务在访问某些数据时,阻止其他事务进行不兼容的操作。
-
锁的类型:
- 共享锁 (Shared Lock, S锁):也叫读锁。多个事务可以同时持有同一份数据的共享锁,并读取该数据。但当数据上有S锁时,其他事务不能获取该数据的排他锁(X锁)。
- 排他锁 (Exclusive Lock, X锁):也叫写锁。如果一个事务持有了某份数据的排他锁,那么其他任何事务都不能再获取该数据的任何锁(无论是S锁还是X锁),直到持有锁的事务释放。这保证了同一时间只有一个事务能修改数据。
-
锁的粒度:
- 表级锁:锁定整张表,开销小,加锁快,但并发度最低。
- 行级锁:锁定数据行,开销大,加锁慢,但并发度最高,能最大程度支持并发处理。InnoDB存储引擎支持行级锁。
- 间隙锁 (Gap Lock):锁定一个范围,但不包括记录本身。主要用于防止幻读。
- 临键锁 (Next-Key Lock):是记录锁和间隙锁的组合,锁定一个左开右闭的区间。这是InnoDB在可重复读隔离级别下解决幻读的关键。
2. 多版本并发控制 (Multi-Version Concurrency Control, MVCC)
MVCC是InnoDB引擎在读已提交和可重复读隔离级别下处理读操作的核心机制。它的主要优势在于处理读-写冲突时无需加锁,实现了非阻塞的并发读取,极大地提升了数据库的并发性能。
MVCC的实现依赖于以下几个关键要素:
-
隐藏字段:InnoDB会为每行数据添加三个隐藏字段。
DB_TRX_ID:记录最近一次创建或修改该行数据的事务ID。DB_ROLL_PTR:回滚指针,指向该行记录的上一个版本,存储在Undo Log中。DB_ROW_ID:隐藏的行ID,当表没有主键时,InnoDB会用它来生成聚簇索引。
-
Undo Log(回滚日志):Undo Log存储了数据的旧版本。当一个事务修改数据时,它会将修改前的数据版本存入Undo Log,形成一个版本链。
DB_ROLL_PTR就是连接这个版本链的指针。 -
Read View(一致性视图):Read View是MVCC的核心,它决定了在当前事务中哪些数据版本是可见的。当事务启动时(或在特定隔离级别下,每条查询语句开始时),会创建一个Read View。 它主要包含以下内容:
m_ids:创建Read View时,当前系统中所有活跃(未提交)的事务ID列表。min_trx_id:m_ids列表中的最小事务ID。max_trx_id:创建Read View时,系统应该分配给下一个事务的ID。creator_trx_id:创建该Read View的事务的ID。
-
可见性判断规则:当一个事务访问某行数据时,会用自己的Read View与该行记录的
DB_TRX_ID进行比较,以判断该版本是否可见:- 如果
DB_TRX_ID等于creator_trx_id,说明是当前事务自己修改的,可见。 - 如果
DB_TRX_ID小于min_trx_id,说明修改该数据的事务在当前事务启动前就已经提交,可见。 - 如果
DB_TRX_ID大于或等于max_trx_id,说明修改该数据的事务在当前事务创建Read View之后才启动,不可见。 - 如果
DB_TRX_ID在min_trx_id和max_trx_id之间,则需要检查m_ids列表。如果DB_TRX_ID在m_ids中,说明修改该数据的事务在当前事务创建Read View时仍然活跃(未提交),因此不可见。如果不在m_ids中,说明已经提交,可见。
- 如果
如果某个版本的数据对当前事务不可见,它就会通过DB_ROLL_PTR回滚指针去Undo Log中查找上一个版本,然后重复上述可见性判断,直到找到一个可见的版本为止。
四大隔离级别的实现
1. 读未提交 (Read Uncommitted)
- 现象:允许脏读、不可重复读、幻读。
- 实现思路和原理:这是最简单的隔离级别,几乎没有隔离性。在此级别下,读取数据不会加任何锁,也不使用MVCC。 它直接读取数据行最新的版本,即使这个版本是由一个尚未提交的事务所修改的。因此,性能最好,但数据一致性最差。
2. 读已提交 (Read Committed)
- 现象:解决了脏读,但仍可能出现不可重复读、幻读。
- 实现思路和原理:这是大多数数据库(如Oracle、PostgreSQL)的默认隔离级别。它的实现主要依赖MVCC。
- 读操作 (SELECT):在事务中的每一条
SELECT语句执行前,都会创建一个新的Read View。 这意味着,如果事务A在两次查询之间,事务B恰好修改了数据并提交,那么事务A的第二个SELECT会生成一个新的Read View,此时就能看到事务B提交的修改。这就导致了“不可重复读”。由于Read View只能看到已提交事务的修改,因此“脏读”问题被解决。 - 写操作 (UPDATE, DELETE):写操作仍然需要加行级排他锁,以防止多个事务同时修改同一行数据。
- 读操作 (SELECT):在事务中的每一条
3. 可重复读 (Repeatable Read)
- 现象:解决了脏读、不可重复读,但理论上仍有幻读问题(InnoDB在很大程度上解决了)。 这是MySQL InnoDB引擎的默认隔离级别。
- 实现思路和原理:此级别的实现结合了MVCC和锁机制。
- 读操作 (快照读, 普通SELECT):只在事务开始后的第一次查询操作时创建一个Read View,并且整个事务期间都复用这一个Read View。 因为Read View是固定的,所以无论其他事务如何修改数据并提交,当前事务看到的始终是事务启动时的数据快照,从而保证了“可重复读”。
- 写操作 (当前读,
SELECT...FOR UPDATE,UPDATE,DELETE) 与幻读的解决:- 标准的MVCC只能解决快照读的幻读问题。对于需要加锁的“当前读”操作,InnoDB通过Next-Key Lock(临键锁)来解决幻读。
- 当一个事务执行当前读时,它不仅会锁定匹配到的记录(记录锁),还会锁定这些记录之间的“间隙”(间隙锁)。 这样一来,其他事务就无法在这个间隙中插入新的记录,从而防止了幻读的发生。
4. 串行化 (Serializable)
- 现象:解决了脏读、不可重复读和幻读所有问题。
- 实现思路和原理:这是最高级别的隔离,也是最影响并发性能的。
- 实现机制:此级别下,InnoDB不再使用MVCC进行非锁定读。它会为所有的读操作(SELECT)隐式地加上共享锁(S锁),为写操作加上排他锁(X锁)。
- 效果:当一个事务读取数据时,会加S锁,其他事务可以继续读取但不能修改。当一个事务写入数据时,会加X锁,其他事务既不能读也不能写。这种机制强制所有事务串行执行,一个接一个地处理,因此完全避免了并发问题,但并发性能也最低。
| 隔离级别 | 实现核心技术 | 读操作处理方式 | 写操作处理方式 | 解决的问题 | 存在的问题 |
|---|---|---|---|---|---|
| 读未提交 | 无锁 | 直接读取最新版本 | 加排他锁 | (无) | 脏读、不可重复读、幻读 |
| 读已提交 | MVCC | 每次SELECT创建新的Read View | 加排他锁 | 脏读 | 不可重复读、幻读 |
| 可重复读 | MVCC + Next-Key Lock | 事务开始时创建唯一Read View | 加排他锁和Next-Key Lock | 脏读、不可重复读、幻读(大部分) | (基本解决幻读) |
| 串行化 | 读写锁 | 所有读操作加共享锁 | 所有写操作加排他锁 | 脏读、不可重复读、幻读 | 并发性能差 |