From bee88da3609e03fd8e0ea7786eae3c796a3d95c3 Mon Sep 17 00:00:00 2001 From: "haoyang.shi" Date: Wed, 19 Feb 2020 22:43:41 +0800 Subject: [PATCH] fix MVCC --- .../database/mysql/architecture/index.md | 4 +- .../database/mysql/innodb/concurrent/index.md | 37 +++++++++---------- .../database/mysql/innodb/index/index.md | 6 +-- .../mysql/innodb/transaction/index.md | 2 +- content/docs/menu/index.md | 8 ++-- 5 files changed, 28 insertions(+), 29 deletions(-) diff --git a/content/docs/basic/database/mysql/architecture/index.md b/content/docs/basic/database/mysql/architecture/index.md index 861f0e101..d6490487a 100644 --- a/content/docs/basic/database/mysql/architecture/index.md +++ b/content/docs/basic/database/mysql/architecture/index.md @@ -9,7 +9,7 @@ categories: database 总体来说 MySQL 可以分为两层,第一层是 MySQL 的服务层,包含 MySQL 核心服务功能:解析、分析、优化、缓存以及内置函数,所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图等。 -第二层是 MySQL 的 **存储引擎层**,MySQL 中可使用多种存储引擎:Innodb、MyISAM、Memory。存储引擎负责 MySQL 中数据的存取。服务层通过统一的 API 与存储引擎进行通信,这些 API 屏蔽来同步存储引擎之间的差异,使得这些差异对上层的查询过程透明。 +第二层是 MySQL 的 **存储引擎层**,MySQL 中可使用多种存储引擎:InnoDB、MyISAM、Memory。存储引擎负责 MySQL 中数据的存取。服务层通过统一的 API 与存储引擎进行通信,这些 API 屏蔽来同步存储引擎之间的差异,使得这些差异对上层的查询过程透明。 ![](./assists/mysql_architecture.svg) @@ -47,7 +47,7 @@ categories: database ![](./assists/mysql_update_process.jpg) -1. MySQL Server 发送更新请求到 Innodb 引擎 +1. MySQL Server 发送更新请求到 InnoDB 引擎 2. 从 Buffer Pool 加载对应记录的 Data Page(P1) 1. 若 Buffer Pool 中没有该记录,则从磁盘加载该记录 3. 将 P1 存储到 Undo Page 中,并在 Redo Log Buffer 中记录 Undo 操作 diff --git a/content/docs/basic/database/mysql/innodb/concurrent/index.md b/content/docs/basic/database/mysql/innodb/concurrent/index.md index 6964a6000..519b5843d 100644 --- a/content/docs/basic/database/mysql/innodb/concurrent/index.md +++ b/content/docs/basic/database/mysql/innodb/concurrent/index.md @@ -1,11 +1,11 @@ --- -title: Innodb 并发控制 +title: InnoDB 并发控制 date: 2020-02-19 draft: false categories: database --- -# Innodb 并发控制 +# InnoDB 并发控制 ## InnoDB 锁机制 @@ -87,7 +87,7 @@ InnoDB 支持多粒度的锁,允许表级锁和行级锁共存。一个类似 #### 间隙锁的缺点 - 间隙锁有一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害 - - 当Query无法利用索引的时候, Innodb会放弃使用行级别锁定而改用表级别的锁定,造成并发性能的降低; + - 当Query无法利用索引的时候, InnoDB会放弃使用行级别锁定而改用表级别的锁定,造成并发性能的降低; - 当Quuery使用的索引并不包含所有过滤条件的时候,数据检索使用到的索引键所指向的数据可能有部分并不属于该Query的结果集的行列,但是也会被锁定,因为间隙锁锁定的是一个范围,而不是具体的索引键; - 当Query在使用索引定位数据的时候,如果使用的索引键一样但访问的数据行不同的时候(索引只是过滤条件的一部分),一样会被锁定 @@ -164,16 +164,27 @@ MySQL Server 此时才会判断返回的记录是否满足`id<7`的查询条件 ## MVCC -到目前为止我们介绍的 **并发控制机制其实都是通过延迟或者终止相应的事务来解决事务之间的竞争条件(`Race condition`)来保证事务的可串行化**;虽然前面的两种并发控制机制确实能够从根本上解决并发事务的可串行化的问题,但是在实际环境中数据库的事务大都是只读的,**读请求是写请求的很多倍**,如果写请求和读请求之前没有并发控制机制,那么最坏的情况也是读请求读到了已经写入的数据,这对很多应用完全是可以接受的。 +InnoDB 引擎支持 MVCC(Multiversion Concurrency Control):InnoDB 保存了行的历史版本,以支持事务的并发控制和回滚。这些历史信息保存在表空间的 **回滚段(Rollback Segment)** 里,回滚段中存储着 **Undo Log**。当事务需要进行回滚时,InnoDB 就会使用这些信息来进行 Undo 操作,同时这些信息也可用来实现 **一致性读**。 -![](images/e9a3a71f517878598235ac6751fe510d.png) +InnoDB 在存储的每行数据中都增加了三列隐藏属性: + - `DB_TRX_ID`:最后一次插入或更新的事务ID + - `DB_ROLL_PTR`:指向已写入回滚段的 Undo Log 记录。如果这行记录是更新的,那么就可以根据这个 Undo Log 记录重建之间的数据 + - `DB_ROW_ID`:自增序列,如果表未指定主键,则由该列作为主键 -在这种大前提下,数据库系统引入了另一种并发控制机制 - 多版本并发控制(`Multiversion Concurrency Control`),**每一个写操作都会创建一个新版本的数据,读操作会从有限多个版本的数据中挑选一个最合适的结果直接返回**;在这时,读写操作之间的冲突就不再需要被关注,而 **管理和快速挑选数据的版本就成了 MVCC 需要解决的主要问题**。 +在回滚段的 Undo Log 被分为 `Insert Undo Log` 和 `Update Undo Log`。Insert Undo Log 只是在事务回滚的时候需要,在事务提交后就可丢弃。Update Undo Log 不仅仅在回滚的时候需要,还要提供一致性读,所以只有在所有需要该 Update Undo Log 构建历史版本数据的事务都提交后才能丢弃。MySQL 建议尽量频繁的提交事务,这样可以保证 InnoDB 快速的丢弃 Update Undo Log,防止其过大。 -**MVCC 并不是一个与乐观和悲观并发控制对立的东西,它能够与两者很好的结合以增加事务的并发量**,在目前最流行的 SQL 数据库 MySQL 和 PostgreSQL 中都对 MVCC 进行了实现;但是由于它们分别实现了悲观锁和乐观锁,所以 MVCC 实现的方式也不同。 +在 InnoDB 中,行数据的物理删除不是立刻执行,InnoDB 会在行删除的 Undo Log 被丢弃时才会进行物理删除。这个过程被称之为 **清理(Purge)**,其执行过程十分迅速。 + +### MVCC 二级索引 + +InnoDB 在更新时对 二级索引 和 聚集索引的处理方式不一样。在聚集索引上的更新是原地更新(in-place),其中的隐藏属性 `DB_ROLL_PTR` 指向的 Undo Log 可以重建历史数据。但是二级索引没有隐藏属性,所以不能原地更新。 + +当二级索引的数据被更新时,旧的二级索引记录标记为 **标记删除(delete-marked)**,然后插入一条新的索引记录,最终标记删除的索引记录会被清除。当二级索引记录被标记为 delete-marked 或者有更新的事务更新时,InnoDB 会查找聚集索引。在聚集索引中检查行的 `DB_TRX_ID`,如果事务修改了记录,则从 Undo Log 中构建行数据的正确版本。如果二级索引记录被标记为 delete-marked 或者 二级索引有更新的事务更新,覆盖索引技术不会被使用(获取行任意数据均需要回表)。 ### MVCC vs 乐观锁 +**MVCC 并不是一个与乐观和悲观并发控制对立的东西,它能够与两者很好的结合以增加事务的并发量**,在目前最流行的 SQL 数据库 MySQL 和 PostgreSQL 中都对 MVCC 进行了实现;但是由于它们分别实现了悲观锁和乐观锁,所以 MVCC 实现的方式也不同。 + MVCC 可以保证不阻塞地读到一致的数据。但是,MVCC 并没有对实现细节做约束,为此不同的数据库的语义有所不同,比如: - `postgres` 对写操作也是乐观并发控制;在表中保存同一行数据记录的多个不同版本,每次写操作,都是创建,而回避更新;在事务提交时,按版本号检查当前事务提交的数据是否存在写冲突,则抛异常告知用户,回滚事务; @@ -181,15 +192,3 @@ MVCC 可以保证不阻塞地读到一致的数据。但是,MVCC 并没有对 - `innodb` 则只对读无锁,写操作仍是上锁的悲观并发控制,这也意味着,`innodb` 中只能见到因死锁和不变性约束而回滚,而见不到因为写冲突而回滚,不像 postgres 那样对数据修改在表中创建新纪录,而是每行数据只在表中保留一份,在更新数据时上行锁,同时将旧版数据写入 `undo log`。表和 undo log 中行数据都记录着事务ID,在检索时,只读取来自当前已提交的事务的行数据。 可见 MVCC 中的写操作仍可以按悲观并发控制实现,而 `CAS` 的写操作只能是乐观并发控制。还有一个不同在于,MVCC 在语境中倾向于 “对多行数据打快照造平行宇宙”,然而 `CAS` 一般只是保护单行数据而已。比如 mongodb 有 CAS 的支持,但不能说这是 MVCC。 - -### MySQL 与 MVCC - -MySQL 中实现的多版本两阶段锁协议(Multiversion 2PL)将 MVCC 和 2PL 的优点结合了起来,每一个版本的数据行都具有一个唯一的时间戳,当有读事务请求时,数据库程序会直接从多个版本的数据项中具有最大时间戳的返回。 - -![](images/b22ce20ca658d10fb6763d1b6b8b29e1.png) - -更新操作就稍微有些复杂了,事务会先读取最新版本的数据计算出数据更新后的结果,然后创建一个新版本的数据,新数据的时间戳是目前数据行的最大版本 `+1`: - -![](images/3e0b6b9589c54d5b93ec689fbbf13275.png) - -数据版本的删除也是根据时间戳来选择的, `MySQL` 会将版本最低的数据定时从数据库中清除以保证不会出现大量的遗留内容。 diff --git a/content/docs/basic/database/mysql/innodb/index/index.md b/content/docs/basic/database/mysql/innodb/index/index.md index a991828e0..72e976bdb 100644 --- a/content/docs/basic/database/mysql/innodb/index/index.md +++ b/content/docs/basic/database/mysql/innodb/index/index.md @@ -1,11 +1,11 @@ --- -title: Innodb 索引 +title: InnoDB 索引 date: 2020-02-19 draft: false categories: database --- -# Innodb 索引 +# InnoDB 索引 ## 数据存储 @@ -53,7 +53,7 @@ categories: database `B+` 树在查找对应的记录时,并不会直接从树中找出对应的行记录,它只能获取记录所在的页,将整个页加载到内存中,再通过 `Page Directory` 中存储的稀疏索引和 `n_owned、next_record` 属性取出对应的记录,不过因为这一操作是在内存中进行的,所以通常会忽略这部分查找的耗时。这样就存在一个命中率的问题,如果一个page中能够相对的存放足够多的行,那么命中率就会相对高一些,性能就会有提升。 -B+树底层的叶子节点为一双向链表,因此 **每个页中至少应该有两行记录**,这就决定了 Innodb 在存储一行数据的时候不能够超过 `8kb`,但事实上应该更小,因为还有一些 InnoDB 内部数据结构要存储。 +B+树底层的叶子节点为一双向链表,因此 **每个页中至少应该有两行记录**,这就决定了 InnoDB 在存储一行数据的时候不能够超过 `8kb`,但事实上应该更小,因为还有一些 InnoDB 内部数据结构要存储。 通常我们认为 `blob` 这类的大对象的存储会把数据存放在 off-page,其实不然,**关键点还是要看一个 page 中到底能否存放两行数据,blob 可以完全存放在数据页中(单行长度没有超过 `8kb`),而 `varchar` 类型的也有可能存放在溢出页中(单行长度超过 `8kb`,前 `768byte` 存放在数据页中)**。 diff --git a/content/docs/basic/database/mysql/innodb/transaction/index.md b/content/docs/basic/database/mysql/innodb/transaction/index.md index 4ac6e6d53..790723e62 100644 --- a/content/docs/basic/database/mysql/innodb/transaction/index.md +++ b/content/docs/basic/database/mysql/innodb/transaction/index.md @@ -1,5 +1,5 @@ --- -title: Innodb 事务 +title: InnoDB 事务 date: 2020-02-19 draft: false categories: database diff --git a/content/docs/menu/index.md b/content/docs/menu/index.md index 5a6736b1b..980a4e4d0 100644 --- a/content/docs/menu/index.md +++ b/content/docs/menu/index.md @@ -31,10 +31,10 @@ headless: true - [Websocket]({{< relref "/docs/basic/net/websocket/index.md" >}}) - [MySQL]({{< relref "/docs/basic/database/mysql/_index.md" >}}) - [MySQL架构]({{< relref "/docs/basic/database/mysql/architecture/index.md" >}}) - - [Innodb]({{< relref "/docs/basic/database/mysql/innodb/_index.md" >}}) - - [Innodb索引]({{< relref "/docs/basic/database/mysql/innodb/index/index.md" >}}) - - [Innodb并发控制]({{< relref "/docs/basic/database/mysql/innodb/concurrent/index.md" >}}) - - [Innodb事务]({{< relref "/docs/basic/database/mysql/innodb/transaction/index.md" >}}) + - [InnoDB]({{< relref "/docs/basic/database/mysql/innodb/_index.md" >}}) + - [InnoDB索引]({{< relref "/docs/basic/database/mysql/innodb/index/index.md" >}}) + - [InnoDB并发控制]({{< relref "/docs/basic/database/mysql/innodb/concurrent/index.md" >}}) + - [InnoDB事务]({{< relref "/docs/basic/database/mysql/innodb/transaction/index.md" >}}) - [分库分表]({{< relref "/docs/basic/database/mysql/sharding/index.md" >}}) - [Redis]({{< relref "/docs/basic/database/redis/index.md" >}}) - [密码学]({{< relref "/docs/basic/cryptology/index.md" >}})