MVCC实现原理

发布于:2025-03-13 ⋅ 阅读:(17) ⋅ 点赞:(0)

一、引言

在现代数据库管理系统中,数据的一致性和并发性是两个至关重要的特性。传统的锁机制虽然有效,但也存在着性能瓶颈,特别是在高并发环境下,锁的争用会导致系统响应时间变慢,甚至引发死锁等问题。为了克服这些挑战,多版本并发控制(MVCC,Multi-Version Concurrency Control)技术应运而生,成为了处理并发事务的一种非常有效的解决方案。本文将深入探讨MVCC的实现原理。我们先来思考一个问题,为什么innDB会需要MVCC?它可以解决什么问题呢?

二、为什么需要MVCC?

在开始这个话题之前,我们先来看看官方给出的MVCC定义:

MVCC多版本并发控制

Multi-Version Concurrency Control 多版本并发控制,MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问;在编程语言中实现事务内存

通过官方的定义我们知道了MVCC是控制并发的一种方法,我们在代码层面去解决并发问题就是通过锁的方式去解决,同样我们数据库也提供了各种各样的锁。知道锁本身就是用于并发控制的,那么为什么 InnoDB 还需要引入 MVCC,读写都加锁不就可以控制住并发吗?

锁确实可以,但是性能太差。如果是纯粹的锁,那么写和写、读和写、读和读之间都是互斥 的。如果是读写锁,那么写和写、读和写之间依旧是互斥的。

数据库和一般的应用有一个很大的区别,就是数据库即便是读,也不能被写阻塞住。试想一 下,如果一个线程准备执行 UPDATE 一行数据,如果这时候阻塞住了所有的 SELECT 语句,这个性能是我们无法接受的。所以数据库要有一种机制,避免读写阻塞。在理解了为什么 MVCC 必不可少 之后,现在我们需要进一步了解一个和 MVCC 紧密关联的概念:隔离级别

三、事务隔离级别

数据库的隔离级别是一组规则,用来控制并发访问数据库时如何分配、保护和共享资源。不同 的隔离级别在不同的并发控制策略之间进行调整,从而提供了不同的读写隔离级别和安全性。 用人话来说,就是隔离级别代表了一个事务是否了解别的事务以及了解程度怎么样。MySQL 支持的事务隔离级别分别为 读未提交READ UNCOMMITTED)读已提交READ COMMITTED)可重复读REPEATABLE READ)串行化SERIALIZABLE)。每个级别决定了事务在处理并发时的行为,并定义了事务如何处理并发事务带来的四种常见问题:脏读(Dirty Read)不可重复读(Non-repeatable Read)幻读(Phantom Read)锁争用(Lock Contention)。

3.1 读未提交

在该隔离级别下,一个事务可以读取到另一个事务还没有提交的数据。这是最低级别的事务隔离,性能最好,但数据一致性最差。

3.2 读已提交

读已提交(Read Committed,简写 RC)是指一个事务只能看到已经提交的事务的修改。 这意味着如果在事务执行过程中有别的事务提交了,那么事务还是能够看到别的事务最新提交的修改。

3.3 可重复读

可重复读(Repeatable Read,简写 RR)是指在这一个事务内部读同一个数据多次,读到 的结果都是同一个。这意味着即便在事务执行过程中有别的事务提交,这个事务依旧看不到 别的事务提交的修改。这是 MySQL 默认的隔离级别。

3.4 串行化

是指事务对数据的读写都是串行化的。

从上到下,隔离性变强但是性能变差了。所以一个提升 MySQL 性能最简单的方式,就是将隔 离级别往下调。

不同的事务隔离级别也会出现各种不同的问题主要有以下几种:

  • 脏读是指读到了别的事务还没有提交的数据。之所以叫做“脏”读,就是因为未提交数据可 能会被回滚掉。
  • 不可重复读是指在一个事务执行过程中,对同一行数据读到的结果不同。
  • 幻读是指在事务执行过程中,别的事务插入了新的数据并且提交了,然后事务在后续步骤中 读到了这个新的数据。
隔离级别 脏读 不可重复读 幻读 性能
读未提交 允许 允许 允许 最高
读已提交 不允许 允许 允许 较高
可重复读 不允许 不允许 允许 默认
串行化 不允许 不允许 不允许 最低

四、当前读、快照读

在了解MVCC是如何解决事务并发带来的问题之前,需要先明白俩个概念,当前读、快照读。

4.1 当前读(Current Read)

定义:当前读是指读取数据时,事务会对正在读取的数据加锁,保证其他事务不能修改该数据。通常,当前读是为了避免并发事务之间的数据冲突。

特性:

  • 加锁:当前读会加锁,通常是 行锁,比如使用 SELECT FOR UPDATE 或 SELECT LOCK IN SHARE MODE 语句。这种锁定会防止其他事务对相同数据行进行修改,确保事务中的读取操作得到保护。
  • 事务一致性:通过加锁保证数据的一致性,但会导致其他事务的阻塞,影响并发性。
  • 发生场景:通常在事务中更新、删除或插入数据时,MySQL 会执行当前读操作。
-- 当前读示例,会加锁防止其他事务修改这行数据
SELECT * FROM table_name WHERE id = 1 FOR UPDATE;

在执行该查询时,MySQL 会锁住id = 1 这一行数据,直到当前事务提交,其他事务不能修改该行数据。

4.2 快照读(Snapshot Read)

定义:快照读是指事务读取的数据是某个时间点的数据快照,不会受其他事务提交或回滚的影响。即便其他事务在该事务执行期间修改了数据,当前事务仍然会看到自己读取时的快照数据。简单来说,快照读就是在事务开始的时候创建了 一个数据的快照,在整个事务过程中都读这个快照。

特性:

  • 不加锁:快照读通常不加锁,而是通过 多版本并发控制(MVCC) 来实现。在 MySQL 中,InnoDB 存储引擎使用 MVCC 来提供快照读操作,确保事务读取的数据在整个事务过程中保持一致。
  • 避免阻塞:由于快照读不加锁,它允许更高的并发性,减少了事务间的阻塞。
  • 一致性视图:事务读取数据时,会看到一个一致的视图(即快照),这个视图是事务开始时的数据状态,其他事务提交的更改对该事务不可见。
-- 快照读示例,读取数据不会加锁,看到的是事务开始时的数据快照
SELECT * FROM table_name WHERE id = 1;

 在执行该查询时,如果事务开始时 id = 1 的数据为 100,即使其他事务在这期间更新了该数据,当前事务依然会看到 100 这一数据,不会受到影响。

区别总结:

特性 当前读(Current Read) 快照读(Snapshot Read)
是否加锁 是(行级锁) 否(使用 MVCC)
并发性 较低,会造成事务间的阻塞 较高,不会阻塞其他事务
数据一致性 保证事务读取的数据在整个事务期间不变 保证事务读取的数据在事务开始时一致
影响其他事务 会阻塞其他事务,直到事务提交 不会影响其他事务
适用场景 需要确保读取的数据在事务期间不被修改 只需要读取一致性视图的场景

五、MVCC实现原理

在这之前需要知道MVCC只在读已提交(Read Committed,简写 RC)可重复读(Repeatable Read,简写 RR)这俩种隔离级别下适用。

MVCC实现原理是由俩个隐式字段、undo日志、Read view来实现的。

5.1 隐式字段

为了实现 MVCC,InnoDB 引擎给每一行都加了两个额外的字段 trx_id 和 roll_ptr。

trx_id:事务 ID,也叫做事务版本号。MVCC 里面的 V 指的就是这个数字。每一个事务在 开始的时候就会获得一个 ID,然后这个事务内操作的行的事务 ID,都会被修改为这个事务 的 ID。

roll_ptr:回滚指针。InnoDB 通过 roll_ptr 把每一行的历史版本串联在一起。

5.2 undo log

undo log细分为俩种,insert时产生的undo log、update,delete时产生的undo log

在Innodb中insert产生的undo log在提交事务之后就会被删除,因为新插入的数据没有历史版本,所以无需维护undo log。

update和delete操作产生的undo log都属于一种类型,在事务回滚时需要,而且在快照读时也需要,则需要维护多个版本信息。只有在快照读和事务回滚不涉及该日志时,对应的日志才会被purge线程统一删除。

purge线程会清理undo log的历史版本,同样也会清理del flag标记的记录。

5.2.1 undo log在mvcc中的作用

写到这里关于undo log在mvcc中的作用估计还是蒙圈的。

undo log保存的是一个版本链,也就是使用DB_ROLL_PTR这个字段来连接的。

当数据库执行一个select语句时会产生一致性视图Read View。

那么这个Read View是由查询时所有未提交事务ID组成的数组,数组中最小的事务ID为min_id和已创建的最大事务ID为max_id组成,查询的数据结果需要跟Read View比较从而得到快照结果。

所以说undo log在mvcc中的作用就是为了根据存储的事务ID和一致性视图做对比,从而得到快照结果。

5.3 Read View

Read View 你可以理解成是一种可见性规则。前面你已经知道了 undolog 里面存放着历史版 本的数据,当事务内部要读取数据的时候,Read View 就被用来控制这个事务应该读取哪个 版本的数据。

Read View 最关键的字段叫做 m_ids,它代表的是当前已经开始,但是还没有结束的事务的 ID,也叫做活跃事务 ID。

Read View 只用于读已提交可重复读两个隔离级别,它用于这两个隔离级别的不同点就在 于什么时候生成 Read View。

读已提交:事务每次发起查询的时候,都会重新创建一个新的 Read View。

可重复读:事务开始的时候,创建出 Read View。

5.3.1 Read View 与读已提交

在读已提交的隔离级别下,每一次查询语句都会重新生成一个 Read View。这意味着在事务执行过程中,Read View 是在不断变动的。现在我们来看一个例子,假如说现在已经有三个事务了,状态分别是已提交、未提交、未提交。

假如说现在新开了一个事务 A,分配给它的 ID 是 4。如果这个时候 A 开始查询 x 的值,那么 MySQL 会创建一个新的 Read View,其中 m_ids = 2,3。事务 A 发现最后一个已经提交 的是事务 trx_id = 1,对应的 x 的值是 3。于是事务 A 读到 x = 3。

如果这个时候事务 2 提交了,事务 A 再次读取 x,这个时候 MySQL 又会生成一个新的 Read View m_ids=3。因此事务 A 会读取到 x = 4。

5.3.2 Read View 与可重复读

在可重复读的隔离级别下,数据库会在事务开始的时候生成一个 Read View。这意味着整个 Read View 在事务执行过程中都是稳定不变的。我们用前面的例子来说明,就是在事务 A 开 始的时候就会创建出来一个 Read View m_ids=2,3。

如果这时候事务 A 去读 x 的数据,毫无疑问,读出来的是 x=3。

如果这时候事务 2 提交了,然后事务 A 想要再去读 x 的值,Read View 不会发生变化,还是 m_ids = 2,3。所以你可以看到,虽然事务 2 提交了,但是事务 A 完全不知道这回事,因此它还是读到 x=3。

六、小结

MVCC(多版本并发控制)通过为每个数据项维护多个版本来解决并发事务之间的冲突问题,从而提升数据库的并发性能。它的核心思想是通过为每个事务提供一致的视图,确保读取操作不会受到其他事务的影响。MVCC不依赖于传统的锁机制,减少了锁竞争,提高了并发性,尤其适合读多写少的应用场景。