背景
我们都知道,在MySQL InnoDB中,支持四种事物隔离级别,分别为:
1、READ UNCOMMITED(未提交读):使用查询语句不会加锁,允许脏读,也就是可能读取到其他会话中未提交事务修改的数据。
2、READ COMMITED(提交读):只能读取到已经提交的数据,只对记录加记录锁,而不会在记录之间加间隙锁,所以允许新的记录插入到被锁定记录的附近,所以再多次使用查询语句时,可能得到不同的结果(Non-Repeatable Read)。
3、REPEATABLE READ(可重复读):多次读取同一范围的数据会返回第一次查询的快照,会返回相同的数据行。
4、SERIALIZABLE(串行读):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞。
其中,REPEATABLE READ(下文中简称RR) 是MySQL的默认事务隔离级别,而RR的核心实现机制,是基于MVCC(Multi-Version Concurrency Control) 机制来进行实现的。
在MVCC
中,非常重要的一个实现机制则是依赖于ReadView
,那么什么是MVCC的 ReadView
,本篇我们就来一起探讨一下。
MySQL InnoDB MVCC机制
在前面的文章中,我们介绍过MySQL的MVCC机制,这里我们做个简单回顾。
MVCC多版本控制
指的是一种提高并发的技术。
最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。
引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。
InnoDB的一致性的非锁定读就是通过MVCC
实现的,MySQL的大多数事务型存储引擎实现的都不是简单的行级锁。
基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制(MVCC
)。
MVCC
的实现,是通过保存数据在某一个时间点的快照来实现的。因此每一个事务无论执行多长时间看到的数据,都是一样的。
MVCC实现细节
MVCC的
核心实现主要基于两部分:多事务并发操作数据与一致性读实现。
多事务并发操作数据
多事务并发操作数据核心基于Undo log
进行实现,Undo log
可以用来做事务的回滚操作,保证事务的原子性。
同时可以用来构建数据修改之前的版本,支持多版本读。
InnoDB中,每一行记录都有两个隐藏列:DATA_TRX_ID和DATA_ROLL_PTR。(若没有主键,则还有一个隐藏主键)
- DATA_TRX_ID:记录最近更新这条记录的事务ID(6字节)
- DATA_ROLL_PTR:指向该行回滚段的指针,通过指针找到之前版本,通过链表形式组织(7字节)
- DB_ROW_ID:行标识(隐藏单增ID),没有主键时主动生成(6字节)
当存在多个事务进行并发操作数据时,不同事务对同一行的更新操作产生多个版本,通过回滚指针将这些版本链接成一条Undo Log
链。
操作过程如下:
-
1、将待操作的行加排他锁。
-
2、将该行原本的值拷贝到
Undo Log
中,DB_TRX_ID
和DB_ROLL_PTR
保持不变。(形成历史版本) -
3、修改该行的值,更新该行的
DATA_TRX_ID
为当前操作事务的事务ID,将DATA_ROLL_PTR
指向第二步拷贝到Undo Log
链中的旧版本记录。(通过DB_ROLL_PTR
可以找到历史记录) -
4、记录
Redo Log
,包括Undo Log
中的修改。 -
INSERT操作:产生新的记录,其
DATA_TRX_ID
为当前插入记录的事务ID。 -
DELETE操作:软删除,将
DATA_TRX_ID
记录下删除该记录的事务ID,真正删除操作在事务提交时完成。
一致性读实现
在InnoDB中,对于不同的事务隔离级别,一致性读实现均不相同,具体如下:
READ UNCOMMITED
隔离级别:直接读取版本的最新记录。SERIALIZABLE
隔离级别:通过加锁互斥访问数据实现。READ COMMITED
和REPEATABLE READ
隔离级别:使用版本链实现。(ReadView,可读性视图)
对于RC
与RR
隔离级别,实现一致性读都是通过ReadView
,也就是今天的重点,什么是ReadView
?
MVCC ReadView
ReadView
是事务开启时,当前所有活跃事务(还未提交的事务)的一个集合,ReadView
数据结构决定了不同事务隔离级别下,数据的可见性。
Read view lists the trx ids of those transactions for which a consistent read should not see the modifications to the database.
ReadView
的数据结构如下示例:
ReadView的组成:
up_limit_id:
The read should see all trx ids which are strictly smaller (<) than this value.
In other words, this is the low water mark".
low_limit_id:
The read should not see any transaction with trx id >= this value.
In other words, this is the “high water mark”.
m_ids:
Set of RW transactions that was active when this snapshot was taken.
- up_limit_id:最先开始的事务,该SQL启动时,当前事务链表中最小的事务id编号,也就是当前系统中创建最早但还未提交的事务
- low_limit_id:最后开始的事务,该SQL启动时,当前事务链表中最大的事务id编号,也就是最近创建的除自身以外最大事务编号
- m_ids:当前活跃事务ID列表,所有事务链表中事务的id集合
注:ID越小,事务开始的越早;ID越大,事务开始的越晚
OK,ReadVeiw核心数据结构如上所示,我们来解读一下两个核心字段low_limit_id
与up_limit_id
。
首先需要明确两个重点概念(敲黑板!!!):
- 1、下面所说的
db_trx_id
,是来自于数据行中的db_trx_id
字段,并非开启了一个事务分配的ID,分配的事务ID只有操作了数据行,才会更新数据行中的db_trx_id
字段 - 2、
ReadView
是与SQL绑定的,而并不是事务,所以即使在同一个事务中,每次SQL启动时构造的ReadView
的up_trx_id
和low_trx_id
也都是不一样的
up_limit_id
表示“低水位”,即当时活跃事务列表的最小事务id(最早创建的事务),如果读取出来的数据行上的的db_trx_id
小于up_limit_id
,则说明这条记录的最后修改在ReadView
创建之前,因此这条记录可以被看见。
if (trx_id < view->up_limit_id) {
return(TRUE);
}
low_limit_id
表示“高水位”,即当前活跃事务的最大id(最晚创建的事务),如果读取出来的数据行上的的db_trx_id
大于low_limit_id
,则说明这条记录的最后修改在ReadView
创建之后,因此这条记录肯定不可以被看见。
if (trx_id > view->low_limit_id) {
return(FALSE);
}
如果读取出来的数据行上的的db_trx_id
在low_limit_id
和up_limit_id
之间,则查找该数据上的db_trx_id
是否在ReadView
的m_ids
列表中:
- 如果存在,则表示这条记录的最后修改是在
ReadView
创建之时,被另外一个活跃事务所修改,所以这条记录也不可以被看见。 - 如果不存在,则表示这条记录的最后修改在
ReadView
创建之前,所以可以看到。
这里非常的绕,如果没看懂,请您多读几遍,仔细理解一下这个过程。
不同的事务隔离级别下,生成ReadView
的时机则各不相同,下面我们分别来看看一下RR
与RC
的ReadView
。
REPEATABLE READ下的ReadView生成
每个事务首次执行SELECT
语句时,会将当前系统所有活跃事务拷贝到一个列表中生成ReadView
。
每个事务后续的SELECT
操作复用其之前生成的ReadView
。
UPDATE,DELETE,INSERT
对一致性读snapshot
无影响。
示例:事务A,B同时操作同一行数据
- 若事务A的第一个
SELECT
在事务B提交之前进行,则即使事务B修改记录后先于事务A进行提交,事务A后续的SELECT
操作也无法读到事务B修改后的数据。 - 若事务A的第一个
SELECT
在事务B修改数据并提交事务之后,则事务A能读到事务B的修改。
针对RR
隔离级别,在第一次创建ReadView
后,这个ReadView
就会一直持续到事务结束,也就是说在事务执行过程中,数据的可见性不会变,所以在事务内部不会出现不一致的情况。
READ COMMITED下的ReadView生成
每次SELECT
执行,都会重新将当前系统中的所有活跃事务拷贝到一个列表中生成ReadView
。
针对RC
隔离级别,事务中的每个查询语句都单独构建一个ReadView
,所以如果两个查询之间有事务提交了,两个查询读出来的结果就不一样。
总结一下:
RC的本质:每一条SELECT
都可以看到其他已经提交的事务对数据的修改,只要事务提交,其结果都可见,与事务开始的先后顺序无关。
RR的本质:第一条SELECT
生成ReadView
前,已经提交的事务的修改可见。
从这里可以看出,在InnoDB中,RR
隔离级别的效率是比RC
隔离级别的高。
此外,针对RU隔离级别,由于不会去检查可见性,所以在一条SQL中也会读到不一致的数据。
针对串行化隔离级别,InnoDB是通过锁机制来实现的,而不是通过多版本控制的机制,所以性能很差。
结语
本篇,我们了解了InnoDB中的MVCC的实现机制,这里来总结一下:
MVCC的
核心实现主要基于两部分:多事务并发操作数据与一致性读实现- 多事务并发操作数据核心基于
Undo Log
进行实现 - 一致性读实现核心基于
ReadView
进行实现
关于MVCC
的实现,还是极为复杂的,本文仅谈了一点皮毛,如果您对更多的核心细节感兴趣,还是建议阅读官方文档或其他权威资料,感谢您的阅读。
本篇参考:
ReadView Class Reference
MySQL · 引擎特性 · InnoDB 事务系统
MySQL InnoDB MVCC实现
MySQL中的MVCC