MySQL MVCC机制
MySQL 为解决高并发场景下 “读写互斥导致的性能瓶颈”,引入了 MVCC(多版本并发控制)机制。该机制通过维护数据的多版本历史(存储于 undo log 版本链),允许多线程对相同数据同时执行读写操作 —— 读操作(快照读)访问历史版本,写操作修改当前版本,二者互不阻塞,大幅提升并发效率。
MVCC 需与事务隔离级别协同工作,通过为不同事务生成符合隔离级别的 ReadView(一致性视图) 来实现 “可见性” 与 “隔离性”:不同事务会依据自身的 ReadView 规则,从 undo log 版本链中筛选出 “允许自己访问的历史版本”,从而完成 “快照读”(普通 SELECT
)。
与依赖 MVCC 的 “快照读” 不同,“当前读”(如 SELECT ... FOR UPDATE
)及写操作(INSERT
/UPDATE
/DELETE
)需通过锁机制控制并发:通过行锁、间隙锁或二者组合的 Next-Key Lock,防止并发修改导致的数据不一致。
MVCC 机制的核心价值"多版本并发控制"主要在 Read Committed (RC)
和 Repeatable Read (RR)
两个隔离级别下生效;而在 Read Uncommitted (RU)
和 Serializable
级别下,MVCC 要么不被启用,要么不发挥其核心作用。RU级别直接读取最新版本数据,不需要历史版本,而Serializable级别下,所有读操作都会加锁串行执行,也不需要历史版本。但是undo log 还是会用作事务回滚。
一、MVCC 基础
1.1 MVCC 的核心目标与实现基础
MVCC(Multi-Version Concurrency Control)即多版本并发控制,是一种在数据库管理系统中用于处理高并发场景下读写操作的技术。传统的数据库并发控制主要依赖锁机制,当一个事务对数据进行修改时,会对数据加锁,阻止其他事务同时访问或修改,这在高并发场景下容易导致锁争用,降低系统性能 。MVCC 的出现就是为了突破这一局限,它通过维护数据的多个版本,使得读操作和写操作可以在很大程度上并行进行,避免了读写操作之间的阻塞,从而显著提升数据库在高并发环境下的性能和吞吐量。
MVCC 的实现依赖于 InnoDB 存储引擎中的多个关键组件:
隐式字段:InnoDB 为每一行数据记录添加了三个隐式字段。其中,
DB_TRX_ID
字段记录了对该行数据进行最新修改的事务 ID,这个事务 ID 是自增的,每次有新的事务对数据进行修改,DB_TRX_ID
就会更新为最新事务的 ID;DB_ROLL_PTR
是回滚指针,它指向该行数据在Undo Log
中的上一个版本记录,通过这个指针可以构建起数据的历史版本链;DB_ROW_ID
是一个隐藏主键,当表中没有显式定义主键时,InnoDB 会自动生成这个隐藏主键来唯一标识每一行记录。Undo Log 版本链:
Undo Log
是 MVCC 实现的重要基础。当事务对数据进行修改时,InnoDB 会先将修改前的数据版本记录到Undo Log
中,然后再进行数据的修改操作。每一次修改都会产生一个新的Undo Log
记录,而DB_ROLL_PTR
指针则将这些不同版本的数据记录串联起来,形成一个版本链。例如,事务 A 将数据行中的某个字段从值 1 修改为值 2,此时会在Undo Log
中记录下修改前的值 1 以及相关信息,并且数据行的DB_ROLL_PTR
会指向这个新的Undo Log
记录,后续再有事务对该数据进行修改,同样会依此方式生成新的Undo Log
记录并更新DB_ROLL_PTR
,从而不断完善版本链 。ReadView 一致性视图:在进行快照读时,
ReadView
是判断数据可见性的关键依据。ReadView
记录了当前系统中活跃事务(未提交的事务)的集合,以及这些事务的事务 ID 范围等信息。当一个事务进行快照读操作时,会根据ReadView
中的信息来判断哪些数据版本是可见的,哪些是不可见的,以此来确保事务读取到的数据在其事务开始时的一致性状态。
1.2 当前读与快照读:两类查询的本质区别
在 MVCC 机制下,MySQL 中的查询操作可以分为当前读和快照读两类,它们在数据读取方式、数据版本获取以及并发控制等方面有着本质的区别。
当前读(加锁读):当前读的核心目的是获取最新的数据,并且为了保证数据在读取过程中的一致性和完整性,会对读取的数据加锁。常见的当前读操作包括
SELECT ... FOR UPDATE
、SELECT ... LOCK IN SHARE MODE
以及数据修改操作(如INSERT
、UPDATE
、DELETE
)。以SELECT ... FOR UPDATE
为例,当执行这条语句时,会对查询结果集中的每一行数据加上排他锁(X 锁),这意味着其他事务不能对这些数据进行修改或删除操作,直到当前事务结束释放锁为止,这样就确保了在当前事务处理过程中,数据不会被其他事务意外修改 。当前读总是读取数据库中最新已提交的数据版本,它直接从存储引擎的最新数据页中获取数据,不依赖于历史版本信息。快照读(无锁读):快照读主要用于读取数据的一致性视图,它读取的并不是最新的数据版本,而是事务开始时的数据快照。在执行普通的
SELECT
语句(不加锁的情况下)时,就会进行快照读操作。例如,在可重复读的隔离级别下,事务 A 在某一时刻开始执行一个SELECT
查询,此时会生成一个ReadView
,后续该事务内的所有快照读操作都会依据这个ReadView
来判断数据的可见性,即使在事务执行过程中,其他事务对数据进行了修改并提交,事务 A 看到的数据仍然是其开始时的数据版本,这就实现了在一个事务内多次读取同一数据时结果的一致性 。快照读通过ReadView
和Undo Log
版本链来确定数据的可见版本,不需要对数据加锁,从而避免了读写冲突,大大提高了并发性能。
二、SQL 语句触发的 MVCC 处理流程
2.1 语句解析与事务类型判断
当客户端向 MySQL 数据库发送 SQL 语句后,MySQL 的查询解析器首先会对语句进行词法分析和语法分析,确定语句的类型 。如果是一条普通的SELECT
语句,且没有加锁相关的指令(如FOR UPDATE
、LOCK IN SHARE MODE
),则被判定为快照读操作,此时会进入 MVCC 的处理流程;如果是带有加锁指令的SELECT
语句或者是数据修改语句(INSERT
、UPDATE
、DELETE
),则属于当前读操作,会走数据库的锁机制流程。
同时,事务的隔离级别也在这个阶段起着关键作用。MySQL 支持四种事务隔离级别:读未提交(READ UNCOMMITTED
)、读已提交(READ COMMITTED
)、可重复读(REPEATABLE READ
)和串行化(SERIALIZABLE
) 。其中,MVCC 主要应用于读已提交和可重复读这两个隔离级别,因为读已提交直接读取缓冲区,串行化会加读锁变为当前读,都不使用MVCC快照读。在这两个级别下,事务在进行快照读时,会根据各自的规则生成ReadView
,不同的隔离级别生成ReadView
的策略不同,这将直接影响到事务读取数据的可见性和一致性。例如,在READ COMMITTED
隔离级别下,每次执行快照读操作时都会生成一个新的ReadView
,这使得事务能够读取到最新已提交的数据;而在REPEATABLE READ
隔离级别下,事务在第一次执行快照读时生成ReadView
,后续的快照读操作都会复用这个ReadView
,从而保证了事务在整个执行过程中多次读取数据的一致性 。
2.2 ReadView 生成与版本链遍历
2.2.1 ReadView 的核心字段
在进入 MVCC 处理流程后,事务会生成一个ReadView
,它是判断数据可见性的关键数据结构,包含以下四个核心字段:
m_ids:这是一个列表,记录了当前系统中所有活跃事务(即未提交的事务)的事务 ID。这些事务 ID 标识了正在进行中且尚未完成提交操作的事务,它们对数据的修改可能还处于不确定状态,因此在判断数据可见性时需要考虑这些事务的影响。
min_trx_id:表示活跃事务中最小的事务 ID。这个值用于确定哪些事务是在当前事务生成
ReadView
之前就已经开始的,通过与数据版本的事务 ID 进行比较,可以判断数据版本是否可见。max_trx_id:代表全局下一个待分配的事务 ID,它的值是当前系统中已经分配的最大事务 ID 再加 1。这个值用于判断哪些事务是在当前事务生成
ReadView
之后才开始的,对于这些事务修改的数据版本,在当前事务中是不可见的。creator_trx_id:指生成当前
ReadView
的事务 ID。通过将数据版本的事务 ID 与creator_trx_id
进行比较,可以判断当前事务是否是修改该数据版本的事务,如果是,则该数据版本对当前事务可见 。
2.2.2 数据可见性判断规则
生成ReadView
后,事务开始读取数据。从数据行的最新版本开始,沿着Undo Log
版本链进行遍历,根据以下规则判断每个版本的数据是否对当前事务可见:
若数据版本的
DB_TRX_ID
等于creator_trx_id
,说明这个数据版本是当前事务自己修改产生的,那么该数据版本对当前事务直接可见。例如,事务 A 修改了某一行数据,在事务 A 后续的查询中,通过 MVCC 机制判断,这个由事务 A 自己修改的数据版本会因为DB_TRX_ID
等于creator_trx_id
而直接被事务 A 看到。若数据版本的
DB_TRX_ID
小于min_trx_id
,这表明修改该数据版本的事务在当前事务生成ReadView
之前就已经提交了,所以该数据版本对当前事务是可见的。比如,有事务 B 在事务 A 生成ReadView
之前就完成了对数据的修改并提交,此时事务 A 查询数据时,对于事务 B 修改的数据版本,由于其DB_TRX_ID
小于事务 A 的min_trx_id
,所以事务 A 可以看到这个数据版本。若数据版本的
DB_TRX_ID
在[min_trx_id, max_trx_id)
区间内,说明生成该数据版本的事务也是在当前事务生成ReadView之前启动并分配的事务ID:如果
DB_TRX_ID
在m_ids
中,说明修改该数据版本的事务是当前活跃事务(未提交),那么该数据版本对当前事务不可见。例如,事务 C 正在修改数据但还未提交,事务 A 生成ReadView
后查询数据,对于事务 C 修改的数据版本,因为其DB_TRX_ID
在事务 A 的m_ids
中,所以事务 A 看不到这个数据版本。如果
DB_TRX_ID
不在m_ids
中(即修改该版本的事务在 ReadView 生成时已提交),则该数据版本对当前事务可见。例如:事务 D(ID=102)在事务 A 生成 ReadView 之前启动、修改数据并提交(已结束)。事务 A 生成 ReadView 时,当前活跃事务的 ID 列表为m_ids = [100, 103]
(即此时未提交的事务是 100 和 103),但是102已经提交事务,所以可见。
若数据版本的
DB_TRX_ID
大于等于max_trx_id
(可重复读才会出现此情况,读已提交级别下每次select都会生成新的ReadView,所以DB_TRX_ID不会大于等于max_trx_id),这意味着修改该数据版本的事务是在当前事务生成ReadView
之后才启动的,所以该数据版本对当前事务不可见。比如,事务 E 在事务 A 生成ReadView
之后才开始事务并修改数据,事务 A 查询数据时,对于事务 E 修改的数据版本,由于其DB_TRX_ID
大于等于事务 A 的max_trx_id
,所以事务 A 看不到这个数据版本 。
2.3 结果集返回与 ReadView 复用策略
事务根据上述规则遍历版本链,找到符合可见性规则的第一个数据版本后,将其作为查询结果返回。在整个事务执行过程中,对于快照读操作,不同的隔离级别有不同的ReadView
复用策略:
读已提交(RC):在
READ COMMITTED
隔离级别下,每次执行快照读操作时都会生成一个新的ReadView
。这是因为在该隔离级别下,事务希望每次读取都能获取到最新已提交的数据,所以每次查询都基于当前最新的活跃事务状态生成ReadView
,以保证数据的实时性。例如,事务 A 在执行过程中多次进行快照读,每次读取时都会重新生成ReadView
,如果在两次读取之间有其他事务提交了对数据的修改,那么第二次读取时由于新生成的ReadView
反映了最新的事务状态,事务 A 就能够读取到这些新提交的数据修改 。可重复读(RR):在
REPEATABLE READ
隔离级别下,事务仅在第一次执行快照读操作时生成ReadView
,后续的所有快照读操作都会复用这个ReadView
。这样做的目的是确保事务在整个执行过程中多次读取同一数据时,看到的数据始终是一致的,避免了不可重复读的问题。例如,事务 B 在开始时生成了ReadView
,在后续的多次查询中,无论其他事务如何修改数据并提交,事务 B 始终依据第一次生成的ReadView
来判断数据可见性,所以每次查询结果都保持不变,从而保证了事务内数据的一致性 。
三、不同隔离级别下 ReadView 生成机制对比
3.1 读未提交(Read Uncommitted):无 ReadView 的 “裸读”
在 “读未提交” 隔离级别下,事务直接读取数据库中数据的最新版本,不会生成ReadView
。这种方式简单直接,事务在读取数据时不会受到任何限制,能够获取到其他事务尚未提交的修改结果 。例如,当事务 A 对某一行数据进行修改但还未提交时,事务 B 在 “读未提交” 隔离级别下进行读取操作,就可以直接看到事务 A 修改后的数据。
虽然这种隔离级别下的读操作没有额外的开销,性能看似很高,但是它带来了严重的数据一致性问题,即可能会读到脏数据。脏数据是指那些被其他事务修改但尚未提交的数据,如果这些数据随后被回滚,那么读取到脏数据的事务就会基于这些无效的数据进行后续操作,从而导致数据处理错误。由于其数据一致性无法得到保障,“读未提交” 隔离级别在实际应用中极少使用,仅在一些对数据一致性要求极低,且允许脏读的特殊场景下可能会被考虑,比如某些临时数据的统计场景,对数据准确性要求不高,更注重快速获取大致数据 。
3.2 读已提交(Read Committed, RC):每次查询生成独立 ReadView
在 “读已提交” 隔离级别下,事务每次执行快照读操作(如普通的SELECT
语句)时,都会生成一个独立的ReadView
。生成ReadView
时,事务会获取当前系统中所有活跃事务(未提交的事务)的集合,构建出包含m_ids
(活跃事务 ID 列表)、min_trx_id
(最小活跃事务 ID)、max_trx_id
(下一个待分配事务 ID)和creator_trx_id
(生成该ReadView
的事务 ID)的一致性视图 。
这种机制使得事务在每次读取时,都能根据最新的活跃事务状态来判断数据的可见性,从而保证只能读取到已经提交的数据版本 。例如,事务 A 在执行过程中进行多次快照读,在第一次读取时生成了ReadView1
,此时如果有事务 B 对数据进行修改并提交,当事务 A 进行第二次读取时,会生成新的ReadView2
,这个新的ReadView2
反映了事务 B 提交后的活跃事务状态,所以事务 A 在第二次读取时能够看到事务 B 提交后的修改结果 。
然而,由于每次查询都生成新的ReadView
,同一事务内多次查询相同数据可能会得到不同的结果,这就导致了不可重复读的问题。例如,在一个电商应用中,事务 A 先查询商品库存为 100 件,在这之后事务 B 修改了商品库存并提交,当事务 A 再次查询时,由于生成了新的ReadView
,会读取到事务 B 修改后的库存数据,可能就不再是 100 件了 。“读已提交” 隔离级别是 OLTP(联机事务处理)系统中常用的默认隔离级别之一,它在一定程度上平衡了数据一致性和系统性能,适用于对数据实时性要求较高,但对同一事务内多次读取数据一致性要求相对较低的场景,如大多数普通的业务查询场景 。
3.3 可重复读(Repeatable Read, RR):首次查询生成 ReadView 并复用
在 “可重复读” 隔离级别下,事务在第一次执行快照读操作时生成ReadView
,后续在该事务内的所有快照读操作都会复用这个ReadView
。当事务首次执行快照读时,同样会获取当前系统的活跃事务集合来生成ReadView
,这个ReadView
记录了事务开始时的活跃事务状态 。
基于首次查询时生成的ReadView
,事务在后续的查询过程中,会依据这个固定的视图来判断数据的可见性,从而屏蔽了其他事务在该事务执行期间提交的修改操作 。例如,事务 A 在开始时生成了ReadView
,在事务执行过程中,无论其他事务如何修改数据并提交,事务 A 后续的查询始终依据最初的ReadView
,所以多次查询相同数据的结果保持一致,有效解决了不可重复读的问题 。
但是,“可重复读” 隔离级别并不能完全避免幻读问题。幻读是指在一个事务中,前后两次相同条件的查询,结果集中的数据行数不一致,通常是因为其他事务在两次查询之间插入或删除了符合查询条件的数据 。虽然 MVCC 机制在一定程度上缓解了幻读问题,但在某些特殊情况下,如使用范围查询时,仍可能出现幻读 。在 MySQL 中,通过 Next-Key Lock(记录锁和间隙锁的组合)机制来进一步解决幻读问题,它可以锁定一个范围,防止其他事务在该范围内插入或删除数据 。MySQL 将 “可重复读” 作为默认的事务隔离级别,通过复用ReadView
,减少了ReadView
频繁生成带来的开销,提升了系统在高并发场景下的并发性能,适用于对事务内数据一致性要求较高的业务场景,如金融交易、订单处理等 。
3.4 串行化(Serializable):ReadView 失效,全靠锁机制
在 “串行化” 隔离级别下,MySQL 放弃了 MVCC 机制,所有的读操作都转为当前读,即对读取的数据加锁,事务以串行的方式依次执行 。在这种隔离级别下,事务在读取数据时,会对数据加上共享锁(S 锁)或排他锁(X 锁),防止其他事务同时对数据进行修改,从而保证了事务之间的完全隔离 。
由于所有事务都是串行执行,不再需要ReadView
来判断数据可见性,因此不会出现脏读、不可重复读和幻读等并发问题 。例如,在一个银行转账事务中,事务 A 在进行转账操作时,会对涉及的账户数据加锁,其他事务必须等待事务 A 完成并释放锁后,才能对这些数据进行操作,这样就确保了数据的一致性和完整性 。
然而,串行化执行方式会导致系统的并发性急剧下降,性能最差 。因为每个事务都需要等待前一个事务完成才能开始,这在高并发场景下会产生大量的锁等待和超时现象,严重影响系统的吞吐量 。所以,“串行化” 隔离级别仅适用于对数据一致性要求极高,如金融领域的核心交易系统、涉及资金清算等不容许任何数据不一致情况的场景,这些场景愿意牺牲性能来换取绝对的数据隔离和一致性 。
四、总结
MVCC 通过版本链和 ReadView 的巧妙设计,在数据库并发场景下实现了读写操作的高效并行。版本链利用Undo Log
记录数据的历史版本,为快照读提供了丰富的数据来源,使得读操作无需等待写操作完成,避免了读写阻塞,极大地提升了系统的并发性能 。而ReadView
作为数据可见性的判断依据,确保了事务在读取数据时能够获取到符合其事务一致性要求的数据版本,在保证数据一致性的前提下,进一步优化了读性能。例如,在高并发的电商商品查询场景中,大量的读操作可以通过 MVCC 机制快速获取到历史版本数据,而不会因为写操作(如库存更新)而被阻塞,从而提高了系统的响应速度和吞吐量 。
不同的事务隔离级别在 MVCC 机制下对数据可见性和一致性有着不同的保障,开发者需要根据具体业务场景来选择合适的隔离级别。“读已提交(RC)” 隔离级别每次查询都生成新的ReadView
,这使得事务能够及时读取到最新提交的数据,非常适合对数据实时性要求较高的场景,如实时报表查询,能够保证报表数据的及时性和准确性 。而 “可重复读(RR)” 隔离级别在事务首次查询时生成ReadView
并复用,确保了事务内数据的一致性,适用于对事务内数据一致性要求极高的场景,如订单处理系统,在一个订单处理事务中,多次读取订单相关数据时,能够保证数据的一致性,避免因其他事务的修改而导致数据不一致的问题 。