多版本并发控制(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 UNCOMMITTED
READ COMMITTED
REPEATABLE 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 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。