diff --git a/docs/notes/数据库系统原理.md b/docs/notes/数据库系统原理.md index a38ae975..e97faa90 100644 --- a/docs/notes/数据库系统原理.md +++ b/docs/notes/数据库系统原理.md @@ -20,10 +20,9 @@ * [可串行化(SERIALIZABLE)](#可串行化serializable) * [五、多版本并发控制](#五多版本并发控制) * [基本思想](#基本思想) - * [Undo 日志](#undo-日志) * [版本号](#版本号) - * [隐藏的列](#隐藏的列) - * [实现过程](#实现过程) + * [Undo 日志](#undo-日志) + * [ReadView](#readview) * [快照读与当前读](#快照读与当前读) * [六、Next-Key Locks](#六next-key-locks) * [Record Locks](#record-locks) @@ -257,19 +256,22 @@ SELECT ... FOR UPDATE; ## 基本思想 -在封锁一节中提到,加锁能解决多个事务同时执行时出现的并发一致性问题。但是加锁操作代价很高,并且在实际场景中读多写少,所有事务都是只是进行读操作的话就没必要进行加锁。 +在封锁一节中提到,加锁能解决多个事务同时执行时出现的并发一致性问题。在实际场景中读操作往往多于写操作,因此又引入了读写锁来避免不必要的加锁操作,例如读和读没有互斥关系。读写锁中读和写操作仍然是互斥的,而 MVCC 利用了多版本的思想,写操作更新最新的版本快照,而读操作去读旧版本快照,没有互斥关系,这一点和 CopyOnWrite 类似。 -MVCC 的读操作不需要进行加锁,并且在可重复读隔离级别下能解决脏读和不可重复读问题。它的基本思想是为每个数据行维护多个版本的快照,多个事务可以同时去操作这个数据行。 +在 MVCC 中事务的修改操作(DELETE、INSERT、UPDATE)会为数据行新增一个版本快照。 -事务的修改操作(DELETE、INSERT、UPDATE)会去修改该事务对应版本的快照。 +脏读和不可重复读最根本的原因是事务读取到其它事务未提交的修改。在事务进行读取操作时,为了解决脏读和不可重复读问题,MVCC 规定只能读取已经提交的快照。当然一个事务可以读取自身未提交的快照,这不算是脏读。 -脏读和不可重复读最根本的原因是事务读取到其它事务未提交的修改。在事务在进行读取操作时,为了解决脏读和不可重复读问题,读取的快照需要满足以下条件:快照在该事务开始之后没有被其它事务修改,否则会读取到其它事务的未提交的修改操作。 +## 版本号 + +- 系统版本号 SYS_ID:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。 +- 事务版本号 TRX_ID :事务开始时的系统版本号。 ## Undo 日志 -MVCC 的多版本指的是多个版本的快照,这个快照存储在 Undo 日志中,该日志通过回滚指针把一个数据行的所有快照连接起来。 +MVCC 的多版本指的是多个版本的快照,快照存储在 Undo 日志中,该日志通过回滚指针 ROLL_PTR 把一个数据行的所有快照连接起来。 -例如我们在 MySQL 创建一个表 t,包含主键 id 和一个字段 x。我们先插入一个数据行,然后对该数据行执行两次操作。 +例如在 MySQL 创建一个表 t,包含主键 id 和一个字段 x。我们先插入一个数据行,然后对该数据行执行两次更新操作。 ```sql INSERT INTO t(id, x) VALUES(1, "a"); @@ -277,45 +279,28 @@ UPDATE t SET x="b" WHERE id=1; UPDATE t SET x="c" WHERE id=1; ``` -因为我们没有使用 `START TRANSACTION` 将上面的操作当成一个事务来执行,根据 MySQL 的 AUTOCOMMIT 机制,每个操作都会被当成一个事务来执行,所以上面总共涉及到三个事务。 +因为没有使用 `START TRANSACTION` 将上面的操作当成一个事务来执行,根据 MySQL 的 AUTOCOMMIT 机制,每个操作都会被当成一个事务来执行,所以上面的操作总共涉及到三个事务。快照中除了记录事务版本号 TRX_ID 和操作之外,还记录了一个 bit 的 DEL 字段,用于标记是否被删除。 -

+

-## 版本号 +INSERT、UPDATE、DELETE 操作会创建一个日志,并将事务版本号 TRX_ID 写入。DELETE 可以看成是一个特殊的 UPDATE,还会额外将 DEL 字段设置为 1。 -- 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。 -- 事务版本号:事务开始时的系统版本号。 +## ReadView -## 隐藏的列 +MVCC 维护了一个 ReadView 结构,主要包含了当前系统未提交的事务列表 TRX_IDs {TRX_ID_1, TRX_ID_2, ...},还有该列表的最小值 TRX_ID_MIN 和 TRX_ID_MAX。 -MVCC 在每个数据行后面都保存着两个隐藏的列,用来存储两个版本号: +

-- 创建版本号:指示创建一个数据行的快照时的系统版本号; -- 删除版本号:如果该快照的删除版本号未定义或删除版本号大于当前事务版本号表示该快照有效。 +在进行 SELECT 操作时,根据数据行快照的 TRX_ID 与 TRX_ID_MIN 和 TRX_ID_MAX 之间的关系,从而判断数据行快照是否可以使用: -## 实现过程 +- TRX_ID < TRX_ID_MIN,表示该数据行快照时在当前所有未提交事务之前进行更改的,因此可以使用。 -以下实现过程针对可重复读隔离级别。 +- TRX_ID > TRX_ID_MAX,表示该数据行快照是在事务启动之后被更改的,因此不可使用。 +- TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根据隔离级别再进行判断: + - 提交读:如果 TRX_ID 在 TRX_IDs 列表中,表示该数据行快照对应的事务还未提交,则该快照不可使用。否则表示已经提交,可以使用。 + - 可重复读:都不可以使用。因为如果可以使用的话,那么其它事务也可以读到这个数据行快照并进行修改,那么当前事务再去读这个数据行得到的值就会发生改变,也就是出现了不可重复读问题。 -因为数据行快照的创建版本号是创建数据行快照时的系统版本号,系统版本号随着创建事务而递增,所以新开始一个事务时,这个事务的系统版本号比之前的系统版本号都大,也就是比当前所有数据行快照的创建版本号都大。 - -### 1. SELECT - -多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。 - -把没有对一个数据行做修改的事务称为 T,T 所要读取的数据行快照的创建版本号必须小于等于 T 的版本号,因为如果大于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。除此之外,T 所要读取的数据行快照的删除版本号必须是未定义或者大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。 - -### 2. INSERT - -将当前系统版本号作为数据行快照的创建版本号。 - -### 3. DELETE - -将当前系统版本号作为数据行快照的删除版本号。 - -### 4. UPDATE - -将当前系统版本号作为更新前的数据行快照的删除版本号,并将当前系统版本号作为更新后的数据行快照的创建版本号。可以理解为先执行 DELETE 后执行 INSERT。 +在数据行快照不可使用的情况下,需要沿着 Undo Log 的回滚指针 ROLL_PTR 找到下一个快照,再进行上面的判断。 ## 快照读与当前读 @@ -329,7 +314,7 @@ SELECT * FROM table ...; ### 2. 当前读 -MVCC 其它会对数据库进行修改的操作(INSERT、UPDATE、DELETE)需要进行加锁操作,从而读取最新的数据。 +MVCC 其它会对数据库进行修改的操作(INSERT、UPDATE、DELETE)需要进行加锁操作,从而读取最新的数据。可以看到 MVCC 并不是完全不用加锁,而只是避免了 SELECT 的加锁操作。 ```sql INSERT; diff --git a/notes/pics/image-20191208164808217.png b/notes/pics/image-20191208164808217.png new file mode 100644 index 00000000..36d86012 Binary files /dev/null and b/notes/pics/image-20191208164808217.png differ diff --git a/notes/pics/image-20191208171445674.png b/notes/pics/image-20191208171445674.png new file mode 100644 index 00000000..e1ad39a8 Binary files /dev/null and b/notes/pics/image-20191208171445674.png differ diff --git a/notes/数据库系统原理.md b/notes/数据库系统原理.md index a38ae975..e97faa90 100644 --- a/notes/数据库系统原理.md +++ b/notes/数据库系统原理.md @@ -20,10 +20,9 @@ * [可串行化(SERIALIZABLE)](#可串行化serializable) * [五、多版本并发控制](#五多版本并发控制) * [基本思想](#基本思想) - * [Undo 日志](#undo-日志) * [版本号](#版本号) - * [隐藏的列](#隐藏的列) - * [实现过程](#实现过程) + * [Undo 日志](#undo-日志) + * [ReadView](#readview) * [快照读与当前读](#快照读与当前读) * [六、Next-Key Locks](#六next-key-locks) * [Record Locks](#record-locks) @@ -257,19 +256,22 @@ SELECT ... FOR UPDATE; ## 基本思想 -在封锁一节中提到,加锁能解决多个事务同时执行时出现的并发一致性问题。但是加锁操作代价很高,并且在实际场景中读多写少,所有事务都是只是进行读操作的话就没必要进行加锁。 +在封锁一节中提到,加锁能解决多个事务同时执行时出现的并发一致性问题。在实际场景中读操作往往多于写操作,因此又引入了读写锁来避免不必要的加锁操作,例如读和读没有互斥关系。读写锁中读和写操作仍然是互斥的,而 MVCC 利用了多版本的思想,写操作更新最新的版本快照,而读操作去读旧版本快照,没有互斥关系,这一点和 CopyOnWrite 类似。 -MVCC 的读操作不需要进行加锁,并且在可重复读隔离级别下能解决脏读和不可重复读问题。它的基本思想是为每个数据行维护多个版本的快照,多个事务可以同时去操作这个数据行。 +在 MVCC 中事务的修改操作(DELETE、INSERT、UPDATE)会为数据行新增一个版本快照。 -事务的修改操作(DELETE、INSERT、UPDATE)会去修改该事务对应版本的快照。 +脏读和不可重复读最根本的原因是事务读取到其它事务未提交的修改。在事务进行读取操作时,为了解决脏读和不可重复读问题,MVCC 规定只能读取已经提交的快照。当然一个事务可以读取自身未提交的快照,这不算是脏读。 -脏读和不可重复读最根本的原因是事务读取到其它事务未提交的修改。在事务在进行读取操作时,为了解决脏读和不可重复读问题,读取的快照需要满足以下条件:快照在该事务开始之后没有被其它事务修改,否则会读取到其它事务的未提交的修改操作。 +## 版本号 + +- 系统版本号 SYS_ID:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。 +- 事务版本号 TRX_ID :事务开始时的系统版本号。 ## Undo 日志 -MVCC 的多版本指的是多个版本的快照,这个快照存储在 Undo 日志中,该日志通过回滚指针把一个数据行的所有快照连接起来。 +MVCC 的多版本指的是多个版本的快照,快照存储在 Undo 日志中,该日志通过回滚指针 ROLL_PTR 把一个数据行的所有快照连接起来。 -例如我们在 MySQL 创建一个表 t,包含主键 id 和一个字段 x。我们先插入一个数据行,然后对该数据行执行两次操作。 +例如在 MySQL 创建一个表 t,包含主键 id 和一个字段 x。我们先插入一个数据行,然后对该数据行执行两次更新操作。 ```sql INSERT INTO t(id, x) VALUES(1, "a"); @@ -277,45 +279,28 @@ UPDATE t SET x="b" WHERE id=1; UPDATE t SET x="c" WHERE id=1; ``` -因为我们没有使用 `START TRANSACTION` 将上面的操作当成一个事务来执行,根据 MySQL 的 AUTOCOMMIT 机制,每个操作都会被当成一个事务来执行,所以上面总共涉及到三个事务。 +因为没有使用 `START TRANSACTION` 将上面的操作当成一个事务来执行,根据 MySQL 的 AUTOCOMMIT 机制,每个操作都会被当成一个事务来执行,所以上面的操作总共涉及到三个事务。快照中除了记录事务版本号 TRX_ID 和操作之外,还记录了一个 bit 的 DEL 字段,用于标记是否被删除。 -

+

-## 版本号 +INSERT、UPDATE、DELETE 操作会创建一个日志,并将事务版本号 TRX_ID 写入。DELETE 可以看成是一个特殊的 UPDATE,还会额外将 DEL 字段设置为 1。 -- 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。 -- 事务版本号:事务开始时的系统版本号。 +## ReadView -## 隐藏的列 +MVCC 维护了一个 ReadView 结构,主要包含了当前系统未提交的事务列表 TRX_IDs {TRX_ID_1, TRX_ID_2, ...},还有该列表的最小值 TRX_ID_MIN 和 TRX_ID_MAX。 -MVCC 在每个数据行后面都保存着两个隐藏的列,用来存储两个版本号: +

-- 创建版本号:指示创建一个数据行的快照时的系统版本号; -- 删除版本号:如果该快照的删除版本号未定义或删除版本号大于当前事务版本号表示该快照有效。 +在进行 SELECT 操作时,根据数据行快照的 TRX_ID 与 TRX_ID_MIN 和 TRX_ID_MAX 之间的关系,从而判断数据行快照是否可以使用: -## 实现过程 +- TRX_ID < TRX_ID_MIN,表示该数据行快照时在当前所有未提交事务之前进行更改的,因此可以使用。 -以下实现过程针对可重复读隔离级别。 +- TRX_ID > TRX_ID_MAX,表示该数据行快照是在事务启动之后被更改的,因此不可使用。 +- TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根据隔离级别再进行判断: + - 提交读:如果 TRX_ID 在 TRX_IDs 列表中,表示该数据行快照对应的事务还未提交,则该快照不可使用。否则表示已经提交,可以使用。 + - 可重复读:都不可以使用。因为如果可以使用的话,那么其它事务也可以读到这个数据行快照并进行修改,那么当前事务再去读这个数据行得到的值就会发生改变,也就是出现了不可重复读问题。 -因为数据行快照的创建版本号是创建数据行快照时的系统版本号,系统版本号随着创建事务而递增,所以新开始一个事务时,这个事务的系统版本号比之前的系统版本号都大,也就是比当前所有数据行快照的创建版本号都大。 - -### 1. SELECT - -多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。 - -把没有对一个数据行做修改的事务称为 T,T 所要读取的数据行快照的创建版本号必须小于等于 T 的版本号,因为如果大于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。除此之外,T 所要读取的数据行快照的删除版本号必须是未定义或者大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。 - -### 2. INSERT - -将当前系统版本号作为数据行快照的创建版本号。 - -### 3. DELETE - -将当前系统版本号作为数据行快照的删除版本号。 - -### 4. UPDATE - -将当前系统版本号作为更新前的数据行快照的删除版本号,并将当前系统版本号作为更新后的数据行快照的创建版本号。可以理解为先执行 DELETE 后执行 INSERT。 +在数据行快照不可使用的情况下,需要沿着 Undo Log 的回滚指针 ROLL_PTR 找到下一个快照,再进行上面的判断。 ## 快照读与当前读 @@ -329,7 +314,7 @@ SELECT * FROM table ...; ### 2. 当前读 -MVCC 其它会对数据库进行修改的操作(INSERT、UPDATE、DELETE)需要进行加锁操作,从而读取最新的数据。 +MVCC 其它会对数据库进行修改的操作(INSERT、UPDATE、DELETE)需要进行加锁操作,从而读取最新的数据。可以看到 MVCC 并不是完全不用加锁,而只是避免了 SELECT 的加锁操作。 ```sql INSERT;