diff --git a/notes/pics/acid.png b/notes/pics/acid.png new file mode 100644 index 00000000..0cfcef06 Binary files /dev/null and b/notes/pics/acid.png differ diff --git a/notes/pics/lock-level-1.png b/notes/pics/lock-level-1.png new file mode 100644 index 00000000..3001ced5 Binary files /dev/null and b/notes/pics/lock-level-1.png differ diff --git a/notes/pics/lock-level-2.png b/notes/pics/lock-level-2.png new file mode 100644 index 00000000..396a33d3 Binary files /dev/null and b/notes/pics/lock-level-2.png differ diff --git a/notes/pics/lock-level-3.png b/notes/pics/lock-level-3.png new file mode 100644 index 00000000..3ade520b Binary files /dev/null and b/notes/pics/lock-level-3.png differ diff --git a/notes/pics/mvcc-readview.png b/notes/pics/mvcc-readview.png new file mode 100644 index 00000000..d6037bae Binary files /dev/null and b/notes/pics/mvcc-readview.png differ diff --git a/notes/pics/mvcc-undo-chain.png b/notes/pics/mvcc-undo-chain.png new file mode 100644 index 00000000..1a3fb476 Binary files /dev/null and b/notes/pics/mvcc-undo-chain.png differ diff --git a/notes/pics/next-key-locks-v2.png b/notes/pics/next-key-locks-v2.png new file mode 100644 index 00000000..9044a2ce Binary files /dev/null and b/notes/pics/next-key-locks-v2.png differ diff --git a/notes/pics/txn-dirty-read.png b/notes/pics/txn-dirty-read.png new file mode 100644 index 00000000..b354456d Binary files /dev/null and b/notes/pics/txn-dirty-read.png differ diff --git a/notes/pics/txn-flow.png b/notes/pics/txn-flow.png new file mode 100644 index 00000000..0a1a474a Binary files /dev/null and b/notes/pics/txn-flow.png differ diff --git a/notes/pics/txn-lost-update.png b/notes/pics/txn-lost-update.png new file mode 100644 index 00000000..98d99d20 Binary files /dev/null and b/notes/pics/txn-lost-update.png differ diff --git a/notes/pics/txn-non-repeatable.png b/notes/pics/txn-non-repeatable.png new file mode 100644 index 00000000..268a77d1 Binary files /dev/null and b/notes/pics/txn-non-repeatable.png differ diff --git a/notes/pics/txn-phantom.png b/notes/pics/txn-phantom.png new file mode 100644 index 00000000..c0b52892 Binary files /dev/null and b/notes/pics/txn-phantom.png differ diff --git a/notes/数据库系统原理.md b/notes/数据库系统原理.md index 59006f30..8192a199 100644 --- a/notes/数据库系统原理.md +++ b/notes/数据库系统原理.md @@ -49,7 +49,7 @@ 事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。 -

+

### ACID @@ -61,7 +61,7 @@ #### 2. 一致性(Consistency) -数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对同一个数据的读取结果都是相同的。 +数据库在事务执行前后都保持一致性状态,即满足所有的约束和业务规则。例如转账前后,两个账户的余额总和保持不变。 #### 3. 隔离性(Isolation) @@ -82,7 +82,14 @@ - 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。 - 事务满足持久化是为了能应对系统崩溃的情况。 -

+在 InnoDB 中,这四个特性分别这样实现: + +- 原子性:undo log 记录每次修改之前的值,回滚时用它恢复; +- 持久性:修改数据页之前先写 redo log,默认配置下事务提交时把 redo log 刷到磁盘,崩溃后重放 redo log 恢复已提交的修改,见 [MySQL 的日志](MySQL.md#七日志); +- 隔离性:写操作加锁,普通的读操作通过 MVCC 读取快照,见 [多版本并发控制](#五多版本并发控制); +- 一致性:由以上三者和数据库的约束(主键、唯一索引、外键)共同保证,业务规则还需要应用自己维护。 + +

### AUTOCOMMIT @@ -90,31 +97,31 @@ MySQL 默认采用自动提交模式。也就是说,如果不显式使用`STAR ## 二、并发一致性问题 -在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题。 +在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题。以下例子都是在 MySQL 8.0 上用两个会话实际执行得到的,表 account 中原有 id 为 1、2 的两行,余额都是 100。 ### 丢失修改 -丢失修改指一个事务的更新操作被另外一个事务的更新操作替换。一般在现实生活中常会遇到,例如:T1 和 T2 两个事务都对一个数据进行修改,T1 先修改并提交生效,T2 随后修改,T2 的修改覆盖了 T1 的修改。 +丢失修改指一个事务的更新操作被另外一个事务的更新操作替换。例如:T1 和 T2 先后读到余额为 100,T1 取出 30 元,写回 70 并提交;T2 存入 50 元,在它读到的 100 的基础上写回 150,T1 的修改被覆盖了。 -

+

### 读脏数据 读脏数据指在不同的事务下,当前事务可以读到另外事务未提交的数据。例如:T1 修改一个数据但未提交,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。 -

+

### 不可重复读 不可重复读指在一个事务内多次读取同一数据集合。在这一事务还未结束前,另一事务也访问了该同一数据集合并做了修改,由于第二个事务的修改,第一次事务的两次读取的数据可能不一致。例如:T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。 -

+

### 幻影读 -幻读本质上也属于不可重复读的情况,T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。 +幻读本质上也属于不可重复读的情况,T2 读取某个范围的数据,T1 在这个范围内插入新的数据,T2 再次读取这个范围的数据,此时读取的结果和第一次读取的结果不同。 -

+

---- @@ -147,7 +154,10 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 锁的兼容关系如下: -

+| | X | S | +| :---: | :---: | :---: | +| X | 冲突 | 冲突 | +| S | 冲突 | 兼容 | #### 2. 意向锁 @@ -164,7 +174,12 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 各种锁的兼容关系如下: -

+| | X | IX | S | IS | +| :---: | :---: | :---: | :---: | :---: | +| X | 冲突 | 冲突 | 冲突 | 冲突 | +| IX | 冲突 | 兼容 | 冲突 | 兼容 | +| S | 冲突 | 冲突 | 兼容 | 兼容 | +| IS | 冲突 | 兼容 | 兼容 | 兼容 | 解释如下: @@ -181,7 +196,7 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么事务的修改就不会被覆盖。 -

+

**二级封锁协议** @@ -189,7 +204,7 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。 -

+

**三级封锁协议** @@ -197,7 +212,7 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 可以解决不可重复读的问题,因为读 A 时,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。 -

+

#### 2. 两段锁协议 @@ -244,13 +259,22 @@ SELECT ... FOR UPDATE; ### 可串行化(SERIALIZABLE) -强制事务串行执行,这样多个事务互不干扰,不会出现并发一致性问题。 +保证并发执行的结果与某种串行执行的结果相同,不会出现并发一致性问题。 -该隔离级别需要加锁实现,因为要使用加锁机制保证同一时间只有一个事务执行,也就是保证事务串行执行。 +InnoDB 的实现方式是在关闭自动提交时,把普通的 SELECT 都变成加共享锁的 SELECT ... FOR SHARE,读和写会互相阻塞,并发度最低。实测在可串行化的事务中读取 id 为 1 的行之后,另一个事务修改这一行会一直等待,直到锁等待超时;同样的操作在可重复读下可以立即完成。 ---- -

+各个隔离级别能否避免并发一致性问题: + +| 隔离级别 | 脏读 | 不可重复读 | 幻影读 | +| :---: | :---: | :---: | :---: | +| 未提交读 | 可能 | 可能 | 可能 | +| 提交读 | 不可能 | 可能 | 可能 | +| 可重复读 | 不可能 | 不可能 | 可能 | +| 可串行化 | 不可能 | 不可能 | 不可能 | + +这是 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 字段,用于标记是否被删除。 -

+

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。 -

在进行 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 但还没有提交,另一个事务这时开始读取这一行: + +

+ +在 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, +∞) ``` -

+在 MySQL 8.0 中建表 t(id, c),c 上有普通索引,插入 c 为 10、11、13、20 的四行,然后在可重复读下执行两种加锁读,从 performance_schema.data_locks 中查看实际加的锁,并在另一个事务中尝试插入: + +

+ +按 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/) diff --git a/tools/diagram/src/acid.svg b/tools/diagram/src/acid.svg new file mode 100644 index 00000000..e4227499 --- /dev/null +++ b/tools/diagram/src/acid.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + ACID:原子性、隔离性、持久性是手段,一致性是目的 + + 原子性 + undo log 回滚 + + 隔离性 + 锁和 MVCC + + 持久性 + redo log 刷盘 + + 一致性 + 满足约束和业务规则 + + + 没有并发时 + 有并发时还需要 + + 事务的结果正确 + + + 系统崩溃后 + 已提交的修改不丢失 + + 例如转账:原子性保证不会只扣款不入账,隔离性保证并发的转账互不干扰,持久性保证提交之后断电也不丢。 + diff --git a/tools/diagram/src/lock-level-1.svg b/tools/diagram/src/lock-level-1.svg new file mode 100644 index 00000000..c174f861 --- /dev/null +++ b/tools/diagram/src/lock-level-1.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + 一级封锁协议:修改前加 X 锁,事务结束才释放 + + 事务 T1 + + + 事务 T2 + + 时间 + + + lock-x(A) + + read A + → 100 + + lock-x(A),等待 T1 释放 + + write A = 70 + + commit,unlock(A) + + 获得 X 锁,read A + → 70 + + write A = 120 + + commit,unlock(A) + 解决丢失修改:T2 只能在 T1 提交之后读取并修改,结果为 120。 + diff --git a/tools/diagram/src/lock-level-2.svg b/tools/diagram/src/lock-level-2.svg new file mode 100644 index 00000000..478ff3ed --- /dev/null +++ b/tools/diagram/src/lock-level-2.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + 二级封锁协议:读取前加 S 锁,读完立即释放 + + 事务 T1 + + + 事务 T2 + + 时间 + + + lock-x(A) + + write A = 200 + + lock-s(A),等待 T1 释放 + + rollback,unlock(A) + + 获得 S 锁,read A + → 100 + + unlock(A) + 解决读脏数据:T1 持有 X 锁期间,T2 加不上 S 锁,读不到 T1 未提交的 200。 + diff --git a/tools/diagram/src/lock-level-3.svg b/tools/diagram/src/lock-level-3.svg new file mode 100644 index 00000000..60c86a6c --- /dev/null +++ b/tools/diagram/src/lock-level-3.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + 三级封锁协议:读取前加 S 锁,事务结束才释放 + + 事务 T1 + + + 事务 T2 + + 时间 + + + lock-s(A) + + read A + → 100 + + lock-x(A),等待 T2 释放 + + read A + → 100 + + commit,unlock(A) + + 获得 X 锁,write A = 200 + + commit,unlock(A) + 解决不可重复读:T2 结束之前,T1 无法修改 A,T2 两次读到的都是 100。 + diff --git a/tools/diagram/src/mvcc-readview.svg b/tools/diagram/src/mvcc-readview.svg new file mode 100644 index 00000000..316238c0 --- /dev/null +++ b/tools/diagram/src/mvcc-readview.svg @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + ReadView:沿版本链找到第一个对自己可见的版本 + 读事务开始读取时,事务 103、105 还没有提交,下一个要分配的事务 ID 是 106: + TRX_IDs = {103, 105},TRX_ID_MIN = 103,TRX_ID_MAX = 106 + + 事务 ID + + 101 + 可见 + 已提交 + + 102 + 可见 + 已提交 + + 103 + 不可见 + 在 TRX_IDs 中 + + 104 + 可见 + 已提交 + + 105 + 不可见 + 在 TRX_IDs 中 + + 106 + 不可见 + 之后才开始 + TRX_ID_MIN + TRX_ID_MAX + 读取 id = 1 这一行: + + 版本 x = 'c' + DB_TRX_ID = 103,在 TRX_IDs 中 + 不可见,沿回滚指针找上一个版本 + + + 版本 x = 'b' + DB_TRX_ID = 102,小于 TRX_ID_MIN + 可见,读到 'b' + 可重复读在第一次读取时创建 ReadView,之后一直使用它;提交读每次读取都创建新的 ReadView,103 提交后就能读到 'c'。 + 事务 ID 为示意;读到的结果在 MySQL 8.0 上实测一致。 + diff --git a/tools/diagram/src/mvcc-undo-chain.svg b/tools/diagram/src/mvcc-undo-chain.svg new file mode 100644 index 00000000..5a9c4c47 --- /dev/null +++ b/tools/diagram/src/mvcc-undo-chain.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + Undo 版本链:每次修改都把旧版本写入 undo log,用回滚指针连起来 + 当前的数据行 + + id + + x + + DB_TRX_ID + + DB_ROLL_PTR + + 1 + + c + + 103 + + → 版本 2 + UPDATE t SET x = 'c'(事务 103) + + undo log 中的版本 2 + + id + + x + + DB_TRX_ID + + DB_ROLL_PTR + + 1 + + b + + 102 + + → 版本 1 + UPDATE t SET x = 'b'(事务 102) + + undo log 中的版本 1 + + id + + x + + DB_TRX_ID + + DB_ROLL_PTR + + 1 + + a + + 101 + + 无更早版本 + INSERT INTO t VALUES (1, 'a')(事务 101) + DB_TRX_ID 是最后修改这一行的事务 ID,DB_ROLL_PTR 指向上一个版本;事务 ID 为示意。 + diff --git a/tools/diagram/src/next-key-locks-v2.svg b/tools/diagram/src/next-key-locks-v2.svg new file mode 100644 index 00000000..68de180a --- /dev/null +++ b/tools/diagram/src/next-key-locks-v2.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + Next-Key Locks:锁住记录和它前面的间隙 + ① SELECT * FROM t WHERE c = 11 FOR UPDATE + + (10, 11] + + (11, 13) + + -∞ + + c=10 + + c=11 + + c=13 + + c=20 + +∞ + data_locks:c = 11 的记录上是 X(Next-Key Lock),c = 13 上是 X,GAP;插入 c = 12 被阻塞,c = 9、14、21 可以插入 + ② SELECT * FROM t WHERE c BETWEEN 10 AND 20 FOR UPDATE + + (-∞, 10] + + (10, 11] + + (11, 13] + + (13, 20] + + (20, +∞] + + -∞ + + c=10 + + c=11 + + c=13 + + c=20 + +∞ + data_locks:c = 10、11、13、20 和 supremum 上都是 X;插入 c = 9、12、14、21 都被阻塞 + + Next-Key Lock(前开后闭) + + Gap Lock(开区间) + MySQL 8.0,c 上有普通索引,可重复读 + diff --git a/tools/diagram/src/txn-dirty-read.svg b/tools/diagram/src/txn-dirty-read.svg new file mode 100644 index 00000000..48e30065 --- /dev/null +++ b/tools/diagram/src/txn-dirty-read.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + 读脏数据:读到了另一个事务没有提交的修改 + + 事务 T1 + + + 事务 T2 + READ UNCOMMITTED + + 时间 + + + BEGIN + + UPDATE account SET balance = 200 WHERE id = 1 + + SELECT balance FROM account WHERE id = 1 + → 200 + + ROLLBACK + + SELECT balance FROM account WHERE id = 1 + → 100 + T2 读到的 200 从来没有提交过。 + T2 的隔离级别为 READ COMMITTED 及以上时,第一次读到的是 100。 + diff --git a/tools/diagram/src/txn-flow.svg b/tools/diagram/src/txn-flow.svg new file mode 100644 index 00000000..7971e308 --- /dev/null +++ b/tools/diagram/src/txn-flow.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + 事务:要么全部提交,要么全部回滚 + 从 A 账户向 B 账户转账 30 元 + + 一致的状态 + A = 100 + B = 0 + + 中间状态 + A = 70 + B = 0 + + 中间状态 + A = 70 + B = 30 + + 一致的状态 + A = 70 + B = 30 + + A − 30 + + B + 30 + + COMMIT + + + 出错或执行 ROLLBACK:用 undo log 撤销已做的修改,回到初始状态 + 在提交读及以上的隔离级别下,中间状态只有当前事务看得到;提交后修改才对其它事务可见,并由 redo log 保证不会丢失。 + A、B 两个账户的余额总和在事务前后都是 100,这就是一致性。 + diff --git a/tools/diagram/src/txn-lost-update.svg b/tools/diagram/src/txn-lost-update.svg new file mode 100644 index 00000000..4d0ef578 --- /dev/null +++ b/tools/diagram/src/txn-lost-update.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + 丢失修改:两个事务基于同一个旧值计算后写回 + + 事务 T1:取出 30 元 + REPEATABLE READ + + + 事务 T2:存入 50 元 + REPEATABLE READ + + 时间 + + + SELECT balance FROM account WHERE id = 1 + → 100 + + SELECT balance FROM account WHERE id = 1 + → 100 + + UPDATE account SET balance = 70 WHERE id = 1 + + COMMIT + + UPDATE account SET balance = 150 WHERE id = 1 + + COMMIT + + SELECT balance FROM account WHERE id = 1 + → 150 + 正确结果应为 120,T1 取出的 30 元丢失了。 + 改为 SET balance = balance - 30 后,T2 的 UPDATE 会等待 T1 的行锁,再在 70 的基础上加 50,结果为 120。 + diff --git a/tools/diagram/src/txn-non-repeatable.svg b/tools/diagram/src/txn-non-repeatable.svg new file mode 100644 index 00000000..d309655c --- /dev/null +++ b/tools/diagram/src/txn-non-repeatable.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + 不可重复读:同一个事务两次读取同一行,结果不同 + + 事务 T1 + 自动提交 + + + 事务 T2 + READ COMMITTED + + 时间 + + + BEGIN + + SELECT balance FROM account WHERE id = 1 + → 100 + + UPDATE account SET balance = 200 WHERE id = 1 + + SELECT balance FROM account WHERE id = 1 + → 200 + + COMMIT + T1 在 T2 的两次读取之间修改并提交了这一行。 + T2 的隔离级别为 REPEATABLE READ 时,第二次读到的仍然是 100。 + diff --git a/tools/diagram/src/txn-phantom.svg b/tools/diagram/src/txn-phantom.svg new file mode 100644 index 00000000..4f783d82 --- /dev/null +++ b/tools/diagram/src/txn-phantom.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + 幻影读:同一个事务两次范围查询,行数不同 + + 事务 T1 + 自动提交 + + + 事务 T2 + READ COMMITTED + + 时间 + + + BEGIN + + SELECT id FROM account WHERE balance > 50 + → 1,2 + + INSERT INTO account VALUES (3, 100) + + SELECT id FROM account WHERE balance > 50 + → 1,2,3 + + COMMIT + T1 在 T2 查询的范围内插入了新的一行。 + T2 的隔离级别为 REPEATABLE READ 时,第二次查询仍然只有 1、2。 +