【MySQL系统架构设计】
【MySQL索引设计与选择】
【MySQL事务底层原理】
一、MVCC机制
在了解 MySQL 事务时,首先我们需要明确一个概念 MVCC 机制。MVCC(Multi-Version Concurrency Control)是一种数据库管理系统中常见的并发控制机制,用于管理多个事务同时访问数据库时的数据一致性和隔离性。MVCC的核心思想是为每个事务创建一个可见的、独立的数据版本,而不是锁定整个表或数据行,从而允许多个事务并发执行而不相互干扰。
MySQL 的高速读写能力,离不开对 MVCC 的具体实现,引入 MVCC 后,数据库操作只有写写之间是相互阻塞,读写、写读、读读都是可以并行的。
在了解MVCC多版本并发控制之前,我们必须先了解一下,什么是 MySQL InnoDB 下的 当前读 和 快照读。
1.1 当前读
指读取的记录是最新版本,读取时还需保证其他并发事务不能修改当前记录,需要对读取记录进行加锁。比如 select ... lock in share mode
、select … for update、update、insert、delete 这些操作都是一种当前读。
1.2 快照读
指不对select操作进行加锁的读取。
快照读的出现,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,它在很多情况下,避免了加锁操作,降低了开销。在并发事务下,快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
在 InnoDB 中,快照读的前提是事务隔离级别不能为 串行化,在串行化级别下的快照读会退化为当前读;
二、MySQL MVCC实现原理
MVCC 主要目的是为了解决 读写冲突,在 MySQL InnoDB 中,主要通过行记录中3个隐式字段,undo log,read view来实现,下面我们详细分析一下这三个具体作用。
2.1 行记录3个隐式字段
在 MySQL InnoDB 数据行中,除了记录我们自定义的字段外,数据库还隐式定义了 DB_TRX_ID
、DB_ROLL_PTR
和 DB_ROW_ID
等隐式字段。
DB_ROW_ID
:隐含的自增ID(隐藏主键),占6byte。如果数据表没有主键,InnoDB会自动以DB_ROW_ID
生成一个聚簇索引。DB_TRX_ID
:最近修改(修改/插入)事务ID,占6byte。用于记录当前记录最后一次修改的事务ID,每次事务修改自动+1。DB_ROLL_PTR
:回滚指针,占7byte。指向这条记录的上一个版本 (存储于rollback segment回滚段里),通过当前指针才能查找之前版本的数据。
2.2 Undo Log
MySQL 在进行数据操作是,需要将记录先读取到 Buffer Pool 中,然后进行修改,最后再进行刷盘,为了保证数据的持久性,在进行修改后,需写入一个 Undo Log 日志,记录修改后的数据,以便后续崩溃恢复。
Undo Log 日志主要分为两种:
- Insert Undo Log:记录 MySQL 进行
INSERT
时产生的 Undo Log。只需要在事务回滚时需要,事务提交后可立即删除。 - Update Undo Log:记录 MySQL 进行
UPDATE
、DELETE
时产生的 Undo Log。在事务回滚时需要,在快照读时也需要;不能立即删除,只有在快照读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清理。
不同事务或者相同事务的对同一记录的修改,会导致该记录的 Undo Log 成为一条记录版本线性表,既链表,Undo Log 的链首就是最新的旧记录,链尾就是最早的旧记录。
2.2.1 purge线程
为了实现 InnoDB 的 MVCC 机制,更新或者删除操作都只是设置一下旧记录的 delete_mask
,并不真正将旧记录删除。并且为了节省磁盘空间,InnoDB有专门的 purge 线程来清理 delete_mask
为 true
的记录。为了不影响MVCC的正常工作,purge 线程自己也维护了一个 read view(这个 read view 相当于系统中最老活跃事务的read view),如果某个记录的 delete_mask
为 1,并且 DB_TRX_ID
相对于 purge 线程的 read view 可见,那么这条记录一定是可以被安全清除的。
2.3 Read View
Read View 是用于记录每个事务开始时间的数据结构,以便 MySQL InnoDB 在查询时能够根据事务启动时间戳和各个数据行对应的版本链信息来确定该事务能够访问到哪些数据。
当一个事务开始执行时,MySQL InnoDB 会记录下该事务开始的时间戳。之后,在这个事务执行的过程中,如果需要读取某个数据行,MySQL引擎会根据这个时间戳和该数据行对应的版本链信息(Undo Log 链),找到最近的、在该事务启动时间之前已经提交的数据版本。这样,就可以保证在该事务的执行过程中,读取的数据是和该事务启动时一致的。
每次开启一个事务时,都会创建当前事务对应的 Read View。因此,不同的事务对同一行数据的读取结果可能是不同的,这取决于它们启动的时间。
Read View 包含多个字段,如下:
struct read_view_t{
ulint type;
undo_no_t undo_no;
trx_id_t low_limit_no;
trx_id_t low_limit_id;
trx_id_t up_limit_id;
ulint n_trx_ids;
trx_id_t* trx_ids;
trx_id_t creator_trx_id;
UT_LIST_NODE_T(read_view_t) view_list;
}
其中比如重要的字段如下:
TRX_IDS
:InnoDB 为每个事务构造了一个数组trx_ids
,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。(“活跃”指的就是,启动了但还没提交。)LOW_LIMIT_ID
:TRX_IDS
数组中的最小事务ID。UP_LIMIT_ID
:Read View 生成时刻系统尚未分配的下一个事务ID的值,也就是目前已出现过的事务ID的最大值+1。CREATOR_TRX_ID
:创建当前 Read View 的事务ID。
数组里面事务 ID 的最小值记为低水位(LOW_LIMIT_ID
),当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位(UP_LIMIT_ID
)。这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。
低水位和高水位将事务分成了三段:
Read View 通过判断这四个字段的方式来确定哪些数据对于当前事务来说是可见的,哪些是不可见的。判断过程如下:
- 如果
TRX_ID
(当前事务id) 等于CREATOR_TRX_ID
,则说明当前事务在访问它自己修改过的记录,所以这个版本可以被当前事务访问。 - 如果
TRX_ID
小于LOW_LIMIT_ID
,则说明在 Undo Log 版本链中这个事务