Update 数据库系统原理.md
重新绘制配图,把兼容矩阵和隔离级别对照改为表格,补充 ACID 的实现和幻读的示例,更新可串行化的描述。
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 126 KiB |
@@ -49,7 +49,7 @@
|
||||
|
||||
事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207222237925.png"/> </div><br>
|
||||
<div align="center"> <img src="pics/txn-flow.png" width="760px"/> </div><br>
|
||||
|
||||
### ACID
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
#### 2. 一致性(Consistency)
|
||||
|
||||
数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对同一个数据的读取结果都是相同的。
|
||||
数据库在事务执行前后都保持一致性状态,即满足所有的约束和业务规则。例如转账前后,两个账户的余额总和保持不变。
|
||||
|
||||
#### 3. 隔离性(Isolation)
|
||||
|
||||
@@ -82,7 +82,14 @@
|
||||
- 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
|
||||
- 事务满足持久化是为了能应对系统崩溃的情况。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207210437023.png"/> </div><br>
|
||||
在 InnoDB 中,这四个特性分别这样实现:
|
||||
|
||||
- 原子性:undo log 记录每次修改之前的值,回滚时用它恢复;
|
||||
- 持久性:修改数据页之前先写 redo log,默认配置下事务提交时把 redo log 刷到磁盘,崩溃后重放 redo log 恢复已提交的修改,见 [MySQL 的日志](MySQL.md#七日志);
|
||||
- 隔离性:写操作加锁,普通的读操作通过 MVCC 读取快照,见 [多版本并发控制](#五多版本并发控制);
|
||||
- 一致性:由以上三者和数据库的约束(主键、唯一索引、外键)共同保证,业务规则还需要应用自己维护。
|
||||
|
||||
<div align="center"> <img src="pics/acid.png" width="760px"/> </div><br>
|
||||
|
||||
### AUTOCOMMIT
|
||||
|
||||
@@ -90,31 +97,31 @@ MySQL 默认采用自动提交模式。也就是说,如果不显式使用`STAR
|
||||
|
||||
## 二、并发一致性问题
|
||||
|
||||
在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题。
|
||||
在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题。以下例子都是在 MySQL 8.0 上用两个会话实际执行得到的,表 account 中原有 id 为 1、2 的两行,余额都是 100。
|
||||
|
||||
### 丢失修改
|
||||
|
||||
丢失修改指一个事务的更新操作被另外一个事务的更新操作替换。一般在现实生活中常会遇到,例如:T<sub>1</sub> 和 T<sub>2</sub> 两个事务都对一个数据进行修改,T<sub>1</sub> 先修改并提交生效,T<sub>2</sub> 随后修改,T<sub>2</sub> 的修改覆盖了 T<sub>1</sub> 的修改。
|
||||
丢失修改指一个事务的更新操作被另外一个事务的更新操作替换。例如:T<sub>1</sub> 和 T<sub>2</sub> 先后读到余额为 100,T<sub>1</sub> 取出 30 元,写回 70 并提交;T<sub>2</sub> 存入 50 元,在它读到的 100 的基础上写回 150,T<sub>1</sub> 的修改被覆盖了。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207221744244.png"/> </div><br>
|
||||
<div align="center"> <img src="pics/txn-lost-update.png" width="780px"/> </div><br>
|
||||
|
||||
### 读脏数据
|
||||
|
||||
读脏数据指在不同的事务下,当前事务可以读到另外事务未提交的数据。例如:T<sub>1</sub> 修改一个数据但未提交,T<sub>2</sub> 随后读取这个数据。如果 T<sub>1</sub> 撤销了这次修改,那么 T<sub>2</sub> 读取的数据是脏数据。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207221920368.png"/> </div><br>
|
||||
<div align="center"> <img src="pics/txn-dirty-read.png" width="780px"/> </div><br>
|
||||
|
||||
### 不可重复读
|
||||
|
||||
不可重复读指在一个事务内多次读取同一数据集合。在这一事务还未结束前,另一事务也访问了该同一数据集合并做了修改,由于第二个事务的修改,第一次事务的两次读取的数据可能不一致。例如:T<sub>2</sub> 读取一个数据,T<sub>1</sub> 对该数据做了修改。如果 T<sub>2</sub> 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207222102010.png"/> </div><br>
|
||||
<div align="center"> <img src="pics/txn-non-repeatable.png" width="780px"/> </div><br>
|
||||
|
||||
### 幻影读
|
||||
|
||||
幻读本质上也属于不可重复读的情况,T<sub>1</sub> 读取某个范围的数据,T<sub>2</sub> 在这个范围内插入新的数据,T<sub>1</sub> 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
|
||||
幻读本质上也属于不可重复读的情况,T<sub>2</sub> 读取某个范围的数据,T<sub>1</sub> 在这个范围内插入新的数据,T<sub>2</sub> 再次读取这个范围的数据,此时读取的结果和第一次读取的结果不同。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207222134306.png"/> </div><br>
|
||||
<div align="center"> <img src="pics/txn-phantom.png" width="780px"/> </div><br>
|
||||
|
||||
----
|
||||
|
||||
@@ -147,7 +154,10 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
|
||||
|
||||
锁的兼容关系如下:
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207213523777.png"/> </div><br>
|
||||
| | X | S |
|
||||
| :---: | :---: | :---: |
|
||||
| X | 冲突 | 冲突 |
|
||||
| S | 冲突 | 兼容 |
|
||||
|
||||
#### 2. 意向锁
|
||||
|
||||
@@ -164,7 +174,12 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
|
||||
|
||||
各种锁的兼容关系如下:
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207214442687.png"/> </div><br>
|
||||
| | X | IX | S | IS |
|
||||
| :---: | :---: | :---: | :---: | :---: |
|
||||
| X | 冲突 | 冲突 | 冲突 | 冲突 |
|
||||
| IX | 冲突 | 兼容 | 冲突 | 兼容 |
|
||||
| S | 冲突 | 冲突 | 兼容 | 兼容 |
|
||||
| IS | 冲突 | 兼容 | 兼容 | 兼容 |
|
||||
|
||||
解释如下:
|
||||
|
||||
@@ -181,7 +196,7 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
|
||||
|
||||
可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么事务的修改就不会被覆盖。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207220440451.png"/> </div><br>
|
||||
<div align="center"> <img src="pics/lock-level-1.png" width="780px"/> </div><br>
|
||||
|
||||
**二级封锁协议**
|
||||
|
||||
@@ -189,7 +204,7 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
|
||||
|
||||
可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。
|
||||
|
||||
<div align="center"> <img src="pics/lock-protocol-2.png"/> </div><br>
|
||||
<div align="center"> <img src="pics/lock-level-2.png" width="780px"/> </div><br>
|
||||
|
||||
**三级封锁协议**
|
||||
|
||||
@@ -197,7 +212,7 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
|
||||
|
||||
可以解决不可重复读的问题,因为读 A 时,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207221313819.png"/> </div><br>
|
||||
<div align="center"> <img src="pics/lock-level-3.png" width="780px"/> </div><br>
|
||||
|
||||
#### 2. 两段锁协议
|
||||
|
||||
@@ -244,13 +259,22 @@ SELECT ... FOR UPDATE;
|
||||
|
||||
### 可串行化(SERIALIZABLE)
|
||||
|
||||
强制事务串行执行,这样多个事务互不干扰,不会出现并发一致性问题。
|
||||
保证并发执行的结果与某种串行执行的结果相同,不会出现并发一致性问题。
|
||||
|
||||
该隔离级别需要加锁实现,因为要使用加锁机制保证同一时间只有一个事务执行,也就是保证事务串行执行。
|
||||
InnoDB 的实现方式是在关闭自动提交时,把普通的 SELECT 都变成加共享锁的 SELECT ... FOR SHARE,读和写会互相阻塞,并发度最低。实测在可串行化的事务中读取 id 为 1 的行之后,另一个事务修改这一行会一直等待,直到锁等待超时;同样的操作在可重复读下可以立即完成。
|
||||
|
||||
----
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191207223400787.png"/> </div><br>
|
||||
各个隔离级别能否避免并发一致性问题:
|
||||
|
||||
| 隔离级别 | 脏读 | 不可重复读 | 幻影读 |
|
||||
| :---: | :---: | :---: | :---: |
|
||||
| 未提交读 | 可能 | 可能 | 可能 |
|
||||
| 提交读 | 不可能 | 可能 | 可能 |
|
||||
| 可重复读 | 不可能 | 不可能 | 可能 |
|
||||
| 可串行化 | 不可能 | 不可能 | 不可能 |
|
||||
|
||||
这是 SQL 标准的规定。MySQL 的 InnoDB 默认使用可重复读,并且用 MVCC 和 Next-Key Locks 在很大程度上避免了幻影读,见 [六、Next-Key Locks](#六next-key-locks);Oracle 和 PostgreSQL 默认使用提交读。
|
||||
|
||||
## 五、多版本并发控制
|
||||
|
||||
@@ -266,14 +290,13 @@ SELECT ... FOR UPDATE;
|
||||
|
||||
### 版本号
|
||||
|
||||
- 系统版本号 SYS_ID:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
|
||||
- 事务版本号 TRX_ID :事务开始时的系统版本号。
|
||||
InnoDB 为每个事务分配一个递增的事务 ID(TRX_ID),每个数据行都记录着最后修改它的事务 ID。需要注意的是,事务 ID 不是在 BEGIN 时分配的,而是在事务第一次修改数据时才分配;只读的事务不分配真正的事务 ID,也就不会出现在其它事务的 ReadView 中。在 MySQL 8.0 中实测:事务执行 SELECT 时 information_schema.innodb_trx 中显示的 trx_id 是一个很大的临时编号 281479674465408,执行 UPDATE 之后变为 3095。
|
||||
|
||||
### Undo 日志
|
||||
|
||||
MVCC 的多版本指的是多个版本的快照,快照存储在 Undo 日志中,该日志通过回滚指针 ROLL_PTR 把一个数据行的所有快照连接起来。
|
||||
|
||||
例如在 MySQL 创建一个表 t,包含主键 id 和一个字段 x。我们先插入一个数据行,然后对该数据行执行两次更新操作。
|
||||
例如在 MySQL 创建一个表 t,包含主键 id 和一个字段 x。先插入一个数据行,然后对该数据行执行两次更新操作。
|
||||
|
||||
```sql
|
||||
INSERT INTO t(id, x) VALUES(1, "a");
|
||||
@@ -281,9 +304,9 @@ UPDATE t SET x="b" WHERE id=1;
|
||||
UPDATE t SET x="c" WHERE id=1;
|
||||
```
|
||||
|
||||
因为没有使用 `START TRANSACTION` 将上面的操作当成一个事务来执行,根据 MySQL 的 AUTOCOMMIT 机制,每个操作都会被当成一个事务来执行,所以上面的操作总共涉及到三个事务。快照中除了记录事务版本号 TRX_ID 和操作之外,还记录了一个 bit 的 DEL 字段,用于标记是否被删除。
|
||||
因为没有使用 `START TRANSACTION` 将上面的操作当成一个事务来执行,根据 MySQL 的 AUTOCOMMIT 机制,每个操作都会被当成一个事务来执行,所以上面的操作总共涉及到三个事务,假设它们的 ID 依次为 101、102、103。快照中除了记录事务版本号 TRX_ID 和操作之外,还记录了一个 bit 的 DEL 字段,用于标记是否被删除。
|
||||
|
||||
<div align="center"> <img src="https://cs-notes-1256109796.cos.ap-guangzhou.myqcloud.com/image-20191208164808217.png"/> </div><br>
|
||||
<div align="center"> <img src="pics/mvcc-undo-chain.png" width="760px"/> </div><br>
|
||||
|
||||
INSERT、UPDATE、DELETE 操作会创建一个日志,并将事务版本号 TRX_ID 写入。DELETE 可以看成是一个特殊的 UPDATE,还会额外将 DEL 字段设置为 1。
|
||||
|
||||
@@ -291,7 +314,6 @@ INSERT、UPDATE、DELETE 操作会创建一个日志,并将事务版本号 TRX
|
||||
|
||||
MVCC 维护了一个 ReadView 结构,主要包含了创建 ReadView 时系统中未提交的事务列表 TRX_IDs {TRX_ID_1, TRX_ID_2, ...},该列表的最小值 TRX_ID_MIN,以及下一个将要分配的事务 ID TRX_ID_MAX。
|
||||
|
||||
<div align="center"> <img src="pics/readview.png"/> </div><br>
|
||||
|
||||
在进行 SELECT 操作时,根据数据行快照的 TRX_ID 与 TRX_ID_MIN 和 TRX_ID_MAX 之间的关系,从而判断数据行快照是否可以使用:
|
||||
|
||||
@@ -301,7 +323,13 @@ MVCC 维护了一个 ReadView 结构,主要包含了创建 ReadView 时系统
|
||||
|
||||
提交读和可重复读的判断规则相同,区别在于 ReadView 的创建时机:提交读在每次 SELECT 时都创建新的 ReadView,因此能读到其它事务新提交的修改;可重复读只在事务第一次 SELECT 时创建 ReadView,之后一直复用,因此多次读取的结果相同。
|
||||
|
||||
在数据行快照不可使用的情况下,需要沿着 Undo Log 的回滚指针 ROLL_PTR 找到下一个快照,再进行上面的判断。
|
||||
在数据行快照不可使用的情况下,需要沿着 Undo Log 的回滚指针 ROLL_PTR 找到下一个快照,再进行上面的判断。
|
||||
|
||||
接着上面的例子,假设事务 103 修改了 x 但还没有提交,另一个事务这时开始读取这一行:
|
||||
|
||||
<div align="center"> <img src="pics/mvcc-readview.png" width="760px"/> </div><br>
|
||||
|
||||
在 MySQL 8.0 上实际执行:事务 103 未提交时,可重复读的读事务读到 'b';103 提交之后,这个读事务再读仍然是 'b',而新开始的提交读事务读到 'c'。
|
||||
|
||||
### 快照读与当前读
|
||||
|
||||
@@ -323,11 +351,11 @@ UPDATE;
|
||||
DELETE;
|
||||
```
|
||||
|
||||
在进行 SELECT 操作时,可以强制指定进行加锁操作。以下第一个语句需要加 S 锁,第二个需要加 X 锁。
|
||||
在进行 SELECT 操作时,可以强制指定进行加锁操作。以下第一个语句需要加 S 锁,第二个需要加 X 锁。MySQL 8.0 起推荐用 FOR SHARE 代替 LOCK IN SHARE MODE。
|
||||
|
||||
```sql
|
||||
SELECT * FROM table WHERE ? lock in share mode;
|
||||
SELECT * FROM table WHERE ? for update;
|
||||
SELECT * FROM table WHERE ? FOR SHARE;
|
||||
SELECT * FROM table WHERE ? FOR UPDATE;
|
||||
```
|
||||
|
||||
## 六、Next-Key Locks
|
||||
@@ -336,6 +364,18 @@ Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。
|
||||
|
||||
在可重复读(REPEATABLE READ)隔离级别下,MVCC 使普通的 SELECT(快照读)不会读到其它事务新插入的行,但是 UPDATE、DELETE、SELECT ... FOR UPDATE 等当前读仍然可能出现幻读,Next-Key Locks 就是为了解决这个问题而存在的。InnoDB 使用 MVCC + Next-Key Locks 在可重复读隔离级别下解决了幻读问题。
|
||||
|
||||
需要注意的是,这要求一个事务中的读取全部是快照读或者全部是当前读,两者混用时仍然会出现幻读。在 MySQL 8.0 中实测,表 acct 原有 id 为 1、2 的两行:
|
||||
|
||||
```text
|
||||
A:SELECT * FROM acct WHERE balance >= 100; -- id 1、2
|
||||
B:INSERT INTO acct VALUES (3, 100); -- 自动提交
|
||||
A:SELECT * FROM acct WHERE balance >= 100; -- 仍然是 id 1、2
|
||||
A:UPDATE acct SET balance = balance + 10 WHERE balance >= 100; -- Rows matched: 3
|
||||
A:SELECT * FROM acct WHERE balance >= 100; -- id 1、2、3
|
||||
```
|
||||
|
||||
事务 A 的 UPDATE 是当前读,看到了事务 B 新插入并已提交的第 3 行,并且修改了它;这一行最新版本的事务 ID 变成了 A 自己,所以之后的快照读也能看到它。如果 A 一开始就用 SELECT ... FOR UPDATE 加锁读取,B 的插入会被 Next-Key Locks 阻塞,直到锁等待超时(ERROR 1205 Lock wait timeout exceeded)。
|
||||
|
||||
### Record Locks
|
||||
|
||||
锁定一个记录上的索引,而不是记录本身。
|
||||
@@ -362,7 +402,11 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
|
||||
(20, +∞)
|
||||
```
|
||||
|
||||
<div align="center"> <img src="pics/next-key-locks.png" width="560px"/> </div><br>
|
||||
在 MySQL 8.0 中建表 t(id, c),c 上有普通索引,插入 c 为 10、11、13、20 的四行,然后在可重复读下执行两种加锁读,从 performance_schema.data_locks 中查看实际加的锁,并在另一个事务中尝试插入:
|
||||
|
||||
<div align="center"> <img src="pics/next-key-locks-v2.png" width="760px"/> </div><br>
|
||||
|
||||
按 c = 11 查询时,c 不是唯一索引,后面还可能有 c = 11 的记录,所以除了 (10, 11] 的 Next-Key Lock,还要锁住到下一条记录 13 之间的间隙;如果 c 是唯一索引,就只锁 c = 11 这一条记录。
|
||||
|
||||
## 七、关系数据库设计理论
|
||||
|
||||
@@ -526,8 +570,11 @@ Entity-Relationship,有三个组成部分:实体、属性、联系。
|
||||
- 施瓦茨. 高性能 MYSQL(第3版)[M]. 电子工业出版社, 2013.
|
||||
- 史嘉权. 数据库系统概论[M]. 清华大学出版社有限公司, 2006.
|
||||
- [The InnoDB Storage Engine](https://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html)
|
||||
- [MySQL 8.0: Transaction Isolation Levels](https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html)
|
||||
- [MySQL 8.0: InnoDB Locking](https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html)
|
||||
- [MySQL 8.0: The data_locks Table](https://dev.mysql.com/doc/refman/8.0/en/performance-schema-data-locks-table.html)
|
||||
- [Transaction isolation levels](https://www.slideshare.net/ErnestoHernandezRodriguez/transaction-isolation-levels)
|
||||
- [Concurrency Control](http://scanftree.com/dbms/2-phase-locking-protocol)
|
||||
- [Concurrency Control](https://web.archive.org/web/20260614192237/https://scanftree.com/dbms/2-phase-locking-protocol)
|
||||
- [The Nightmare of Locking, Blocking and Isolation Levels!](https://www.slideshare.net/brshristov/the-nightmare-of-locking-blocking-and-isolation-levels-46391666)
|
||||
- [Database Normalization and Normal Forms with an Example](https://aksakalli.github.io/2012/03/12/database-normalization-and-normal-forms-with-an-example.html)
|
||||
- [The basics of the InnoDB undo logging and history system](https://blog.jcole.us/2014/04/16/the-basics-of-the-innodb-undo-logging-and-history-system/)
|
||||
|
||||