Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
wushizhenking
JavaGuide
提交
dd26b350
J
JavaGuide
项目概览
wushizhenking
/
JavaGuide
与 Fork 源项目一致
从无法访问的项目Fork
通知
2
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
J
JavaGuide
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
前往新版Gitcode,体验更适合开发者的 AI 搜索 >>
提交
dd26b350
编写于
7月 15, 2021
作者:
J
jiangrz
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
add InnoDB对MVCC的实现
上级
5cf515e6
变更
2
隐藏空白更改
内联
并排
Showing
2 changed file
with
272 addition
and
0 deletion
+272
-0
README.md
README.md
+1
-0
docs/database/InnoDB对MVCC的实现.md
docs/database/InnoDB对MVCC的实现.md
+271
-0
未找到文件。
README.md
浏览文件 @
dd26b350
...
...
@@ -148,6 +148,7 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle
2.
[
事务隔离级别(图文详解)
](
<
docs/database/事务隔离级别(图文详解).md
>
)
3.
[
一条 SQL 语句在 MySQL 中如何执行的
](
docs/database/一条sql语句在mysql中如何执行的.md
)
4.
[
关于数据库中如何存储时间的一点思考
](
docs/database/关于数据库存储时间的一点思考.md
)
5.
[
InnoDB存储引擎对MVCC的实现
](
docs/database/InnoDB对MVCC的实现.md
)
### Redis
...
...
docs/database/InnoDB对MVCC的实现.md
0 → 100644
浏览文件 @
dd26b350
<!-- TOC -->
-
[
一致性非锁定读和锁定读
](
#一致性非锁定读和锁定读
)
-
[
一致性非锁定读
](
#一致性非锁定读
)
-
[
锁定读
](
#锁定读
)
-
[
InnoDB对MVCC的实现
](
#InnoDB对MVCC的实现
)
-
[
隐藏字段
](
#隐藏字段]
)
-
[
ReadView
](
#ReadView
)
-
[
undo-log
](
#undo-log
)
-
[
数据可见性算法
](
#数据可见性算法
)
-
[
RC、RR隔离级别下MVCC的差异
](
#RC、RR隔离级别下MVCC的差异
)
-
[
MVCC解决不可重复读问题
](
#MVCC解决不可重复读问题
)
-
[
在RC下ReadView生成情况
](
#在RC下ReadView生成情况
)
-
[
在RR下ReadView生成情况
](
#在RR下ReadView生成情况
)
-
[
MVCC+Next-key-Lock防止幻读
](
#MVCC➕Next-key-Lock防止幻读
)
<!-- /TOC -->
## 一致性非锁定读和锁定读
#### 一致性非锁定读
对于
[
**一致性非锁定读(Consistent Nonlocking Reads)**
](
https://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html
)
的实现,通常做法是加一个版本号或者时间戳字段,在更新数据的同时版本号 + 1或者更新时间戳。查询时,将当前可见的版本号与对应记录的版本号进行比对,如果记录的版本小于可见版本,则表示该记录可见
在
`InnoDB`
存储引擎中,
[
多版本控制 (multi versioning)
](
https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html
)
就是对非锁定读的实现。如果读取的行正在执行
`DELETE`
或
`UPDATE`
操作,这时读取操作不会去等待行上锁的释放。相反地,
`InnoDB`
存储引擎会去读取行的一个快照数据,对于这种读取历史数据的方式,我们叫它快照读 (snapshot read)
在
`Repeatable Read`
和
`Read Committed`
两个隔离级别下,如果是执行普通的
`select`
语句(不包括
`select ... lock in share mode`
,
` select ... for update`
)则会使用
`一致性非锁定读(MVCC)`
。并且在
`Repeatable Read`
下
`MVCC`
实现了可重复读和防止部分幻读
#### 锁定读
如果执行的是下列语句,就是
[
**锁定读(Locking Reads)**
](
https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html
)
-
select ... lock in share mode
-
select ... for update
-
insert、update、delete 操作
在锁定读下,读取的是数据的最新版本,这种读也被称为
`当前读(current read)`
。锁定读会对读取到的记录加锁:
-
`select ... lock in share mode`
:对记录加
`S`
锁,其它事务也可以加
`S`
锁,如果加
`x`
锁则会被阻塞
-
`select ... for update`
、
`insert`
、
`update`
、
`delete `
:对记录加
`X`
锁,且其它事务不能加任何锁
在一致性非锁定读下,即使读取的记录已被其它事务加上
`X`
锁,这时记录也是可以被读取的,即读取的快照数据。上面说了在
`Repeatable Read`
下
`MVCC`
防止了部分幻读,这边的 “部分” 是指在
`一致性非锁定读`
情况下,只能读取到第一次查询之前所插入的数据(根据Read View判断数据可见性,Read View在第一次查询时生成),但如果是
`当前读`
,每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。
**所以 `InnoDB` 在实现`Repeatable Read` 时,如果执行的是当前读,则会对读取的记录使用 `Next-key Lock` ,来防止其它事务在间隙间插入数据**
## InnoDB对MVCC的实现
`MVCC`
的实现依赖于:
**隐藏字段、Read View、undo log**
。在内部实现中,
`InnoDB`
通过数据行的
`DB_TRX_ID`
和
`Read View`
来判断数据的可见性,如不可见,则通过数据行的
`DB_ROLL_PTR`
找到
`undo log`
中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建
`Read View`
之前已经提交的修改和该事务本身做的修改
#### 隐藏字段
在内部,
`InnoDB`
存储引擎为每行数据添加了三个
[
隐藏字段
](
https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html
)
:
-
`DB_TRX_ID(6字节)`
:表示最后一次插入或更新该行的事务id。此外,
`delete`
操作在内部被视为更新,只不过会在记录头
`Record header`
中的
`deleted_flag`
字段将其标记为已删除
-
`DB_ROLL_PTR(7字节)`
回滚指针,指向该行的
`undo log`
。如果该行未被更新,则为空
-
`DB_ROW_ID(6字节)`
:如果没有设置主键且该表没有唯一非空索引时,
`InnoDB`
会使用该id来生成聚簇索引
#### ReadView
[
`Read View`
](
https://github.com/facebook/mysql-8.0/blob/8.0/storage/innobase/include/read0types.h#L298
)
主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务”
主要有以下字段:
-
`m_low_limit_id`
:目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。大于这个ID的数据版本均不可见
-
`m_up_limit_id`
:活跃事务列表
`m_ids`
中最小的事务ID,如果
`m_ids`
为空,则
`m_up_limit_id`
为
`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`:**
1.
**`insert undo log`**
:指在
`insert`
操作中产生的
`undo log`
。因为
`insert`
操作的记录只对事务本身可见,对其他事务不可见,故该
`undo log`
可以在事务提交后直接删除。不需要进行
`purge`
操作
**`insert` 时的数据初始状态:**
![
markdown
](
https://ddmcc-1255635056.file.myqcloud.com/317e91e1-1ee1-42ad-9412-9098d5c6a9ad.png
)
2.
**`update undo log`**
:
`update`
或
`delete`
操作中产生的
`undo log`
。该
`undo log`
可能需要提供
`MVCC`
机制,因此不能在事务提交时就进行删除。提交时放入
`undo log`
链表,等待
`purge线程`
进行最后的删除
**数据第一次被修改时:**
![
markdown
](
https://ddmcc-1255635056.file.myqcloud.com/c52ff79f-10e6-46cb-b5d4-3c9cbcc1934a.png
)
**数据第二次被修改时:**
![
markdown
](
https://ddmcc-1255635056.file.myqcloud.com/6a276e7a-b0da-4c7b-bdf7-c0c7b7b3b31c.png
)
不同事务或者相同事务的对同一记录行的修改,会使该记录行的
`undo log`
成为一条链表,链首就是最新的记录,链尾就是最早的旧记录
#### 数据可见性算法
在
`InnoDB`
存储引擎中,创建一个新事务后,执行每个
`select`
语句前,都会创建一个快照(Read View),
**快照中保存了当前数据库系统中正处于活跃(没有commit)的事务的ID号**
。其实简单的说保存的是系统中当前不应该被本事务看到的其他事务ID列表(即m_ids)。当用户在这个事务中要读取某个记录行的时候,
`InnoDB`
会将该记录行的
`DB_TRX_ID`
与
`Read View`
中的一些变量及当前事务ID进行比较,判断是否满足可见性条件
[
具体的比较算法
](
https://github.com/facebook/mysql-8.0/blob/8.0/storage/innobase/include/read0types.h#L161
)
如下:
[
图源
](
https://leviathan.vip/2019/03/20/InnoDB%E7%9A%84%E4%BA%8B%E5%8A%A1%E5%88%86%E6%9E%90-MVCC/#MVCC-1
)
![
markdown
](
https://ddmcc-1255635056.file.myqcloud.com/8778836b-34a8-480b-b8c7-654fe207a8c2.png
)
1.
如果记录 DB_TRX_ID < m_up_limit_id,那么表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照之前就提交了,所以该记录行的值对当前事务是可见的
2.
如果 DB_TRX_ID >= m_low_limit_id,那么表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照之后才修改该行,所以该记录行的值对当前事务不可见。跳到步骤5
3.
m_ids 为空,则表明在当前事务创建快照之前,修改该行的事务就已经提交了,所以该记录行的值对当前事务是可见的
4.
如果 m_up_limit_id <= DB_TRX_ID < m_up_limit_id,表明最新修改该行的事务(DB_TRX_ID)在当前事务创建快照的时候可能处于“活动状态”或者“已提交状态”;所以就要对活跃事务列表 m_ids 进行查找(源码中是用的二分查找,因为是有序的)
- 如果在活跃事务列表 m_ids 中能找到 DB_TRX_ID,表明:①在当前事务创建快照前,该记录行的值被事务ID为 DB_TRX_ID 的事务修改了,但没有提交;或者 ②在当前事务创建快照后,该记录行的值被事务ID为 DB_TRX_ID 的事务修改了。这些情况下,这个记录行的值对当前事务都是不可见的。跳到步骤5
- 在活跃事务列表中找不到,则表明“id为trx_id的事务”在修改“该记录行的值”后,在“当前事务”创建快照前就已经提交了,所以记录行对当前事务可见
5.
在该记录行的 DB_ROLL_PTR 指针所指向的
`undo log`
取出快照记录,用快照记录的 DB_TRX_ID 跳到步骤1重新开始判断,直到找到满足的快照版本或返回空
## RC和RR隔离级别下MVCC的差异
在事务隔离级别
`RC`
和
`RR`
(InnoDB存储引擎的默认事务隔离级别)下,
` InnoDB`
存储引擎使用
`MVCC`
(非锁定一致性读),但它们生成
`Read View`
的时机却不同
-
在 RC 隔离级别下的
**`每次select`**
查询前都生成一个
`Read View`
(m_ids列表)
-
在 RR 隔离级别下只在事务开始后
**`第一次select`**
数据前生成一个
`Read View`
(m_ids列表)
## MVCC解决不可重复读问题
虽然 RC 和 RR 都通过
`MVCC`
来读取快照数据,但由于
**生成 Read View 时机不同**
,从而在 RR 级别下实现可重复读
举个例子:
![
markdown
](
https://ddmcc-1255635056.file.myqcloud.com/6fb2b9a1-5f14-4dec-a797-e4cf388ed413.png
)
#### **在RC下ReadView生成情况**
1.
**`假设时间线来到 T4 ,那么此时数据行 id = 1 的版本链为`:**
!
[
markdown
](
https://ddmcc-1255635056.file.myqcloud.com/a3fd1ec6-8f37-42fa-b090-7446d488fd04.png
)
由于 RC 级别下每次查询都会生成
` Read View`
,并且事务101、102 并未提交,此时
`103`
事务生成的
`Read View`
中活跃的事务
**`m_ids` 为:[101,102]**
,
`m_low_limit_id`
为:104,
`m_up_limit_id`
为:101,
`m_creator_trx_id`
为:103
-
此时最新记录的
`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 = 菜花`
2.
**`时间线来到 T6 ,数据的版本链为`:**
!
[
markdown
](
https://ddmcc-1255635056.file.myqcloud.com/528559e9-dae8-4d14-b78d-a5b657c88391.png
)
因为在 RC 级别下,重新生成
`Read View`
,这时事务101已经提交,102并未提交,所以此时
`Read View`
中活跃的事务
**`m_ids`:[102]**
,
`m_low_limit_id`
为:104,
`m_up_limit_id`
为:102,
`m_creator_trx_id`
为:103
-
此时最新记录的
`DB_TRX_ID`
为102,m_up_limit_id <= 102 < m_low_limit_id,所以要在
`m_ids`
列表中查找,发现
`DB_TRX_ID`
存在列表中,那么这个记录不可见
-
根据
`DB_ROLL_PTR`
找到
`undo log`
中的上一版本记录,上一条记录的
`DB_TRX_ID`
为101,满足 102 < m_up_limit_id,记录可见,所以在
`T6`
时间点查询到数据为
`name = 李四`
,与时间 T4 查询到的结果不一致,不可重复读!
3.
**`时间线来到 T9 ,数据的版本链为`:**
![
markdown
](
https://ddmcc-1255635056.file.myqcloud.com/6f82703c-36a1-4458-90fe-d7f4edbac71a.png
)
重新生成
`Read View`
, 这时事务 101 和 102 都已经提交,所以
**m_ids**
为空,则 m_up_limit_id = m_low_limit_id = 104,最新版本事务ID为102,满足 102 < m_low_limit_id,可见,查询结果为
`name = 赵六`
> **总结:** **在RC隔离级别下,事务在每次查询开始时都会生成并设置新的 Read View,所以导致不可重复读**
#### **在RR下ReadView生成情况**
**在可重复读级别下,只会在事务开始后第一次读取数据时生成一个Read View(m_ids列表)**
1.
**`在 T4 情况下的版本链为`:**
![
markdown
](
https://ddmcc-1255635056.file.myqcloud.com/0e906b95-c916-4f30-beda-9cb3e49746bf.png
)
在当前执行
`select`
语句时生成一个
`Read View`
,此时
**`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 = 菜花`
2.
**`时间点 T6 情况下`:**
!
[
markdown
](
https://ddmcc-1255635056.file.myqcloud.com/79ed6142-7664-4e0b-9023-cf546586aa39.png
)
在 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`
列表中查找,发现
`DB_TRX_ID`
存在列表中,那么这个记录不可见
-
根据
`DB_ROLL_PTR`
找到
`undo log`
中的上一版本记录,上一条记录的
`DB_TRX_ID`
为101,不可见
-
继续根据
`DB_ROLL_PTR`
找到
`undo log`
中的上一版本记录,上一条记录的
`DB_TRX_ID`
还是101,不可见
-
继续找上一条
`DB_TRX_ID`
为1,满足 1 < m_up_limit_id,可见,所以事务103查询到数据为
`name = 菜花`
3.
**时间点 T9 情况下:**
![
markdown
](
https://ddmcc-1255635056.file.myqcloud.com/cbbedbc5-0e3c-4711-aafd-7f3d68a4ed4e.png
)
此时情况跟 T6 完全一样,由于已经生成了
`Read View`
,此时依然沿用
**`m_ids` :[101,102]**
,所以查询结果依然是
`name = 菜花`
## MVCC➕Next-key-Lock防止幻读
`InnoDB`
存储引擎在 RR 级别下通过
`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
](
https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-next-key-locks
)
来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读
## 参考
-
**《MySQL技术内幕InnoDB存储引擎第2版》**
-
[
Innodb中的事务隔离级别和锁的关系
](
https://tech.meituan.com/2014/08/20/innodb-lock.html
)
-
[
MySQL事务与MVCC如何实现的隔离级别
](
https://blog.csdn.net/qq_35190492/article/details/109044141
)
\ No newline at end of file
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录