目录
[MySQL(InnoDB 存储引擎)](#MySQL(InnoDB 存储引擎))
[1). 什么是 MVCC?它解决了什么问题?](#1). 什么是 MVCC?它解决了什么问题?)
[2). MySQL InnoDB 和 PostgreSQL 的 MVCC 实现有什么区别?](#2). MySQL InnoDB 和 PostgreSQL 的 MVCC 实现有什么区别?)
[3). MVCC 如何实现事务的可重复读?](#3). MVCC 如何实现事务的可重复读?)
[4). MVCC 中的 "快照读" 和 "当前读" 有什么区别?](#4). MVCC 中的 “快照读” 和 “当前读” 有什么区别?)
[5). MVCC 相比传统锁机制有什么优势?为什么读写可以不阻塞?](#5). MVCC 相比传统锁机制有什么优势?为什么读写可以不阻塞?)
[6). MVCC 会带来哪些问题?如何解决?](#6). MVCC 会带来哪些问题?如何解决?)
[7). MVCC 是如何保证事务的隔离级别的?以 "读提交" 和 "可重复读" 为例说明。](#7). MVCC 是如何保证事务的隔离级别的?以 “读提交” 和 “可重复读” 为例说明。)
[8). 在 MVCC 中,如何判断一个数据版本对当前事务是否可见?](#8). 在 MVCC 中,如何判断一个数据版本对当前事务是否可见?)
[9). MVCC 属于乐观锁还是悲观锁?为什么?](#9). MVCC 属于乐观锁还是悲观锁?为什么?)
[10). 为什么 InnoDB 在可重复读隔离级别下能避免幻读?](#10). 为什么 InnoDB 在可重复读隔离级别下能避免幻读?)
MVCC(Multi-Version Concurrency Control)即多版本并发控制 ,是数据库管理系统中用于处理并发访问的一种机制 ,主要应用于事务型数据库,用于提高并发性能,避免传统锁机制带来的性能开销。
以下从概念、实现方式、优点和缺点几个方面详细介绍 MVCC 机制。
1.基本概念
MVCC 通过保存数据在某个时间点的快照,允许多个事务 在同一时间 对同一数据 进行读写操作 而不会相互阻塞,每个事务看到的数据是特定版本的数据,就好像数据库在该事务开始时被冻结了一样。
2.实现方式
不同的数据库实现 MVCC 的方式有所不同,下面以常见的关系型数据库 MySQL(InnoDB 存储引擎)和 PostgreSQL 为例进行说明:
MySQL(InnoDB 存储引擎)
InnoDB 通过在每行记录后面保存两个隐藏的列来实现 MVCC,分别是创建版本号和删除版本号,这两个版本号都指向系统版本号。系统版本号是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
- 插入操作:InnoDB 为新插入的每一行保存当前系统版本号作为行的创建版本号。
- 删除操作:InnoDB 为删除的每一行保存当前系统版本号作为行的删除版本号。
- 更新操作:InnoDB 先标记旧的那一行记录的删除版本号为当前系统版本号,然后插入一行新的记录,保存当前系统版本号作为创建版本号。
- 查询操作 :InnoDB 会根据以下规则过滤数据:
- 只查找创建版本号小于或等于当前事务版本号的记录,这样可以确保事务读取到在其开始之前已经存在或者是由该事务自身插入或修改的记录。
- 查找删除版本号要么未定义,要么大于当前事务版本号的记录,这样可以确保事务不会读取到在其开始之后被删除的记录。
PostgreSQL
PostgreSQL 使用多版本元组来实现 MVCC,每个元组(行)都有一个头部,包含了一些与事务相关的信息,如 xmin(创建事务 ID)和 xmax(删除事务 ID)。
- 插入操作:新插入的元组的 xmin 被设置为当前事务的 ID,xmax 未定义。
- 删除操作:将被删除元组的 xmax 设置为当前事务的 ID。
- 更新操作:实际上是先删除旧的元组(设置 xmax 为当前事务 ID),然后插入一个新的元组(设置新元组的 xmin 为当前事务 ID)。
- 查询操作:PostgreSQL 根据事务的快照来决定哪些元组是可见的。事务的快照包含了在该事务开始时活跃的所有事务的 ID 列表,查询时会根据 xmin 和 xmax 以及快照中的信息来判断元组是否可见。
优点
- 提高并发性能:MVCC 允许读写操作并发执行,读操作不会阻塞写操作,写操作也不会阻塞读操作,从而显著提高了数据库的并发处理能力。
- 一致性读:事务在执行过程中看到的数据是一致的,不会出现脏读、不可重复读等问题,保证了事务的隔离性。
- 减少锁竞争:相比于传统的锁机制,MVCC 减少了锁的使用,降低了锁竞争带来的性能开销,减少了死锁的发生概率。
缺点
- 存储空间开销:为了保存多个版本的数据,数据库需要额外的存储空间来存储这些版本信息,尤其是在数据更新频繁的情况下,存储空间的开销会比较大。
- 垃圾回收:随着数据的不断更新,会产生大量的旧版本数据,这些旧版本数据需要进行定期的垃圾回收,否则会占用大量的存储空间,影响数据库的性能。
- 实现复杂度:MVCC 的实现相对复杂,需要数据库管理系统在事务管理、数据版本控制等方面进行精细的设计和实现,增加了数据库的开发和维护难度。
3.常见面试题
1). 什么是 MVCC?它解决了什么问题?
答 :
MVCC 是多版本并发控制机制,通过为数据行维护多个版本,让读写操作在并发时互不阻塞。
- 解决的核心问题 :
- 避免传统锁机制(如读锁、写锁)的阻塞问题,提升并发性能。
- 保证事务的隔离性(如可重复读),避免脏读、不可重复读等问题。
- 核心思想:每个事务访问的是数据在某个时间点的快照,而非实时数据,读写互不阻塞。
2). MySQL InnoDB 和 PostgreSQL 的 MVCC 实现有什么区别?
答:
特性 | InnoDB(MySQL) | PostgreSQL |
---|---|---|
版本号类型 | 基于全局递增的 系统版本号(单调递增的长整数) | 基于 事务 ID(XID,32 位或 64 位整数) |
数据行版本存储 | 每行记录隐藏 trx_id (创建版本)和 roll_ptr (回滚指针,指向旧版本) |
每行记录包含 xmin (创建事务 ID)、xmax (删除事务 ID)、cmin /cmax (命令级版本) |
快照生成时机 | 事务启动时生成快照(可重复读隔离级别下,整个事务期间快照不变) | 语句执行时生成快照(读提交隔离级别下,每条语句的快照独立) |
可见性判断 | 通过比较系统版本号与当前事务版本号,过滤创建版本 ≤ 当前版本且删除版本 > 当前版本的行 | 根据事务快照中的活跃事务列表,判断 xmin 不在活跃列表且 xmax 未定义或在活跃列表之后 |
旧版本回收 | 依赖事务回滚日志(undo log),由 purge 线程定期清理 | 依赖 VACUUM 命令手动或自动清理旧版本数据 |
3). MVCC 如何实现事务的可重复读?
答 :
以 MySQL InnoDB 的可重复读隔离级别为例:
- 快照生成 :事务启动时,记录当前的系统版本号(
read view
),作为该事务的快照版本。 - 读操作:只访问版本号 ≤ 快照版本且未被当前快照版本之后的事务删除的数据行。
- 写操作 :新数据的版本号是递增的,旧数据的删除版本号标记为当前事务版本,但不影响已生成快照的读操作。
因此,同一事务内多次查询结果一致,不受其他事务提交的影响。
4). MVCC 中的 "快照读" 和 "当前读" 有什么区别?
答:
- 快照读(Snapshot Read) :
- 通过 MVCC 读取历史版本数据,不加锁(如
SELECT ... FROM table;
)。 - 结果取决于事务的快照版本,保证可重复读。
- 通过 MVCC 读取历史版本数据,不加锁(如
- 当前读(Current Read) :
- 读取最新数据,且会加锁(如
SELECT ... FOR UPDATE/LOCK IN SHARE MODE
、INSERT/UPDATE/DELETE
)。 - 直接访问最新版本,用于保证写操作的原子性和一致性。
- 读取最新数据,且会加锁(如
5). MVCC 相比传统锁机制有什么优势?为什么读写可以不阻塞?
答:
- 优势 :
- 无锁读:读操作不阻塞写操作,写操作也不阻塞读操作,提升并发性能。
- 隔离性保证:通过版本控制避免脏读、不可重复读,无需显式加锁。
- 减少锁竞争:降低死锁概率,避免锁粒度细化带来的复杂度。
- 读写不阻塞的本质 :
读操作访问的是历史版本数据,写操作生成新版本数据,两者操作不同版本,无需互斥。
6). MVCC 会带来哪些问题?如何解决?
答:
- 问题 :
- 存储空间膨胀:旧版本数据积累导致存储占用增加(如 InnoDB 的 undo log、PostgreSQL 的膨胀表)。
- 性能影响:高并发写场景下,版本管理和垃圾回收可能成为瓶颈。
- 长事务阻塞回收:长时间运行的事务会阻止旧版本回收,导致存储泄漏。
- 解决方案 :
- 定期执行垃圾回收(如 InnoDB 的
purge
线程、PostgreSQL 的VACUUM
)。 - 避免长事务,缩短事务持续时间。
- 合理设置隔离级别,减少不必要的版本保留(如使用读提交代替可重复读)。
- 定期执行垃圾回收(如 InnoDB 的
7). MVCC 是如何保证事务的隔离级别的?以 "读提交" 和 "可重复读" 为例说明。
答:
- 读提交(Read Committed) :
- 每次语句执行时生成新的快照(如 PostgreSQL),或每次查询时生成快照(如 InnoDB 8.0 前的读提交)。
- 只能看到已提交的事务所做的修改,避免脏读。
- 可重复读(Repeatable Read) :
- 事务启动时生成唯一快照(如 InnoDB 的默认隔离级别),整个事务期间使用同一快照。
- 保证事务内多次查询结果一致,避免不可重复读和幻读(InnoDB 通过间隙锁额外解决幻读)。
8). 在 MVCC 中,如何判断一个数据版本对当前事务是否可见?
答 (以 InnoDB 为例):
假设当前事务版本为 curr_version
,数据行的创建版本为 trx_id
,删除版本为 roll_ptr
指向的版本:
- 若
trx_id < curr_version
:数据行在当前事务启动前已创建,可能可见。 - 若
roll_ptr > curr_version
:数据行在当前事务启动后被删除,不可见。 - 若
trx_id == curr_version
:数据行由当前事务创建,可见。
最终,可见性需同时满足:trx_id ≤ curr_version
且(roll_ptr
未定义 或roll_ptr > curr_version
)。
9). MVCC 属于乐观锁还是悲观锁?为什么?
答:
- MVCC 不属于传统的乐观锁或悲观锁,但更接近乐观并发控制。
- 原因 :
- 悲观锁(如行锁)通过提前加锁防止冲突,适用于写多读少场景。
- 乐观锁通过版本号或 CAS 检测冲突,适用于写少读多场景。
- MVCC 通过版本控制让读写无阻塞,本质是为读操作提供 "无锁" 的一致性视图,写操作仍可能需要锁(如当前读)。
10). 为什么 InnoDB 在可重复读隔离级别下能避免幻读?
答:
- MVCC 解决部分幻读:事务快照中不包含后续插入的新行(创建版本 > 快照版本),因此普通查询(快照读)看不到新插入的行。
- 间隙锁(Gap Lock)辅助 :当使用
SELECT ... FOR UPDATE
(当前读)时,InnoDB 会加间隙锁,锁定索引间隙,防止其他事务插入新行,完全避免幻读。