|
18 | 18 |
|
19 | 19 | ## 一致性非锁定读和锁定读
|
20 | 20 |
|
21 |
| -#### 一致性非锁定读 |
| 21 | +### 一致性非锁定读 |
22 | 22 |
|
23 | 23 | 对于 [**一致性非锁定读(Consistent Nonlocking Reads)** ](https://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html)的实现,通常做法是加一个版本号或者时间戳字段,在更新数据的同时版本号 + 1 或者更新时间戳。查询时,将当前可见的版本号与对应记录的版本号进行比对,如果记录的版本小于可见版本,则表示该记录可见
|
24 | 24 |
|
25 | 25 | 在 `InnoDB` 存储引擎中,[多版本控制 (multi versioning)](https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html) 就是对非锁定读的实现。如果读取的行正在执行 `DELETE` 或 `UPDATE` 操作,这时读取操作不会去等待行上锁的释放。相反地,`InnoDB` 存储引擎会去读取行的一个快照数据,对于这种读取历史数据的方式,我们叫它快照读 (snapshot read)
|
26 | 26 |
|
27 | 27 | 在 `Repeatable Read` 和 `Read Committed` 两个隔离级别下,如果是执行普通的 `select` 语句(不包括 `select ... lock in share mode` ,`select ... for update`)则会使用 `一致性非锁定读(MVCC)`。并且在 `Repeatable Read` 下 `MVCC` 实现了可重复读和防止部分幻读
|
28 | 28 |
|
29 |
| -#### 锁定读 |
| 29 | +### 锁定读 |
30 | 30 |
|
31 | 31 | 如果执行的是下列语句,就是 [**锁定读(Locking Reads)**](https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html)
|
32 | 32 |
|
33 |
| -- select ... lock in share mode |
34 |
| -- select ... for update |
35 |
| -- insert、update、delete 操作 |
| 33 | +- `select ... lock in share mode` |
| 34 | +- `select ... for update` |
| 35 | +- `insert`、`update`、`delete` 操作 |
36 | 36 |
|
37 | 37 | 在锁定读下,读取的是数据的最新版本,这种读也被称为 `当前读(current read)`。锁定读会对读取到的记录加锁:
|
38 | 38 |
|
39 | 39 | - `select ... lock in share mode`:对记录加 `S` 锁,其它事务也可以加`S`锁,如果加 `x` 锁则会被阻塞
|
40 | 40 |
|
41 | 41 | - `select ... for update`、`insert`、`update`、`delete`:对记录加 `X` 锁,且其它事务不能加任何锁
|
42 | 42 |
|
43 |
| -在一致性非锁定读下,即使读取的记录已被其它事务加上 `X` 锁,这时记录也是可以被读取的,即读取的快照数据。上面说了在 `Repeatable Read` 下 `MVCC` 防止了部分幻读,这边的 “部分” 是指在 `一致性非锁定读` 情况下,只能读取到第一次查询之前所插入的数据(根据 Read View 判断数据可见性,Read View 在第一次查询时生成),但如果是`当前读` ,每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。**所以 `InnoDB` 在实现`Repeatable Read` 时,如果执行的是当前读,则会对读取的记录使用 `Next-key Lock` ,来防止其它事务在间隙间插入数据** |
| 43 | +在一致性非锁定读下,即使读取的记录已被其它事务加上 `X` 锁,这时记录也是可以被读取的,即读取的快照数据。上面说了,在 `Repeatable Read` 下 `MVCC` 防止了部分幻读,这边的 “部分” 是指在 `一致性非锁定读` 情况下,只能读取到第一次查询之前所插入的数据(根据 Read View 判断数据可见性,Read View 在第一次查询时生成)。但是!如果是 `当前读` ,每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。所以, **`InnoDB` 在实现`Repeatable Read` 时,如果执行的是当前读,则会对读取的记录使用 `Next-key Lock` ,来防止其它事务在间隙间插入数据** |
44 | 44 |
|
45 | 45 | ## InnoDB 对 MVCC 的实现
|
46 | 46 |
|
47 | 47 | `MVCC` 的实现依赖于:**隐藏字段、Read View、undo log**。在内部实现中,`InnoDB` 通过数据行的 `DB_TRX_ID` 和 `Read View` 来判断数据的可见性,如不可见,则通过数据行的 `DB_ROLL_PTR` 找到 `undo log` 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 `Read View` 之前已经提交的修改和该事务本身做的修改
|
48 | 48 |
|
49 |
| -#### 隐藏字段 |
| 49 | +### 隐藏字段 |
50 | 50 |
|
51 | 51 | 在内部,`InnoDB` 存储引擎为每行数据添加了三个 [隐藏字段](https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html):
|
52 | 52 |
|
53 | 53 | - `DB_TRX_ID(6字节)`:表示最后一次插入或更新该行的事务 id。此外,`delete` 操作在内部被视为更新,只不过会在记录头 `Record header` 中的 `deleted_flag` 字段将其标记为已删除
|
54 | 54 | - `DB_ROLL_PTR(7字节)` 回滚指针,指向该行的 `undo log` 。如果该行未被更新,则为空
|
55 | 55 | - `DB_ROW_ID(6字节)`:如果没有设置主键且该表没有唯一非空索引时,`InnoDB` 会使用该 id 来生成聚簇索引
|
56 | 56 |
|
57 |
| -#### ReadView |
| 57 | +### ReadView |
58 | 58 |
|
59 | 59 | [`Read View`](https://github.com/facebook/mysql-8.0/blob/8.0/storage/innobase/include/read0types.h#L298) 主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务”
|
60 | 60 |
|
|
65 | 65 | - `m_ids`:`Read View` 创建时其他未提交的活跃事务 ID 列表。创建 `Read View`时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。`m_ids` 不包括当前事务自己和已提交的事务(正在内存中)
|
66 | 66 | - `m_creator_trx_id`:创建该 `Read View` 的事务 ID
|
67 | 67 |
|
68 |
| -#### undo-log |
| 68 | +### undo-log |
69 | 69 |
|
70 | 70 | `undo log` 主要有两个作用:
|
71 | 71 |
|
|
78 | 78 |
|
79 | 79 | **`insert` 时的数据初始状态:**
|
80 | 80 |
|
81 |
| - |
| 81 | + |
82 | 82 |
|
83 | 83 | 2. **`update undo log`** :`update` 或 `delete` 操作中产生的 `undo log`。该 `undo log`可能需要提供 `MVCC` 机制,因此不能在事务提交时就进行删除。提交时放入 `undo log` 链表,等待 `purge线程` 进行最后的删除
|
84 | 84 |
|
85 | 85 | **数据第一次被修改时:**
|
86 | 86 |
|
87 |
| - |
| 87 | + |
88 | 88 |
|
89 | 89 | **数据第二次被修改时:**
|
90 | 90 |
|
91 |
| - |
| 91 | + |
92 | 92 |
|
93 | 93 | 不同事务或者相同事务的对同一记录行的修改,会使该记录行的 `undo log` 成为一条链表,链首就是最新的记录,链尾就是最早的旧记录
|
94 | 94 |
|
95 |
| -#### 数据可见性算法 |
| 95 | +### 数据可见性算法 |
96 | 96 |
|
97 | 97 | 在 `InnoDB` 存储引擎中,创建一个新事务后,执行每个 `select` 语句前,都会创建一个快照(Read View),**快照中保存了当前数据库系统中正处于活跃(没有 commit)的事务的 ID 号**。其实简单的说保存的是系统中当前不应该被本事务看到的其他事务 ID 列表(即 m_ids)。当用户在这个事务中要读取某个记录行的时候,`InnoDB` 会将该记录行的 `DB_TRX_ID` 与 `Read View` 中的一些变量及当前事务 ID 进行比较,判断是否满足可见性条件
|
98 | 98 |
|
|
127 | 127 |
|
128 | 128 | 举个例子:
|
129 | 129 |
|
130 |
| - |
| 130 | + |
131 | 131 |
|
132 |
| -#### **在 RC 下 ReadView 生成情况** |
| 132 | +### 在 RC 下 ReadView 生成情况 |
133 | 133 |
|
134 | 134 | 1. **`假设时间线来到 T4 ,那么此时数据行 id = 1 的版本链为`:**
|
135 | 135 |
|
136 |
| -  |
| 136 | +  |
137 | 137 |
|
138 | 138 | 由于 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
|
139 | 139 |
|
|
159 | 159 |
|
160 | 160 | > **总结:** **在 RC 隔离级别下,事务在每次查询开始时都会生成并设置新的 Read View,所以导致不可重复读**
|
161 | 161 |
|
162 |
| -#### **在 RR 下 ReadView 生成情况** |
| 162 | +### 在 RR 下 ReadView 生成情况 |
163 | 163 |
|
164 | 164 | **在可重复读级别下,只会在事务开始后第一次读取数据时生成一个 Read View(m_ids 列表)**
|
165 | 165 |
|
|
199 | 199 |
|
200 | 200 | `InnoDB`存储引擎在 RR 级别下通过 `MVCC`和 `Next-key Lock` 来解决幻读问题:
|
201 | 201 |
|
202 |
| -1. **执行普通 `select`,此时会以 `MVCC` 快照读的方式读取数据** |
| 202 | +**1、执行普通 `select`,此时会以 `MVCC` 快照读的方式读取数据** |
203 | 203 |
|
204 | 204 | 在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 `Read View` ,并使用至事务提交。所以在生成 `Read View` 之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”
|
205 | 205 |
|
206 |
| -2. **执行 select...for update/lock in share mode、insert、update、delete 等当前读** |
| 206 | +**2、执行 select...for update/lock in share mode、insert、update、delete 等当前读** |
207 | 207 |
|
208 | 208 | 在当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!`InnoDB` 使用 [Next-key Lock](https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-next-key-locks) 来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读
|
209 | 209 |
|
|
0 commit comments