热衷学习,热衷生存!😄
积淀、分享、成长,让本人和别人都能有所播种!😄
一、一致性非锁定读
对于 一致性非锁定度 的实现,通常的形式是加一个版本号或者工夫戳,在更新数据的时候版本号 + 1 或者更新工夫戳。查问时,将以后可见的版本号与对应记录的版本号做比照,如果记录的版本号小于可见版本,则示意该记录可见。
在 InnoDB
存储引擎中,多版本控制 就是对一致性非锁定读的实现。如果读取的行正在执行 delete
或者 update
操作,这时候读取操作不会去期待行开释锁,而是会去读取行的一个快照数据,对于这种读取历史数据的形式,叫做 快照度。
在 可反复读 和读取已提交 两个隔离级别下,如果是执行一般的 select
语句(不包含 select ... lock in share mode, select ... for update
)则会应用 一致性非锁定读
。
并且在 可反复读 下 MVCC
实现了可反复读和避免局部幻读。
二、锁定读(以后读)
如果执行的是上面语句,就是 锁定读。
select ... lock in share mode
select ... for update
insert
、update
、delete
操作
在锁定读下,读取的是数据的最新版本,这种读也被称为 以后读。锁定读会对读取到的记录加锁:
select ... lock in share mode
:对记录加S
锁,其余事务也能够加S
锁,如果加X
锁则会被阻塞。select ... for update
、insert
、update
、delete
:对记录加X
锁,且其余事务不能加任何锁。
在一致性非锁定读下,即便读取的数据曾经被其它事务加上了 X
锁,记录也是能够被读取的,读取的是快照数据。下面说了在 可反复读隔离级别 下MVCC
避免了局部幻读,这个 局部 是指在 一致性锁定读
状况下,只能读取到第一次查问之前插入的数据(依据 Read View
判断数据可见性,Read View
在第一次查问时生成)。然而如果是 以后读 ,每次读取的都是最新数据,这个如果两次查问两头有其余事物插入数据就能够产生幻读。所以InnoDB
在可重读 时,如果以后执行的是以后读,则会对读取的记录 应用Next-Key Lock
,来避免其余事物在间隙间插入数据。
快照读和以后读栗子
开启 A 和 B 两个会话。
首先在 A 会话中查问 user_id = 1
的user_name
的记录:
begin;
select user_name from t_user where user_id = 1;
查问进去的后果是:user_name = '张三'
。
而后再 B 会话对 user_id = 1
的user_name
进行批改:
update t_user set user_name = '李四' where user_id = 1;
而后再回到 A 会话持续做查问操作:
select user_name from t_user where user_id = 1;
select user_name from t_user where user_id = 1 for update;
select user_name from t_user where user_id = 1 lock in share mode;
三条数据查问进去的后果别离是:user_name = '张三'
、user_name = '李四'
、user_name = '李四'
能够看出 A 会话中的第一条查问是快照读,读取到的以后事务开启时的数据记录,前面两个查问是以后读,读取到的是最新数据。
三、InnoDB 对 MVCC 的实现
MVCC(Multi-Version Concurrency Control)
多版本并发管制。
MVCC
的实现次要依赖于:暗藏字段、Read View
、undo log
。在外部实现中通过数据行的 DB_TRX_ID
和Read View
来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR
找到 undo log
中的历史版本。每个事务读取到的数据版本可能是不统一的,在同一个事务中,用户只能看到该事务创立 Read View
之前曾经提交的批改或者该事务自身做的批改。
暗藏字段
在外部,InnoDB
存储引擎为每行数据增加了三个暗藏字段,如下:
DB_TRX_ID(6 字节)
:示意最初一次插入或者更新改行的事务 id,当咱们要开始一个事务时,会向InnoDB
的事务零碎申请一个事务 id,这个事务 id 是 一个严格递增且惟一的数字 ,以后行是被哪个事务批改的,就会把对应的事务 id 记录在以后行中。对于delete
操作会在记录头Record header
中的delete_flag
字段将其标记为已删除。DB_ROLL_PTR(7 字节)
:回滚指针,这个回滚指针指向一个undo log
日志的地址,能够通过undo log
日志放这条记录复原到历史版本,如果该行未被更新,则为空。DB_ROW_ID(6 字节)
:行 id,用来惟一标识一行数据,如果没有设置主键且该表没有惟一非空索引时,会应用该 id 来当主键生成聚簇索引。
Read View
class ReadView {
private:
trx_id_t m_low_limit_id; /* 大于等于这个 id 的事务均不可见 */
trx_id_t m_up_limit_id; /* 小于这个 id 的事务均可见 */
trx_id_t m_creator_trx_id; /* 创立该 Read View 的事务 id*/
trx_id_t m_low_limit_no; /* 事务 Number, 小于该 Number 的 Undo log 均能够被 purge*/
ids_t m_ids; /* 创立该 Read View 时的沉闷事务列表 */
m_closed; /* 标记 Read View 是否敞开 */
}
Read View
次要是用来做可见性判断,外面保留了“以后对本事务不可见的其余沉闷事务”。
次要有以下字段:
m_low_limit_id
:目前呈现过的最大事务 id+1,即下一个将为调配的事务 id,大于等于这个 id 的数据版本均不可见。m_up_limit_id
:沉闷事务列表m_ids
中最小的事务 id,如果m_ids
为空,则为m_low_limit_id
,小于这个 id 的数据版本均可见。m_ids
:Read View
创立时其余未提交沉闷事务 ID 列表。创立Read View
时,将以后未提交事务 id 记录下来,后续即便他们批改了记录行的值,对于以后事务也是不可见的。m_ids
不包含以后事务本人和已提交的事务。m_creator_trx_id
:创立该Read View
的事务 id。
事务可见性示意图
undo log
undo log
次要有两个作用:
- 将事务回滚时用于将数据恢复到批改前的样子。
- 另一个作用是
MVCC
,当读取记录时,若该条记录被其余事务占用或者以后版本对该事务不可见时,则能够通过undo log
读取之前的版本数据,实现非锁定读。
在 InnoDB
存储引擎中 undo log
分为了两种:insert undo log
和update undo log
:
-
insert undo log
:在insert
操作中产生的undo log
。因为insert
操作的记录只对事务自身可见,对其余事务不可见,所以insert undo log
能够在事务提交后间接删除。不须要purge
操作。insert
时数据初始化状态: -
update undo log
:update
或者delete
操作中产生的undo log
。该undo log
可能须要提供MVCC
机制,因而不能在事务提交后就进行删除。提交时放入undo log
链表,期待purge 线程
进行最初的删除。数据第一次被批改时:
数据第二次被批改时:
不同事务或者雷同事务对同一记录进行的批改,会使该行记录的 undo log
成为一条链表,链首就是最新的记录,链尾就是最早的旧记录。
数据可见性算法
在 InnoDB
存储引擎中,创立一个新事务后,执行每个 select
语句前都会创立一个快照(Read View
),快照中 保留了以后数据库所有正在处于沉闷的事务(没有提交)id。说简略点就是保留了不应该被以后事务所能见的其余事务 id(即 m_ids)。当用户在这个事务中要读取某个记录行的时候,InnoDB
会将该记录行的 DB_TRX_ID
与Read View
中的以后事务事务 id 进行比拟,判断是否满足可见条件。
具体的比拟算法源码如下:图源
- 如果记录
DB_TRX_ID < m_up_limit_id
示意最新批改的该行事务在以后事务创立快照之前就曾经提交了,所以改行记录的值对以后事务是可见的。 - 如果记录
DB_TRX_ID >= m_low_limit_id
示意最新批改的行事务在以后事务创立快照之后再才批改该行,所以该记录行的值对以后事务是不可见的,跳到步骤 5。 md_ids
为空,阐明在以后事务创立快照之前,批改该行的事务就曾经提交了,所以该记录行的值对所有事务都可见。-
如果
m_up_limit_id <= DB_TRX_ID < m_low_limit_id
,表明最新批改行的事务在以后事务创立快照时可能处于“沉闷状态”或者“已提交状态”。所以要对沉闷事务列表m_ids
进行查找(源码中用的二分查找法):- 如果在沉闷事务列表
m_ids
能找到DB_TRX_ID
阐明:①在以后事务创立快照时,改行记录的值被事务DB_TRX_ID
的事务批改了,但没有提交;或者②在以后事务创立快照后,该记录行的值被 ID 为DB_TRX_ID
的事务批改了,这些状况下这个记录行的值对以后事务是不可见的。跳到步骤 5。 - 如果在沉闷事务列表
m_ids
找不到,阐明DB_TRX_ID
的事务在批改该记录行的值在以后事务创立快照前曾经提交了,所以该行记录的值对以后事务是可见的。
- 如果在沉闷事务列表
- 在该行记录的
DB_ROLL_PTR
执行所指向的undo log
取出快照数据,用快照数据的DB_TRX_ID
跳到步骤 1 从新开始判断直到找到满足的快照版本或返回空。
四、读取已提交(RC)和可反复(RR)隔离级别下 MVCC 的差别。
在事务隔离级别 RC
和RR
下,InnoDB
存储引擎应用 MVCC
生成的 Read View
的机会不同。
- 在
RC
隔离级别下每次 select
查问前都生成一个Read View
(m_ids
列表)。 - 在
RR
隔离级别下只在事务开始后第一次 select
数据前生成一个Read View
(m_ids
列表)。
五、MVCC 解决不可反复读问题
尽管 RC
和RR
都通过 MVCC
来读取快照数据,然而因为 生成 Read View
机会不同,从而在 RR
级别下实现可反复读。
举个例子:
事务 101 | 事务 102 | 事务 103 | |
---|---|---|---|
T1 | begin; | ||
T2 | begin; | begin; | |
T3 | update user set name = ‘ 张三 ’ where id = 1; | ||
T4 | update user set name = ‘ 李四 ’ where id = 1; | … | select * from user where id = 1; |
T5 | commit; | update uset set name = ‘ 王五 ’ where id = 1; | |
T6 | select * from user where id = 1; | ||
T7 | update uset set name = ‘ 赵六 ’ where id = 1; | ||
T8 | commit; | ||
T9 | select * from user where id = 1; | ||
T10 | commit; |
在 RC 下 Read View 生成状况
-
假如工夫线来到 T4,那么此时数据行 id = 1 的版本链为:
因为
RC
级别下每次查问都会生成Read View
,并且事务 101、102 没有提交,此时 103 事务生成的Read View
中沉闷事务为m_ids
为[101,102],m_low_limit_id
为 104,m_up_limit_id
为 101,m_creator_id
为 103。- 此时最新记录的
DB_TRX_ID
为 101,所以m_up_limit_id <= DB_TRX_ID < m_low_limit_id
,所以要在m_ids
列表中查找,发现DB_TRX_ID
存在列表中,所以这个记录不可见。 - 杜绝
DB_ROLL_PTR
找到undo_log
中上一版本记录,上一条记录的DB_TRX_ID
还是 101 不可见。 - 持续找上一条
DB_TRX_ID
为 1,满足1 < m_up_limit_id
所以可见,所以事务 103 查问的数据为name = 菜花
。
- 此时最新记录的
-
假如工夫线来到 T6,数据的版本链为:
因为在
RC
级别下,从新生成Read View
,此时事务 101 曾经提交,102 事务未提交,所以此时Read View
中沉闷的事务m_ids
为[102],m_low_limit_id
为 104,m_up_limit_id
为 102,m_creator_id
为 103。- 此时最新记录的
DB_TRX_ID
为 102,m_up_limit_id <= DB_TRX_ID < m_up_limit_ud
,所以要在m_ids
中查找,发现DB_TRX_ID
存在列表中,那么这个记录不可见。 - 依据
DB_ROLL_PTR
找到undo log
中的上一版本记录,上一条记录的DB_TRX_ID
为 101,满足101 < t_up_limit_id
,所以记录可见,所以在T6
工夫点查问到的数据为name = 李四
,与工夫T4
查问到的后果不统一,产生了不可反复读。
- 此时最新记录的
-
假如工夫先来到 T9,数据的版本链为:
- 因为在
RC
级别下,从新生成Read View
,此时事务 101、102 都曾经提交,所以m_ids
为空,则m_up_limit_id = m_low_limit_id = 104
,最新版本事务 ID 为 102,满足102 < m_up_limit_id
,所以可见,查问后果为name = 赵六
。
- 因为在
总结:在 RC 隔离级别下,事务在每次查问的开始都会生成Read View
,所以导致不可反复读。
在 RR 选 Read View 生成状况
在可反复读级别下,只会在事务开始后的第一次读取数据是生成一个Read View
(m_ids)。
-
假如工夫线来到 T4,那么此时数据行 id = 1 的版本链为:
在执行以后
select
语句时生成一个Read View
,事务 101,102 未提交,此时m_ids
为[101,102],m_low_limit_id
为 104,m_up_limit_id
为 101,m_creator_trx_id
为 103此时和 RC 级别下一样:
- 最新记录的
DB_TRX_ID
为 101,m_up_limit_id <= 101 < m_low_limit_id,所以要在m_ids
列表中查找,发现DB_TRX_ID
存在列表中,那么这个记录不可见 - 依据
DB_ROLL_PTR
找到undo log
中的上一版本记录,上一条记录的DB_TRX_ID
还是 101,不可见 - 持续找上一条
DB_TRX_ID
为 1,满足 1 < m_up_limit_id,可见,所以事务 103 查问到数据为name = 菜花
。
- 最新记录的
-
假如工夫线来到 T6,那么此时数据行 id = 1 的版本链为:
因为在
RR
级别下只会生成一次Read View
,所以此时m_ids
还是为 [101,102],m_low_limit_id
为 104,m_up_limit_id
为 101,m_creator_trx_id
为 103- 最新记录
DB_TRX_ID
为 102,满足m_up_limit_id <= 102 < m_low_limit_id
,且在m_ids
中存在 102,所以这个记录不可见。 - 依据
BD_ROLL_PTR
找到undo log
中的上一版本,上一条记录的DB_TRX_ID
为 101,和下面一样,不可见。 - 持续依据
DB_ROLL_PTR
找到undo log
的中上一版本记录,上一条记录的DB_TRX_ID
还是 101,还是不可见。 - 持续找上一条
DB_TRX_UD
为 1,满足1 < m_up_limit_id
,可见,所以事务 103 查问到的数据为name= 菜花
,和T4
查问进去的后果一样,防止了不可反复。
- 最新记录
-
假如工夫线来到 T9,那么此时数据行 id = 1 的版本链为:
此时状况和
T6
齐全一样,因为曾经生成了Read View
,此时仍然沿用m_ids
:[101,102],所以查问后果仍然是name = 菜花
。
总结:在 RR
级别下只会在事务开始后的第一次查问生成Read View
,所以能够防止不可反复的景象。
六、MVCC+Next-key Lock 避免幻读
InnoDB
存储引擎在 RR 级别下通过 MVCC
和Next-key Lock
来解决幻读问题:
-
执行一般
select
,此时会以MVCC
快照读的形式读取数据在快照读的状况下,
RR
隔离级别只会在事务开始后的第一次查问生成Read View
,并应用至事务提交。所以在生成Read View
之后其它事务所做的更新、插入记录版本对以后事务并不可见,实现了可反复读和避免快照读下的“幻读”。 -
执行
select for update/lock int share mode、insert、update、delete
等以后读、在以后读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在以后事务查问范畴内,就会产生幻读。
InnoDB
应用Next-key lock
来避免这种状况,在执行以后读时,会锁定读取到的记录,同时也会锁定它们的间隙,避免其它事务在查问范畴内插入数据,只有我不让你插入,就不会产生幻读。
参考:https://javaguide.cn/database…