多版本并发控制(MVCC)是一种用于提高数据库并发性能的技术,尤其在处理高并发读写操作时极为有效。MVCC通过维护数据的多个版本来避免读写冲突,使得读操作无需阻塞写操作,写操作也不会影响读操作。下面,我们具体讲解MySQL中InnoDB存储引擎对MVCC的实现原理。

MVCC(Multi-Version Concurrency Control)可以看作是行级锁的一种改进,主要通过保存数据在不同时间点的多个版本来实现数据的并发访问。MVCC 常用于实现乐观锁定策略,通过版本号控制数据的一致性,避免了因锁导致的性能瓶颈。
不知道大家是否想过这样一个问题:在日常操作中,为什么读写操作可以互不阻塞?
在MVCC技术出现之前,为了确保在特定隔离级别下,多个事务之间不发生数据异常,需要通过加锁来控制并发。
例如,当事务A正在读取一行数据时,其他事务不能对这行数据进行修改,因为这可能导致事务A读取到不一致的数据。
MVCC的核心优势就在于解决了读写操作之间的阻塞问题,从而显著提高了事务的并发性。接下来,我们通过一个例子来逐步学习MySQL中MVCC的实现。
我们来看图1中的这个例子;

要回答这个问题,关键在于事务的隔离级别,不同隔离级别下的行为如下:
column= lisi。column= zhangsan,但在提交后,查询结果变为column= lisi。
column= zhangsan。以下是MySQL 5.7和MySQL 8.0中查询和修改数据库隔离级别语句的对比表格:
操作 | MySQL 5.7 | MySQL 8.0 |
|---|---|---|
查询当前会话隔离级别 | SELECT @@tx_isolation; 或 SHOW VARIABLES LIKE 'transaction_isolation'; | SELECT @@transaction_isolation; 或 SHOW VARIABLES LIKE 'transaction_isolation'; |
查询全局隔离级别 | SELECT @@global.tx_isolation; 或 SHOW GLOBAL VARIABLES LIKE 'transaction_isolation'; | SELECT @@global.transaction_isolation; 或 SHOW GLOBAL VARIABLES LIKE 'transaction_isolation'; |
修改当前会话隔离级别 | SET SESSION TRANSACTION ISOLATION LEVEL 隔离级别; | SET SESSION TRANSACTION ISOLATION LEVEL 隔离级别; |
修改全局隔离级别 | SET GLOBAL TRANSACTION ISOLATION LEVEL 隔离级别; | SET GLOBAL TRANSACTION ISOLATION LEVEL 隔离级别; |
其中,隔离级别可以是以下几种之一:
READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READ(MySQL默认)SERIALIZABLE在这里我们暂且不讨论数据库的ACID特性,而是关注一个问题:在数据被修改后,数据库是如何确保查询结果仍然保持为之前的值?
这正是通过数据库中的Undo日志和MVCC技术来实现的,如图1-1所示。

InnoDB 中的 MVCC 主要依赖于以下三个隐藏列,这些列在每行记录中维护,以实现多版本并发控制:
DB_TRX_ID,即事务ID,记录了最后一次对该行进行插入或更新的事务ID。当需要判断某个事务中某条记录是否可见时,InnoDB 会根据该列与当前事务的视图进行对比。DB_ROLL_PTR 是一个回滚指针,指向该行记录的上一个版本。在数据更新时,InnoDB 会保留旧版本的记录,并通过这个指针将其链接起来,从而支持通过回滚找到之前的数据版本。这是实现 MVCC 中多版本存储的关键。DB_ROW_ID 是一个自增的行ID,在没有显式主键的情况下,用来唯一标识每一行数据。尽管 DB_ROW_ID 在 MVCC 机制中并不直接控制并发,但它对行记录的管理和定位起着重要作用,尤其是在没有定义主键时。这三个隐藏列共同支撑了 InnoDB 的 MVCC 实现,确保在高并发场景下,读写操作能够并行进行而不互相阻塞。
当插入一条数据时, 在记录上对应的回滚段指针为NULL, 如图1-2所示

在更新记录时,原始记录会被保存到 Undo 表空间中,查询时未提交的修改数据可以通过读取 Undo 表空间中的旧版本来实现。多个数据版本通过链表结构链接,形成一个版本链。MySQL 通过记录中的回滚指针(DB_ROLL_PTR)和事务ID(DB_TRX_ID)来判断数据版本的可见性。具体判断流程如下:
在每个事务开始时,系统会将当前所有活跃事务的信息拷贝到一个列表中,称为Read View。读取记录时,会根据记录的 TRX_ID 与 Read View 中的最大 TRX_ID 和最小 TRX_ID 进行比较,判断该记录是否对当前事务可见。
TRX_ID 小于 Read View 中的最小 TRX_ID******:**
说明该记录在 Read View 中的所有事务开始之前就已提交,因而对当前事务可见,可以直接返回数据。TRX_ID 大于 Read View 中的最大 TRX_ID******:**
说明该记录在事务启动后被修改。此时需要根据回滚指针找到前一个版本的记录,并将其 TRX_ID 赋值给当前行,再重新进行判断。TRX_ID 位于 Read View 的范围内:需要进一步判断:
TRX_ID 在 Read View 中存在:TRX_ID 进行下一轮判断。TRX_ID 不在 Read View 中:Read View 是 MySQL InnoDB 引擎用于实现 多版本并发控制 (MVCC) 的关键机制。它在事务执行快照读时生成,确保事务能够看到一致的、稳定的数据视图。Read View 记录了事务生成快照时的系统状态,尤其是所有活跃事务的 ID 列表。通过 Read View,数据库可以确保事务只看到在其生成视图时已经提交的数据,而不受之后其他事务的影响。
SELECT ... LOCK IN SHARE MODE, SELECT ... FOR UPDATE, UPDATE, INSERT, DELETE 等。SELECT 操作,它通过 Read View 实现,确保事务读取到的总是快照生成时的数据版本,而不是之后的修改。SELECT 操作,在隔离级别为读已提交 (Read Committed) 或可重复读 (Repeatable Read) 时尤其常见。Read View 的四个字段决定了事务在执行快照读时哪些数据对其可见,哪些不可见。
trx_ids:
low_limit_id:
up_limit_id:
creator_trx_id:
Read View 通过以上四个字段来管理事务快照的可见性,保证了事务在快照读时的一致性。在 MySQL 的 MVCC 实现中,这一机制非常重要,因为它确保了在高并发环境下事务隔离的实现,防止脏读、不可重复读和幻读问题的发生。
在 MySQL 8.0 的源码中,Read View 的核心代码主要涉及 InnoDB 存储引擎的事务管理部分。它们定义了 Read View 的生成、字段初始化和可见性判断等功能。
read_view_open_now 函数read_view_open_now 函数是用来创建一个新的 Read View。它会记录当前活跃事务,并初始化 Read View 的各个字段。
read_view_t* read_view_open_now(trx_t* trx) {
read_view_t* view;
view = static_cast<read_view_t*>(ut_malloc_nokey(sizeof(*view)));
ut_a(view != NULL);
/* 设置创建者事务 ID */
view->creator_trx_id = trx->id;
/* 获取当前活跃事务列表并赋值给 view */
view->m_trx_ids = trx_sys_get_active_trx_ids();
/* 活跃事务的数量 */
view->trx_list_len = trx_sys->rw_trx_list_len;
/* 设置低水位 */
view->low_limit_no = trx_sys->rw_trx_list->start->id;
/* 设置高水位 */
view->up_limit_no = trx_sys->rw_trx_list->end->id;
return(view);
}trx_sys_get_active_trx_ids 函数trx_sys_get_active_trx_ids 函数用于获取当前系统中所有活跃事务的 ID。这个函数返回一个包含所有未提交事务 ID 的列表,这些事务的变更对当前 Read View 不可见。
trx_id_t* trx_sys_get_active_trx_ids() {
trx_id_t* trx_ids;
/* 分配用于存储事务 ID 的内存空间 */
trx_ids = static_cast<trx_id_t*>(ut_malloc_nokey(trx_sys->rw_trx_list_len * sizeof(trx_id_t)));
/* 遍历当前活跃事务列表,存储每个事务的 ID */
rw_trx_list_lock();
rw_trx_t* rw_trx = trx_sys->rw_trx_list->start;
for (size_t i = 0; i < trx_sys->rw_trx_list_len; i++, rw_trx = rw_trx->next) {
trx_ids[i] = rw_trx->id;
}
rw_trx_list_unlock();
return trx_ids;
}read_view_sees_trx_id 函数read_view_sees_trx_id 函数用于判断某个事务 ID 的变更是否对当前 Read View 可见。它依据 Read View 的四个字段来决定可见性。
bool read_view_sees_trx_id(read_view_t* view, trx_id_t trx_id) {
/* 如果事务 ID 小于 up_limit_id,说明该事务在 Read View 生成之前已经提交,因此可见 */
if (trx_id < view->up_limit_no) {
return true;
}
/* 如果事务 ID 大于或等于 low_limit_id,说明该事务在 Read View 生成之后启动,因此不可见 */
if (trx_id >= view->low_limit_no) {
return false;
}
/* 如果事务 ID 在 active_trx_ids 列表中,说明它是活跃事务之一,因此不可见 */
for (size_t i = 0; i < view->trx_list_len; i++) {
if (view->m_trx_ids[i] == trx_id) {
return false;
}
}
/* 如果上述条件都不满足,则该事务可见 */
return true;
}read_view_close 函数read_view_close 函数用于关闭并销毁一个 Read View,释放内存。
void read_view_close(read_view_t* view) {
ut_free(view->m_trx_ids);
ut_free(view);
}创建 Read View (read_view_open_now):
获取活跃事务 ID (trx_sys_get_active_trx_ids):
判断事务可见性 (read_view_sees_trx_id):
关闭 Read View (read_view_close):
这些核心代码段展示了 InnoDB 是如何通过 Read View 实现快照读,确保事务隔离和一致性。
在 Navicat 中执行以下 SQL,准备一张简单的测试表:
CREATE TABLE test_mvcc (
id INT PRIMARY KEY,
value VARCHAR(50)
) ENGINE=InnoDB;
INSERT INTO test_mvcc (id, value) VALUES (1, 'Initial Value');在 Navicat 中打开两个查询窗口,模拟两个事务会话。
会话 1(启动事务并更新记录):
START TRANSACTION;
UPDATE test_mvcc SET value = 'Updated Value by Session 1' WHERE id = 1;
-- 此时事务未提交,数据仅对会话 1 可见会话 2(启动事务并查询记录):
START TRANSACTION;
SELECT * FROM test_mvcc WHERE id = 1;
-- 此时会话 2 应该仍然看到 'Initial Value',因为会话 1 的事务未提交在会话 1 提交事务之前,数据对其他事务不可见。
会话 1 提交事务:
COMMIT;会话 2 再次查询:
SELECT * FROM test_mvcc WHERE id = 1;
-- 现在会话 2 可以看到更新后的值 'Updated Value by Session 1'可以通过这个实验观察事务之间的隔离性和记录的可见性,结合以下 MySQL 内置命令来查看更多细节:
-- 查看 InnoDB 的事务状态
SHOW ENGINE INNODB STATUS;
-- 查看当前事务的 ID 和隔离级别
SELECT @@TRANSACTION_ISOLATION, @@tx_isolation;Navicat 可以帮助在图形界面中直观地管理和操作多个会话,但代码级别的调试,如跟踪 MySQL 源代码中具体的可见性判断过程,需要使用 GDB 等工具。
这里需要说明一点, 对于不同的事务隔离级别, 可见性的实现也不一样。
参考资料: https://dev.mysql.com/blog-archive/mysql-8-0-mvcc-of-large-objects-in-innodb/
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。