事务的隔离级别与MVCC
事务隔离级别与 MVCC 的详细讲解
1. 事务并发执行遇到的问题
在多个事务并发执行时,如果不加控制,可能会产生以下问题(按严重性排序):
脏写(Dirty Write):如果一个事务修改了另一个未提交事务修改过的数据。这是最严重的问题,任何隔离级别都不允许发生。
脏读(Dirty Read):如果一个事务读到了另一个未提交事务修改过的数据。
不可重复读(Non-Repeatable Read):如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值。
幻读(Phantom):如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来。
注:幻读重点强调读取到了之前读取没有获取到的记录。
2. SQL 标准与 MySQL 中的隔离级别
SQL 标准设立了 4 个隔离级别,针对不同级别,并发事务可能发生的问题如下:
MySQL 的特殊情况:
MySQL 支持上述 4 种隔离级别。
默认隔离级别为
REPEATABLE READ。与 SQL 标准不同,MySQL 在
REPEATABLE READ隔离级别下,是可以禁止幻读问题的发生的。
设置隔离级别:
可以通过以下语句修改事务隔离级别:
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;
level 可选值:REPEATABLE READ, READ COMMITTED, READ UNCOMMITTED, SERIALIZABLE。
GLOBAL:只对执行完该语句之后产生的会话起作用。SESSION:对当前会话的所有后续事务有效。都不加:只对当前会话中下一个即将开启的事务有效。
3. MVCC(多版本并发控制)原理
MVCC 是指在使用 READ COMMITTED、REPEATABLE READ 这两种隔离级别的事务在执行普通的 SELECT 操作时访问记录的版本链的过程。这使得不同事务的读 - 写、写 - 读操作并发执行,从而提升系统性能。
3.1 隐藏列与版本链
对于使用 InnoDB 存储引擎的表,聚簇索引记录中包含两个必要的隐藏列:
trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务 id 赋值给trx_id隐藏列。roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo 日志中,这个隐藏列相当于一个指针,可以通过它找到该记录修改前的信息。
每次对记录进行改动,都会记录一条 undo 日志,每条 undo 日志也都有一个 roll_pointer 属性(INSERT 操作对应的 undo 日志没有该属性)。可以将这些 undo 日志连起来,串成一个链表,称之为版本链。版本链的头节点就是当前记录最新的值。
3.2 ReadView
为了判断版本链中的哪个版本是当前事务可见的,InnoDB 提出了 ReadView 的概念。ReadView 主要包含 4 个重要内容:
m_ids:表示在生成 ReadView 时当前系统中活跃的读写事务的事务 id 列表。min_trx_id:表示在生成 ReadView 时当前系统中活跃的读写事务中最小的事务 id。max_trx_id:表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。creator_trx_id:表示生成该 ReadView 的事务的事务 id。
可见性判断规则:
在访问某条记录时,按照以下步骤判断记录的某个版本是否可见:
如果被访问版本的
trx_id属性值与 ReadView 中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,该版本可见。如果被访问版本的
trx_id属性值小于 ReadView 中的min_trx_id值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,该版本可见。如果被访问版本的
trx_id属性值大于 ReadView 中的max_trx_id值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,该版本不可见。如果被访问版本的
trx_id属性值在 ReadView 的min_trx_id和max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中:如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可见。
如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可见。
如果某个版本的数据对当前事务不可见,那就顺着版本链找到下一个版本的数据,继续判断,直到版本链中的最后一个版本。
3.3 READ COMMITTED 与 REPEATABLE READ 的区别
这两个隔离级别的核心区别在于生成 ReadView 的时机不同:
READ COMMITTED (RC):每次读取数据前都生成一个 ReadView。
在执行
SELECT语句时会先生成一个 ReadView。这意味着每次查询都能读到其他事务最新提交的数据。
REPEATABLE READ (RR):在第一次读取数据时生成一个 ReadView。
只会在第一次执行查询语句时生成一个 ReadView,之后的查询就不会重复生成了,而是复用之前的 ReadView。
这意味着在整个事务期间,读取到的数据保持一致,避免了不可重复读。
4. 幻读问题的解决
在 MySQL 中,REPEATABLE READ 隔离级别下幻读问题的解决分为两种情况:
快照读(普通 SELECT):
利用 MVCC 解决。由于 RR 级别下 ReadView 只在第一次生成后复用,后续查询看不到其他事务新插入并提交的数据(因为新事务的
trx_id大于 ReadView 的max_trx_id或在m_ids中),从而避免了幻读。
当前读(锁定读或写操作):
利用 锁(Locking Reads) 解决。
对于
SELECT ... LOCK IN SHARE MODE或SELECT ... FOR UPDATE以及UPDATE、DELETE等操作,InnoDB 会使用 Gap Locks(间隙锁) 或 Next-Key Locks。Gap Locks:锁住记录前的间隙,不允许别的事务在该间隙插入新记录。
Next-Key Locks:记录锁 + 间隙锁的合体,既能保护该条记录,又能阻止别的事务将新记录插入被保护记录前边的间隙。
5. 一致性读与锁定读
一致性读(Consistent Reads):
事务利用 MVCC 进行的读取操作,也称为快照读。
所有普通的
SELECT语句在READ COMMITTED、REPEATABLE READ隔离级别下都算是一致性读。一致性读并不会对表中的任何记录做加锁操作。
锁定读(Locking Reads):
读操作也采用加锁的方式。
SELECT ... LOCK IN SHARE MODE:为读取到的记录加 S 锁(共享锁)。SELECT ... FOR UPDATE:为读取到的记录加 X 锁(独占锁)。这种读取方式读取的是记录的最新版本,而不是历史版本。
6. Purge 机制
insert undo:在事务提交之后就可以被释放掉。
update undo:由于还需要支持 MVCC,不能立即删除掉。
为了支持 MVCC,对于
delete mark操作来说,仅仅是在记录上打一个删除标记,并没有真正将它删除掉。随着系统的运行,在确定系统中包含最早产生的那个 ReadView 的事务不会再访问某些 update undo 日志以及被打上删除标记的记录后,有一个后台运行的 purge 线程 会把它们真正的删除掉。
总结
事务隔离级别决定了并发事务之间可见性的程度,MySQL 默认使用
REPEATABLE READ。MVCC 通过隐藏列(
trx_id,roll_pointer)、版本链和 ReadView 机制,实现了非阻塞的读操作,解决了脏读和不可重复读问题,并在快照读场景下解决了幻读问题。RC 与 RR 的核心区别在于 ReadView 的生成时机(每次查询 vs 第一次查询)。
对于当前读(锁定读)场景下的幻读问题,InnoDB 通过 间隙锁(Gap Lock) 和 Next-Key 锁 来解决。