谈谈MYSQL的MVCC的实现

Posted by maybelence on 2022-03-04

在讲这篇文章之前,我们先来讲一下 JAVA 的 CAS。 CAS 本身的一种乐观锁的实现,可以看一下之前的文章百问不厌的乐观锁和悲观锁

JAVA 实现 CAS 实基于一个魔法类 Unsafe 来实现的。这个类里面的方法都是一些 native 方法。也就是说 JAVA 对于乐观锁的实现还是依靠于操作系统以及 CPU 。

CAS 里面我们有一个经典的问题,ABA 问题。如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 ABA 问题。

ABA 问题的解决办法是在变量前面追加上版本号或者时间戳。JDK 1.5 以后的 AtomicStampedReference 类就是用来解决 ABA 问题的,其中的 compareAndSet() 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

1
2
3
4
5
6
7
8
9
10
11
12
public boolean compareAndSet(V   expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}

这里我讲 CAS 是觉得变量前面追加上版本号或者时间戳与 MVCC 有异曲同工之处。虽然解决的问题不同,但是方法很相似。

MVCC 中对于数据可见性,基于 ReadView 和隐藏字段控制。本质也是相当于给数据头加上类似于版本号的形式。来保证每个事物只能看到其可见的版本。

Mysql 中 InnoDb 存储引擎中实现 MVCC 主要依赖 隐藏字段Read Viewundo log。在内部实现中,InnoDB 通过数据行的 DB_TRX_ID 和 Read View 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR 找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View 之前已经提交的修改和该事务本身做的修改。

DB_TRX_ID 作为隐藏字段之一,表示最后一次插入或更新该行的事务 id。此外,delete 操作在内部被视为更新,只不过会在记录头 Record header 中的 deleted_flag 字段将其标记为已删除。

隐藏字段中还有一个很重要的字段,DB_ROLL_PTR 回滚指针,指向该行的 undo log 。如果该行未被更新,则为空。

ReadView 主要做一些可见性判断,里面保存了创建该 ReadView 的事务Id ,以及不可见的其他活跃事务。我理解这里的事务Id算是一种版本标记。

MVCC中还有一个比较重要的一个组成部分:undo log。当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过 undo log 读取之前的版本数据,以此实现非锁定读。

最后复习一下 Read View 在读取已提交和可重复读场景下的生成时机问题。

  • 在 RC 隔离级别下的 每次select 查询前都生成一个Read View (m_ids 列表)
  • 在 RR 隔离级别下只在事务开始后 第一次select 数据前生成一个Read View(m_ids 列表)

Copyright by @maybelence.

...

...

00:00
00:00