MVCC
当前读与快照读
1.当前读
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:
select .. lock in share mode(共享锁), select…for update、update、insert、delete(排他锁)都是一种当前读。
2.快照读
简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
- Read Committed:每次select,都生成一个快照读。
- Repeatable Read:开启事务后第一个select语句才是快照读的地方。
- Serializable:快照读会退化为当前读。
3.MVCC
全称 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现
MVCC允许多个事务同时读取同一行数据,而不会彼此阻塞,每个事务看到的数据版本是该事务开始时的数据版本。这意味着,如果其他事务在此期间修改了数据,正在运行的事务仍然看到的是它开始时的数据状态,从而实现了非阻塞读操作。
MVCC提供了一个非阻塞读功能。MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log日志、readView。
MVCC
的实现依赖于:隐藏字段、Read View、undo log。在内部实现中,InnoDB
通过数据行的隐藏字段 creator_trx_id
和 Read View
来判断数据的可见性,如不可见,则通过数据行的 隐藏字段DB_ROLL_PTR
找到 undo log
中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View
之前已经提交的修改和该事务本身做的修改
了解MVCC对可重复读的实现原理之前,需要明白三件事情,隐藏字段、undo log 、Read View
隐藏字段
在内部,InnoDB
存储引擎为每行数据添加了三个 隐藏字段
creator_trx_id(6字节)
:表示最后一次插入或更新该行的事务 id。此外,delete
操作在内部被视为更新,只不过会在记录头Record header
中的deleted_flag
字段将其标记为已删除DB_ROLL_PTR(7字节)
回滚指针,指向该行的undo log
。如果该行未被更新,则为空。在Read View中会通过回滚指针形成指针链。DB_ROW_ID(6字节)
:如果没有设置主键且该表没有唯一非空索引时,InnoDB
会使用该 id 来生成聚簇索引
对于MVCC的实现来说,creator_trx_id
和 DB_ROLL_PTR
两个字段是最重要的。
ReadView
Read View
主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务”
主要有以下字段:
max_trx_id
:目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见min_trx_id
:活跃事务列表m_ids
中最小的事务 ID,如果m_ids
为空,则min_trx_id
为max_trx_id
。小于这个 ID 的数据版本均可见m_ids
:Read View
创建时其他未提交的活跃事务 ID 列表。创建Read View
时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids
不包括当前事务自己和已提交的事务(正在内存中)creator_trx_id
:创建该Read View
的事务 ID
事务可见性示意图
undo-log
undo log
主要有两个作用:
- 当事务回滚时用于将数据恢复到修改前的样子
- 另一个作用是
MVCC
,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过undo log
读取之前的版本数据,以此实现非锁定读
在undo log 中根据隐藏字段中的 DB_ROLL_PTR
来将记录版本连成链表,通过隐藏字段creator_trx_id
来记录事务id
数据可见性算法
在 InnoDB
存储引擎中,创建一个新事务后,执行每个 select
语句前,都会创建一个快照(Read View),快照中保存了当前数据库系统中正处于活跃(没有 commit)的事务的 ID 号。其实简单的说保存的是系统中当前不应该被本事务看到的其他事务 ID 列表(即 m_ids(事务 ID集合))。当用户在这个事务中要读取某个记录行的时候,InnoDB
会将该记录行的 creator_trx_id
与 Read View
中的一些变量及当前事务 ID 进行比较,判断是否满足可见性条件
如果记录
creator_trx_id
(当前事务id)<min_trx_id(最小的事务 ID)
,那么表明最新修改该行的事务creator_trx_id
在当前事务创建快照之前就提交了,所以该记录行的值对当前事务是可见的如果
creator_trx_id
(当前事务id) >=max_trx_id(最大的事务 ID+1)
,那么表明最新修改该行的事务creator_trx_id
在当前事务创建快照之后才修改该行,所以该记录行的值对当前事务不可见。跳到步骤 5m_ids(事务 ID集合)
为空,则表明在当前事务创建快照之前,修改该行的事务就已经提交了,所以该记录行的值对当前事务是可见的如果
min_trx_id(最小的事务 ID)
<=creator_trx_id
<max_trx_id(最大的事务 ID+1)
,表明最新修改该行的事务creator_trx_id
在当前事务创建快照的时候可能处于“活动状态”或者“已提交状态”;所以就要对活跃事务列表m_ids(事务 ID集合)
进行查找(源码中是用的二分查找,因为是有序的)
如果在活跃事务列表
m_ids(事务 ID集合)
中能找到creator_trx_id
,表明:① 在当前事务创建快照前,该记录行的值被事务 ID 为creator_trx_id
的事务修改了,但没有提交;或者 ② 在当前事务创建快照后,该记录行的值被事务 ID 为creator_trx_id
的事务修改了。这些情况下,这个记录行的值对当前事务都是不可见的。跳到步骤 5在活跃事务列表中找不到,则表明“
id
为trx_id
的事务”在修改“该记录行的值”后,在“当前事务”创建快照前就已经提交了,所以记录行对当前事务可见
- 在该记录行的 DB_ROLL_PTR 指针所指向的
undo log
取出快照记录,用快照记录的creator_trx_id
跳到步骤 1 重新开始判断,直到找到满足的快照版本或返回空
MVCC 解决不可重复读问题
在事务隔离级别 RC
和 RR
(InnoDB 存储引擎的默认事务隔离级别)下,InnoDB
存储引擎使用 MVCC
(非锁定一致性读),但它们生成 Read View
的时机却不同
- 在 RC 隔离级别下的
每次select
查询前都生成一个Read View
(m_ids 列表) - 在 RR 隔离级别下只在事务开始后
第一次select
数据前生成一个Read View
(m_ids 列表)
RC隔离级别下MVCC 不可重复读问题
虽然 RC 和 RR 都通过 MVCC
来读取快照数据,但由于 生成 Read View 时机不同,从而在 RR 级别下实现可重复读
对于RC的情况,在每次查询查询时都会生成ReadView,根据Read View里的四个字段,分别为当前事务ID,最小事务ID,最大事务ID+1,活动事务ID集合,再结合数据可见性算法来从undo log中的数据链中的事务ID一个一个的分析,直道找到一个可以访问的数据为止。
从undo log日志表中找到最新数据记录版本的事务ID,看这个事务ID和undo log四个字段的关系
- 如果当前版本事务ID等于当前事务ID,说明该数据是由当前事务修改的,则可以访问该版本数据
- 如果当前版本事务ID小于最小事务ID,说明事务已经提交,可以访问该版本数据
- 如果当前版本事务ID大于最大事务ID+1,说明该事务是在readview生成后才开启,不可以访问该版本数据
- 如果当前版本事务ID大于等于最小事务ID,并且小于等于最大事务ID+1,并且当前版本事务ID不处于事务ID集合中,说明数据已经提交,可以访问该版本数据。
如果这四个条件都不满足,表明undo log当前版本的数据不能访问,则顺着链找到下一个版本的数据,重复上面五个步骤,直到找到可以访问的版本数据。
在RC隔离级别下无法解决不可重复读的问题的根本原因就是因为ReadView的生成,在同一个事务中的每次查询都会生成新的ReadView,这就导致了不可重复读的问题
与RC相反的是RR,它解决了不可重复读的问题,原因就是在事务开启后,查询数据前生成一份ReadView,整个事务过程中只会生成这一份ReadView,所以可以解决不可重复读的问题
RR隔离级别下MVCC 解决不可重复读问题
RR和RC在处理不可重复读的问题上的不同之处,就是ReadView的生成问题。
RR在事务开启后,查询数据前生成一份ReadView,整个事务过程中只会生成这一份ReadView,所以可以解决不可重复读的问题。在对版本数据访问的判断过程和RC的过程一样。
MVCC➕Next-key-Lock 防止幻读
InnoDB
存储引擎在 RR 级别下通过 MVCC
和 Next-key Lock
来解决幻读问题:
- 快照读的情况下使用MVCC解决幻读问题
- 当前读的情况下使用MVCC + Next-key Lock 解决幻读问题
1、执行普通 select
,此时会以 MVCC
快照读的方式读取数据
在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 Read View
,并使用至事务提交。所以在生成 Read View
之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”
2、执行 select…for update/lock in share mode、insert、update、delete 等当前读
在当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!InnoDB
使用 Next-key Lock (临键锁) 来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读