From d5b5bda35a47450094e01bc35e23fb735aaceaff Mon Sep 17 00:00:00 2001 From: "595208882@qq.com" Date: Mon, 20 Sep 2021 09:01:36 +0800 Subject: [PATCH] adjust --- Database.md | 1191 ++++++++++++++++++++++++++++----------------------- 1 file changed, 647 insertions(+), 544 deletions(-) diff --git a/Database.md b/Database.md index 24aa6e7..ae62690 100644 --- a/Database.md +++ b/Database.md @@ -1279,6 +1279,8 @@ MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议 **特点**:避免脏读 +**脏读解决方案:基于乐观锁理论的MVCC(多版本并发控)实现** + **读已提交的数据库锁情况** - 读取数据:**加行级共享锁(读到时才加锁),读完后立即释放** @@ -1286,17 +1288,6 @@ MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议 -**Read Committed隔离级别下的加锁分析** - -隔离级别的实现与锁机制密不可分,所以需要引入锁的概念,首先我们看下InnoDB存储引擎提供的两种标准的行级锁: - -- **共享锁(S Lock)**:又称为读锁,可以允许多个事务并发的读取同一资源,互不干扰。即如果一个事务T对数据A加上共享锁后,其他事务只能对A再加共享锁,不能再加排他锁,只能读数据,不能修改数据 -- **排他锁(X Lock)**: 又称为写锁,如果事务T对数据A加上排他锁后,其他事务不能再对A加上任何类型的锁,获取排他锁的事务既能读数据,也能修改数据 - -**注意**: 共享锁和排他锁是不相容的。 - - - ### Repeatable Read(可重复读) **它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行**。但会导致**幻读 (Phantom Read)**问题。 @@ -1305,6 +1296,8 @@ MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议 **特点**:避免脏读、不可重复读。MySQL默认事务隔离级别 +**不可重复读解决方案:基于乐观锁理论的MVCC(多版本并发控)实现** + **可重复读的数据库锁情况** - 读取数据:**开始读取的瞬间对其增加行级共享锁,直到事务结束才释放** @@ -1442,1029 +1435,1139 @@ public void addGoods(){ -# 索引机制 - -索引是为了加速对表中数据行的检索而创建的一种分散存储的(不连续的)数据结构,硬盘级的。 +# InnoDB引擎 -**索引意义**:索引能极大的减少存储引擎需要扫描的数据量,索引可以把随机IO变成顺序IO。索引可以帮助我们在进行分组、排序等操作时,避免使用临时表。正确的创建合适的索引是提升数据库查询性能的基础。 +## 线程模型 +InnoDB存储引擎是多线程的模型,所以犹太有多个不同的后台线程,负责处理不同的任务,主要有:`Master Thread`、`I/O Thread`、`Purge Thread`、`Page Cleaner Thread` 四种。 +### Master Thread -**优势** +**核心后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲(Insert Buffer)、回滚页(UNDO PAGE)的回收等。** -- **大大减少服务器需要扫描的数据量** -- **可以提高数据检索的效率(将随机I/O变成顺序I/O),降低数据库的I/O成本** -- **通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗** -**劣势** -- **索引会占据磁盘空间** -- **索引虽然会提高查询效率,但是会降低更新表的效率**。比如每次对表进行增删改操作,MySQL不仅要保存数据,还有保存或者更新对应的索引文件 +### I/O Thread +**在InnoDB存储引擎中大量使用AIO来处理IO请求,而I/O Thread主要负责处理这些 I/O 请求的回调(call back)处理。**这里共有四类I/O线程,分别是:`insert buffer thread`、`log thread`、`read thread`、`write thread`。 +```sql +mysql> show variables like "%innodb%io_threads%"; ++-------------------------+-------+ +| Variable_name | Value | ++-------------------------+-------+ +| innodb_read_io_threads | 4 | +| innodb_write_io_threads | 4 | ++-------------------------+-------+ +``` -## 数据结构 -### Hash索引 -![Hash索引](images/Database/Hash索引.png) +### Purge Thread -**原理** +**purge thread线程用来回收事务提交后其被分配的undo页,默认是开启的,可以通过修改配置文件来配置多个Purge Thread线程。** -- 事先将索引通过 hash算法后得到的hash值(即磁盘文件指针)存到hash表中 -- 在进行查询时,将索引通过hash算法,得到hash值,与hash表中的hash值比对。通过磁盘文件指针,只要**一次磁盘IO**就能找到要的值 +```sql +mysql> show variables like "%purge_threads%"; ++----------------------+-------+ +| Variable_name | Value | ++----------------------+-------+ +| innodb_purge_threads | 4 | ++----------------------+-------+ +1 row in set (0.00 sec) +``` -例如:在第一个表中,要查找col=6的值。hash(6) 得到值,比对hash表,就能得到89。性能非常高。 +在提交了一个事务给数据库时,为了保证事务能够回滚,会在内存中缓存下该事务未执行时的状态(undo 日志),如果事务执行不成功,那么就恢复之前的状态;如果执行成功,undo日志就用不着了,要把它删除以腾出空间。Purge 线程就是做这个删除工作的。默认有4个线程。 -**优点** +### Page Cleaner Thread -- 快速查询:参与索引的字段只要进行Hash运算之后就可以快速定位到该记录,时间复杂度约为1 +**用于多版本控制功能中回收delete和update操作产生的脏页,用来执行将脏页刷新到磁盘。** -**缺点** +MySQL 5.7 版本以后,支持设置多个刷脏页线程,提高脏页处理性能。设置命令,比如: -- 哈希索引只包含哈希值和行指针,所以不能用索引中的值来避免读取行 -- 哈希索引数据并不是按照索引值顺序存储的,所以也就无法用于排序和范围查询 -- 哈希索引也不支持部分索引列查询,因为哈希索引始终是使用索引列的全部数据进行哈希计算的 -- 哈希索引只支持等值比较查询,如=,IN(),<=>操作 -- 如果哈希冲突较多,一些索引的维护操作的代价也会更高 +`SET GLOBAL innodb_page_cleaner = 3` -### R-Tree索引 +## 数据页 -空间数据索引。 +数据页主要是用来存储表中记录的,它在磁盘中是用双向链表相连的,方便查找,能够非常快速得从一个数据页,定位到另一个数据页。 +- 写操作时,先将数据写到内存的某个批次中,然后再将该批次的数据一次性刷到磁盘上。如下图所示: + ![InnoDB-数据页-写操作](images/Database/InnoDB-数据页-写操作.jpg) -### B-Tree索引 +- 读操作时,从磁盘上一次读一批数据,然后加载到内存当中,以后就在内存中操作。如下图所示: -**背景**:二叉查找树查询的时间复杂度是O(logN),查找速度最快和比较次数较少。但用于数据库索引,当数据量过大,不可能将所有索引加载进内存,使用二叉树会导致磁盘IO过于频繁,最坏的情况下磁盘IO的次数由树的高度来决定。 + ![InnoDB-数据页-读操作](images/Database/InnoDB-数据页-读操作.jpg) -B-Tree(平衡多路查找树)对二叉树进行了横向扩展,能很好解决红黑树中遗留的高度问题,使树结构更加**矮胖**,使得一次IO能加载更多关键字,对比在内存中完成,减少了磁盘IO次数,更适用于大型数据库,但是为了保持自平衡,插入或者删除元素都会导致节点发生裂变反应,有时候会非常麻烦。 +磁盘中各数据页的整体结构如下图所示: -![索引-B树结构](images/Database/索引-B树结构.png) +![InnoDB-数据页](images/Database/InnoDB-数据页.jpg) +通常情况下,单个数据页默认的大小是`16kb`。当然,我们也可以通过参数:`innodb_page_size`,来重新设置大小。不过,一般情况下,用它的默认值就够了。单个数据页包含内容如下: +![InnoDB-单个数据页内容](images/Database/InnoDB-单个数据页内容.jpg) -**案例分析**:模拟下查找key为10的data的过程 +### 文件头部 -![B-Tree案例分析](images/Database/B-Tree案例分析.png) +通过前面介绍的行记录中`下一条记录的位置`和`页目录`,innodb能非常快速的定位某一条记录。但有个前提条件,就是用户记录必须在同一个数据页当中。 -- 根据根结点指针读取文件目录的根磁盘块1。【磁盘IO操作第**1次**】 -- 磁盘块1存储15,45和三个指针数据。我们发现10<15,因此我们找到指针p1 -- 根据p1指针,我们定位并读取磁盘块2。【磁盘IO操作**2次**】 -- 磁盘块2存储7,12和三个指针数据。我们发现7<10<12,因此我们找到指针p2 -- 根据p2指针,我们定位并读取磁盘块6。【磁盘IO操作**3次**】 -- 磁盘块6中存储8,10。我们找到10,获取10所对应的数据data +如果用户记录非常多,在第一个数据页找不到我们想要的数据,需要到另外一页找该怎么办呢?这时就需要使用`文件头部`了。它里面包含了多个信息,但我只列出了其中4个最关键的信息: +- 页号 +- 上一页页号 +- 下一页页号 +- 页类型 +顾名思义,innodb是通过页号、上一页页号和下一页页号来串联不同数据页的。如下图所示: -**存在问题** +![InnoDB-文件头部](images/Database/InnoDB-文件头部.jpg) -- **不支持范围查询的快速查找**。可以用B+Tree解决 +不同的数据页之间,通过上一页页号和下一页页号构成了双向链表。这样就能从前向后,一页页查找所有的数据了。此外,页类型也是一个非常重要的字段,它包含了多种类型,其中比较出名的有:数据页、索引页(目录项页)、溢出页、undo日志页等。 -- **每行数据量很大时,会导致B-Tree深度较大,进而影响查询效率**。可以用B+Tree解决 +### 页头部 -### B+Tree索引 +比如一页数据到底保存了多条记录,或者页目录到底使用了多个槽等。这些信息是实时统计,还是事先统计好了,保存到某个地方?为了性能考虑,上面的这些统计数据,当然是先统计好,保存到一个地方。后面需要用到该数据时,再读取出来会更好。这个保存统计数据的地方,就是`页头部`。当然页头部不仅仅只保存:槽的数量、记录条数等信息。它还记录了: -B+树是B-树的变体,也是一种多路搜索树。其定义基本与B-树相同,除了: +- 已删除记录所占的字节数 +- 最后插入记录的位置 +- 最大事务id +- 索引id +- 索引层级 -- 非叶子结点的子树指针与关键字个数相同 -- 非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B-树是开区间) -- 为所有叶子结点增加一个链指针 -- 所有关键字都在叶子结点出现 -![索引-B+Tree结构](images/Database/索引-B+Tree结构.png) +### 最大和最小记录 +在一个数据页当中,如果存在多条用户记录,它们是通过`下一条记录的位置`相连的。不过有个问题:如果才能快速找到最大的记录和最小的记录呢?这就需要在保存用户记录的同时,也保存最大和最小记录了。最大记录保存到Supremum记录中。最小记录保存在Infimum记录中。 -**案例分析:等值查询** +在保存用户记录时,数据库会自动创建两条额外的记录:Supremum 和 Infimum。它们之间的关系,如下图所示: -假如我们查询值等于9的数据。查询路径磁盘块1->磁盘块2->磁盘块6。 +![InnoDB-最大和最小记录](images/Database/InnoDB-最大和最小记录.jpg) -![B+树索引-等值查询](images/Database/B+树索引-等值查询.png) +从图中可以看出用户数据是从最小记录开始,通过下一条记录的位置,从小到大,一步步查找,最后找到最大记录为止。 -- 第一次磁盘IO:将磁盘块1加载到内存中,在内存中从头遍历比较,9<15,走左路,到磁盘寻址磁盘块2 -- 第二次磁盘IO:将磁盘块2加载到内存中,在内存中从头遍历比较,7<9<12,到磁盘中寻址定位到磁盘块6 -- 第三次磁盘IO:将磁盘块6加载到内存中,在内存中从头遍历比较,在第三个索引中找到9,取出data,如果data存储的行记录,取出data,查询结束。如果存储的是磁盘地址,还需要根据磁盘地址到磁盘中取出数据,查询终止。(这里需要区分的是在InnoDB中Data存储的为行数据,而MyIsam中存储的是磁盘地址。) +### 用户记录 -**案例分析:范围查询** -假如我们想要查找9和26之间的数据。查找路径是磁盘块1->磁盘块2->磁盘块6->磁盘块7。 +对于新申请的数据页,用户记录是空的。当插入数据时,innodb会将一部分`空闲空间`分配给用户记录。用户记录是innodb的重中之重,我们平时保存到数据库中的数据,就存储在它里面。其实在innodb支持的数据行格式有四种: -![案例分析-B+树-范围查询](images/Database/案例分析-B+树-范围查询.png) +- compact行格式 +- redundant行格式 +- dynamic行格式 +- compressed行格式 -- 首先查找值等于9的数据,将值等于9的数据缓存到结果集。这一步和前面等值查询流程一样,发生了三次磁盘IO -- 查找到15之后,底层的叶子节点是一个有序列表,我们从磁盘块6,键值9开始向后遍历筛选所有符合筛选条件的数据 -- 第四次磁盘IO:根据磁盘6后继指针到磁盘中寻址定位到磁盘块7,将磁盘7加载到内存中,在内存中从头遍历比较,9<25<26,9<26<=26,将data缓存到结果集。 -- 主键具备唯一性(后面不会有<=26的数据),不需再向后查找,查询终止。将结果集返回给用户 +以compact行格式为例: -**优点** +![InnoDB-compact行格式](images/Database/InnoDB-compact行格式.jpg) -- 单次请求涉及的磁盘IO次数少(出度d大,且非叶子节点不包含表数据,树的高度小) -- 查询效率稳定(任何关键字的查询必须走从根结点到叶子结点,查询路径长度相同) -- 遍历效率高(从符合条件的某个叶子节点开始遍历即可) +一条用户记录主要包含三部分内容: -**缺点** +- 记录额外信息:它包含了变长字段、null值列表和记录头信息 +- 隐藏列:它包含了行id、事务id和回滚点 +- 真正的数据列:包含真正的用户数据,可以有很多列 -B+树最大的性能问题在于会产生大量的随机IO,主要存在以下两种情况: -- 主键不是有序递增的,导致每次插入数据产生大量的数据迁移和空间碎片 -- 即使主键是有序递增的,大量写请求的分布仍是随机的 +#### 额外信息 +额外信息并非真正的用户数据,它是为了辅助存数据用的。 -### B*Tree +- **变长字段列表** -是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针; + 有些数据如果直接存会有问题,比如:如果某个字段是varchar或text类型,它的长度不固定,可以根据存入数据的长度不同,而随之变化。如果不在一个地方记录数据真正的长度,innodb很可能不知道要分配多少空间。假如都按某个固定长度分配空间,但实际数据又没占多少空间,岂不是会浪费?所以,需要在变长字段中记录某个变长字段占用的字节数,方便按需分配空间。 -![索引机制-B星树](images/Database/索引机制-B星树.jpg) +- **null值列表** -B*树定义了非叶子结点关键字个数至少为(2/3)M,即块的最低使用率为2/3(代替B+树的1/2)。 + 数据库中有些字段的值允许为null,如果把每个字段的null值,都保存到用户记录中,显然有些浪费存储空间。有没有办法只简单的标记一下,不存储实际的null值呢?答案:将为null的字段保存到null值列表。在列表中用二进制的值1,表示该字段允许为null,用0表示不允许为null。它只占用了1位,就能表示某个字符是否为null,确实可以节省很多存储空间。 -**B+树的分裂** +- **记录头信息** -当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针。 + 记录头信息用于描述一些特殊的属性。它主要包含: -**B*树的分裂** + - deleted_flag:即删除标记,用于标记该记录是否被删除了 + - min_rec_flag:即最小目录标记,它是非叶子节点中的最小目录标记 + - n_owned:即拥有的记录数,记录该组索引记录的条数 + - heap_no:即堆上的位置,它表示当前记录在堆上的位置 + - record_type:即记录类型,其中0表示普通记录,1表示非叶子节点,2表示Infrimum记录, 3表示Supremum记录 + - next_record:即下一条记录的位置 -当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;所以,B*树分配新结点的概率比B+树要低,空间使用率更高。 +#### 隐藏列 -## 索引类型 +数据库在保存一条用户记录时,会自动创建一些隐藏列。如下图所示: -### 普通索引 +![InnoDB-隐藏列](images/Database/InnoDB-隐藏列.jpg) -**单列索引是最基本的索引,它没有任何限制,允许在定义索引的列中插入重复值和空值。** +目前innodb自动创建的隐藏列有三种: -```sql --- 直接创建索引 -CREATE INDEX index_name ON table_name(col_name); --- 修改表结构的方式添加索引 -ALTER TABLE table_name ADD INDEX index_name(col_name); +- db_row_id,即行id,它是一条记录的唯一标识。 +- db_trx_id,即事务id,它是事务的唯一标识。 +- db_roll_ptr,即回滚点,它用于事务回滚。 --- 创建表的时候同时创建索引 -CREATE TABLE `news` ( - `id` int(11) NOT NULL AUTO_INCREMENT , - `title` varchar(255) NOT NULL , - `content` varchar(255) NULL , - `time` varchar(20) NULL DEFAULT NULL , - PRIMARY KEY (`id`), - INDEX index_name (title(255)) -) +如果表中有主键,则用主键做行id,无需额外创建。如果表中没有主键,假如有不为null的unique唯一键,则用它做为行id,同样无需额外创建。如果表中既没有主键,又没有唯一键,则数据库会自动创建行id。也就是说在innodb中,隐藏列中`事务id`和`回滚点`是一定会被创建的,但行id要根据实际情况决定。 --- 删除索引 -DROP INDEX index_name ON table_name; --- 或 -alter table `表名` drop index 索引名; -``` +#### 真正数据列 -### 唯一索引 +真正的数据列中存储了用户的真实数据,它可以包含很多列的数据。 -**索引列中的值必须是唯一的,但是允许为空值(只允许存在一条空值)。** -```mysql --- 创建单个索引 -CREATE UNIQUE INDEX index_name ON table_name(col_name); --- 创建多个索引 -CREATE UNIQUE INDEX index_name on table_name(col_name,...); --- 修改表结构 --- 单个 -ALTER TABLE table_name ADD UNIQUE index index_name(col_name); --- 多个 -ALTER TABLE table_name ADD UNIQUE index index_name(col_name,...); -``` +### 页目录 +从上面可以看出,如果我们要查询某条记录的话,数据库会从最小记录开始,一条条查找所有记录。如果中途找到了,则直接返回该记录。如果一直找到最大记录,还没有找到想要的记录,则返回空。 +但效率会不会有点低?这不是要对整页用户数据进行扫描吗? -### 主键索引 +这就需要使用`页目录`了。说白了,就是把一页用户记录分为若干组,每一组的最大记录都保存到一个地方,这个地方就是`页目录`。每一组的最大记录叫做`槽`。由此可见,页目录是有多个槽组成的。所下图所示: -**索引列中的值必须是唯一的,不允许有空值。** +![InnoDB-页目录](images/Database/InnoDB-页目录.jpg) -```sql --- 主键索引(创建表时添加) -CREATE TABLE `news` ( - `id` int(11) NOT NULL AUTO_INCREMENT , - `title` varchar(255) NOT NULL , - `content` varchar(255) NULL , - `time` varchar(20) NULL DEFAULT NULL , - PRIMARY KEY (`id`) -) +假设一页的数据分为4组,这样在页目录中,就对应了4个槽,每个槽中都保存了该组数据的最大值。这样就能通过二分查找,比较槽中的记录跟需要找到的记录的大小。如果用户需要查找的记录,小于当前槽中的记录,则向上查找上一个槽。如果用户需要查找的记录,大于当前槽中的记录,则向下查找下一个槽。如此一来,就能通过二分查找,快速的定位需要查找的记录了。 --- 主键索引(创建表后添加) -CREATE TABLE `order` ( - `orderId` varchar(36) NOT NULL, - `productId` varchar(36) NOT NULL , - `time` varchar(20) NULL DEFAULT NULL -) -alter table `order` add primary key(`orderId`); -``` +### 文件尾部 -### 组合索引 +数据库的数据是以数据页为单位,加载到内存中,如果数据有更新的话,需要刷新到磁盘上。但如果某一天比较倒霉,程序在刷新到磁盘的过程中,出现了异常,比如:进程被kill掉了,或者服务器被重启了。这时候数据可能只刷新了一部分,如何判断上次刷盘的数据是完整的呢?这就需要用到`文件尾部`。它里面记录了页面的`校验和`。 -**在多个字段上创建的索引**。组合索引遵守“**最左前缀**”原则,即在查询条件中使用了复合索引的第一个字段,索引才会被使用。因此,在复合索引中索引列的顺序至关重要。 +在数据刷新到磁盘之前,会先计算一个页面的校验和。后面如果数据有更新的话,会计算一个新值。文件头部中也会记录这个校验和,由于文件头部在前面,会先被刷新到磁盘上。 -```sql --- 创建一个复合索引 -create index index_name on table_name(col_name1,col_name2,...); +接下来,刷新用户记录到磁盘的时候,假设刷新了一部分,恰好程序出现异常了。这时,文件尾部的校验和,还是一个旧值。数据库会去校验,文件尾部的校验和,不等于文件头部的新值,说明该数据页的数据是不完整的。 --- 修改表结构的方式添加索引 -alter table table_name add index index_name(col_name,col_name2,...); -``` +## 内存结构 -### 全文索引 +### Buffer Pool -**只能在文本类型 `CHAR`、`VARCHAR`、`TEXT` 类型字段上创建全文索引**。字段长度比较大时,如果创建普通索引,在进行like模糊查询时效率比较低,这时可以创建全文索引。 MyISAM和InnoDB中都可以使用全文索引。 +`InnoDB `为了解决磁盘 `I/O` 频繁操作问题,`MySQL `需要申请一块内存空间,这块内存空间称为`Buffer Pool`。 -```mysql --- 创建表的适合添加全文索引 -CREATE TABLE `news` ( - `id` int(11) NOT NULL AUTO_INCREMENT , - `title` varchar(255) NOT NULL , - `content` text NOT NULL , - `time` varchar(20) NULL DEFAULT NULL , - PRIMARY KEY (`id`), - FULLTEXT (content) -) +#### 缓存页 --- 修改表结构添加全文索引 -ALTER TABLE table_name ADD FULLTEXT index_fulltext_content(col_name); -``` +`MySQL`数据是以页为单位,每页默认`16KB`,称为**数据页**。在`Buffer Pool`里面会划分出若干个**缓存页**与**数据页**对应。 +![Buffer-Pool-缓存页](images/Database/Buffer-Pool-缓存页.png) -## 索引实现 -### MyIsam索引 +#### 描述数据 -MyISAM的数据文件和索引文件是分开存储的。MyISAM使用B+树构建索引树时,叶子节点中存储的键值为索引列的值,数据为索引所在行的磁盘地址。 +每个**缓存页**会有对应的一份**描述数据**(一一对应),里面存储了**缓存页的元数据信息**,包含一些所属表空间、数据页的编号、`Buffer Pool`中的地址等。可用于**缓存页**直接映射到对应的**数据页**。默认为`800Byte`每个描述数据。 -#### 主键索引 - -![在这里插入图片描述](images/Database/20201024114325883.png) - -表user的索引存储在索引文件`user.MYI`中,数据文件存储在数据文件 `user.MYD`中。简单分析下查询时的磁盘IO情况: +![Buffer-Pool-描述数据](images/Database/Buffer-Pool-描述数据.png) +后续对数据的增删改查都是在`Buffer Pool`里操作 +- 查询:从磁盘加载到缓存,后续直接查缓存 +- 插入:直接写入缓存 +- 更新删除:缓存中存在直接更新,不存在加载数据页到缓存更新 -**场景一:根据主键等值查询数据** -![在这里插入图片描述](images/Database/20201024114404727.png) -- 先在主键树中从根节点开始检索,将根节点加载到内存,比较28<75,走左路。(1次磁盘IO) -- 将左子树节点加载到内存中,比较16<28<47,向下检索。(1次磁盘IO) -- 检索到叶节点,将节点加载到内存中遍历,比较16<28,18<28,28=28。查找到值等于30的索引项。(1次磁盘IO) -- 从索引项中获取磁盘地址,然后到数据文件user.MYD中获取对应整行记录。(1次磁盘IO) -- 将记录返给客户端。 -**磁盘IO次数:3次索引检索+记录数据检索。** +`MySQL`宕机数据不就全丢了吗? +`InnoDB`提供了`WAL`技术(Write-Ahead Logging),通过`redo log`让`MySQL`拥有了崩溃恢复能力。再配合空闲时,会有异步线程做缓存页刷盘,保证数据的持久性与完整性。 +![Buffer-Pool-Write-Ahead-Logging](images/Database/Buffer-Pool-Write-Ahead-Logging.png) -**场景二:根据主键范围查询数据** +直接更新数据的缓存页称为**脏页**,缓存页刷盘后称为**干净页**。 -![在这里插入图片描述](images/Database/20201024114510253.png) -- 先在主键树中从根节点开始检索,将根节点加载到内存,比较28<75,走左路。(1次磁盘IO) -- 将左子树节点加载到内存中,比较16<28<47,向下检索。(1次磁盘IO) -- 检索到叶节点,将节点加载到内存中遍历比较16<28,18<28,28=28<47。查找到值等于28的索引项。 - - 根据磁盘地址从数据文件中获取行记录缓存到结果集中。(1次磁盘IO) - - 我们的查询语句时范围查找,需要向后遍历底层叶子链表,直至到达最后一个不满足筛选条件。 -- 向后遍历底层叶子链表,将下一个节点加载到内存中,遍历比较,28<47=47,根据磁盘地址从数据文件中获取行记录缓存到结果集中。(1次磁盘IO) -- 最后得到两条符合筛选条件,将查询结果集返给客户端。 +#### Free链表 -**磁盘IO次数:4次索引检索+记录数据检索。** +`MySQL`运行起来后,会不停的执行增删改查,需要从磁盘读取一个一个的数据页放入`Buffer Pool`对应的缓存页里,把数据缓存起来,以后就可以在内存里执行增删改查。但这个过程必然涉及一个问题,**哪些缓存页是空闲的**? -**备注:**以上分析仅供参考,MyISAM在查询时,会将索引节点缓存在MySQL缓存中,而数据缓存依赖于操作系统自身的缓存,所以并不是每次都是走的磁盘,这里只是为了分析索引的使用过程。 +为了解决该问题,使用链表结构把空闲缓存页的**描述数据**放入链表(`Free`链表)中。针对`Free`链表要做如下设计: +![Buffer-Pool-Free链表设计](images/Database/Buffer-Pool-Free链表设计.png) +- 新增`free`基础节点 +- 描述数据添加`free`节点指针 -#### 辅助索引 +最终呈现出来的,是由空闲缓存页的**描述数据**组成的`free`链表。 -在 MyISAM 中,辅助索引和主键索引的结构是一样的,没有任何区别,叶子节点的数据存储的都是行记录的磁盘地址。只是主键索引的键值是唯一的,而辅助索引的键值可以重复。查询数据时,由于辅助索引的键值不唯一,可能存在多个拥有相同的记录,所以即使是等值查询,也需要按照范围查询的方式在辅助索引树中检索数据。 +![Buffer-Pool-Free链表组成](images/Database/Buffer-Pool-Free链表组成.png) +有了`free`链表之后,我们只需要从`free`链表获取一个**描述数据**,就可以获取到对应的缓存页。 -### InnoDB索引 +![Buffer-Pool-Free链表-获取描述数据](images/Database/Buffer-Pool-Free链表-获取描述数据.png) -#### 主键索引 +往**描述数据**与**缓存页**写入数据后,就将该**描述数据**移出`free`链表。 -每个InnoDB表都有一个主键索引(聚簇索引) ,聚簇索引使用B+树构建,叶子节点存储的数据是整行记录。一般情况下,聚簇索引等同于主键索引,当一个表没有创建主键索引时,InnoDB会自动创建一个ROWID字段来构建聚簇索引。InnoDB创建索引的具体规则如下: -- 在表上定义主键PRIMARY KEY,InnoDB将主键索引用作聚簇索引 -- 如果表没有定义主键,InnoDB会选择第一个不为NULL的唯一索引列用作聚簇索引 -- 如果以上两个都没有,InnoDB 会使用一个6 字节长整型的隐式字段 ROWID字段构建聚簇索引。该ROWID字段会在插入新行时自动递增 -除聚簇索引之外的所有索引都称为辅助索引。在中InnoDB,辅助索引中的叶子节点存储的数据是该行的主键值都。 在检索时,InnoDB使用此主键值在聚簇索引中搜索行记录。 -主键索引的叶子节点会存储数据行,辅助索引只会存储主键值。 +#### 缓存页哈希表 -![在这里插入图片描述](images/Database/202010241146330.png) +查询数据时,如何在`Buffer Pool`里快速定位到对应的缓存页呢?难道需要一个**非空闲的描述数据**链表,再通过**表空间号+数据页编号**遍历查找吗?这样做也可以实现,但是效率不太高,时间复杂度是`O(N)`。 +![Buffer-Pool-缓存页哈希表](images/Database/Buffer-Pool-缓存页哈希表.png) +所以我们可以换一个结构,使用哈希表来缓存它们间的映射关系,时间复杂度是`O(1)`。 -**场景一:等值查询数据** +![Buffer-Pool-缓存页哈希表-复杂度](images/Database/Buffer-Pool-缓存页哈希表-复杂度.png) -- 先在主键树中从根节点开始检索,将根节点加载到内存,比较28<75,走左路。(1次磁盘IO) -- 将左子树节点加载到内存中,比较16<28<47,向下检索。(1次磁盘IO) -- 检索到叶节点,将节点加载到内存中遍历,比较16<28,18<28,28=28。查找到值等于28的索引项,直接可以获取整行数据。将改记录返回给客户端。(1次磁盘IO) +**表空间号+数据页号**,作为一个`key`,然后缓存页的地址作为`value`。每次加载数据页到空闲缓存页时,就写入一条映射关系到**缓存页哈希表**中。 -**磁盘IO数量:3次。** +![Buffer-Pool-缓存页哈希表-映射关系](images/Database/Buffer-Pool-缓存页哈希表-映射关系.png) -![在这里插入图片描述](images/Database/20201024114716460.png) +后续的查询,就可以通过**缓存页哈希表**路由定位了。 -#### 辅助索引 +#### Flush链表 -除聚簇索引之外的所有索引都称为辅助索引,InnoDB的辅助索引只会存储主键值而非磁盘地址。以表user_innodb的age列为例,age索引的索引结果如下图: +还记得之前有说过「**空闲时会有异步线程做缓存页刷盘,保证数据的持久性与完整性**」吗? -![在这里插入图片描述](images/Database/20201024114750255.png) +新问题来了,难道每次把`Buffer Pool`里所有的缓存页都刷入磁盘吗?当然不能这样做,磁盘`IO`开销太大了,应该把**脏页**刷入磁盘才对(更新过的缓存页)。可是我们怎么知道,那些缓存页是**脏页**?很简单,参照`free`链表,弄个`flush`链表出来就好了,只要缓存页被更新,就将它的**描述数据**加入`flush`链表。针对`flush`链表我们要做如下设计: -底层叶子节点的按照(age,id)的顺序排序,先按照age列从小到大排序,age列相同时按照id列从小到大排序。使用辅助索引需要检索两遍索引:首先检索辅助索引获得主键,然后使用主键到主索引中检索获得记录。 +- 新增`flush`基础节点 +- 描述数据添加`flush`节点指针 +![Buffer-Pool-Flush链表](images/Database/Buffer-Pool-Flush链表.png) +最终呈现出来的,是由更新过数据的缓存页**描述数据**组成的`flush`链表。 -**场景一:等值查询的情况** +![Buffer-Pool-Flush链表-缓存页](images/Database/Buffer-Pool-Flush链表-缓存页.png) -```sql -select * from t_user_innodb where age=19; -``` +后续异步线程都从`flush`链表刷缓存页,当`Buffer Pool`内存不足时,也会优先刷`flush`链表里的缓存页。 -![在这里插入图片描述](images/Database/2020102411481097.png) -根据在辅助索引树中获取的主键id,到主键索引树检索数据的过程称为**回表**查询。 -**磁盘IO数:辅助索引3次+获取记录回表3次** +#### LRU链表 +目前看来`Buffer Pool`的功能已经比较完善了。 +![Buffer-Pool-LRU链表](images/Database/Buffer-Pool-LRU链表.png) -#### 组合索引 +但是仔细思考下,发现还有一个问题没处理。`MySQL`数据库随着系统的运行会不停的把磁盘上的数据页加载到空闲的缓存页里去,因此`free`链表中的空闲缓存页会越来越少,直到没有,最后磁盘的数据页无法加载。 -还是以自己创建的一个表为例:表 abc_innodb,id为主键索引,创建了一个联合索引idx_abc(a,b,c)。 +![Buffer-Pool-LRU链表-无法加载](images/Database/Buffer-Pool-LRU链表-无法加载.png) -```sql -CREATE TABLE `abc_innodb` -( - `id` int(11) NOT NULL AUTO_INCREMENT, - `a` int(11) DEFAULT NULL, - `b` int(11) DEFAULT NULL, - `c` varchar(10) DEFAULT NULL, - `d` varchar(10) DEFAULT NULL, - PRIMARY KEY (`id`) USING BTREE, - KEY `idx_abc` (`a`, `b`, `c`) -) ENGINE = InnoDB; -``` +为了解决这个问题,我们需要淘汰缓存页,腾出空闲缓存页。可是我们要优先淘汰哪些缓存页?总不能一股脑直接全部淘汰吧?这里就要借鉴`LRU`算法思想,把最近最少使用的缓存页淘汰(命中率低),提供`LRU`链表出来。针对`LRU`链表我们要做如下设计: -组合索引的数据结构: +- 新增`LRU`基础节点 +- 描述数据添加`LRU`节点指针 -![在这里插入图片描述](images/Database/20201024114900213.png) +![Buffer-Pool-LRU链表-结构](images/Database/Buffer-Pool-LRU链表-结构.png) -**组合索引的查询过程:** +实现思路也很简单,只要是查询或修改过缓存页,就把该缓存页的描述数据放入链表头部,也就说近期访问的数据一定在链表头部。 -```sql -select * from abc_innodb where a = 13 and b = 16 and c = 4; -``` +![Buffer-Pool-LRU链表-节点](images/Database/Buffer-Pool-LRU链表-节点.png) -![在这里插入图片描述](images/Database/20201024115012887.png) +当`free`链表为空的时候,直接淘汰`LRU`链表尾部缓存页即可。 -**最左匹配原则** -最左前缀匹配原则和联合索引的索引存储结构和检索方式是有关系的。 -在组合索引树中,最底层的叶子节点按照第一列a列从左到右递增排列,但是b列和c列是无序的,b列只有在a列值相等的情况下小范围内递增有序,而c列只能在a,b两列相等的情况下小范围内递增有序。 +#### LRU链表优化 -就像上面的查询,B+树会先比较a列来确定下一步应该搜索的方向,往左还是往右。如果a列相同再比较b列。但是如果查询条件没有a列,B+树就不知道第一步应该从哪个节点查起。 +麻雀虽小五脏俱全,基本`Buffer Pool`里与缓存页相关的组件齐全了。 -可以说创建的idx_abc(a,b,c)索引,相当于创建了(a)、(a,b)(a,b,c)三个索引。、 +![Buffer-Pool-LRU链表优化](images/Database/Buffer-Pool-LRU链表优化.png) -组合索引的最左前缀匹配原则:使用组合索引查询时,mysql会一直向右匹配直至遇到范围查询(>、<、between、like)就停止匹配。 +但是缓存页淘汰这里还有点问题,如果仅仅只是使用`LRU`链表的机制,有两个场景会让**热点数据**被淘汰。 +- **预读机制** + InnoDB使用两种预读算法来提高I/O性能:线性预读(linear read-ahead)和随机预读(randomread-ahead)。 -#### 覆盖索引 +- **全表扫描** -覆盖索引并不是说是索引结构,覆盖索引是一种很常用的优化手段。因为在使用辅助索引的时候,我们只可以拿到主键值,相当于获取数据还需要再根据主键查询主键索引再获取到数据。但是试想下这么一种情况,在上面abc_innodb表中的组合索引查询时,如果我只需要abc字段的,那是不是意味着我们查询到组合索引的叶子节点就可以直接返回了,而不需要回表。这种情况就是覆盖索引。可以看一下执行计划: + 预读机制是指`MySQL`加载数据页时,可能会把它相邻的数据页一并加载进来(局部性原理)。这样会带来一个问题,预读进来的数据页,其实我们没有访问,但是它却排在前面。 -**覆盖索引的情况:** + ![Buffer-Pool-LRU链表优化-全表扫描](images/Database/Buffer-Pool-LRU链表优化-全表扫描.png) -![在这里插入图片描述](images/Database/20201024115203337.png)**未使用到覆盖索引:** + 正常来说,淘汰缓存页时,应该把这个预读的淘汰,结果却把尾部的淘汰了,这是不合理的。我们接着来看第二个场景全表扫描,如果**表数据量大**,大量的数据页会把空闲缓存页用完。最终`LRU`链表前面都是全表扫描的数据,之前频繁访问的热点数据全部到队尾了,淘汰缓存页时就把**热点数据页**给淘汰了。 -![在这里插入图片描述](images/Database/20201024115218222.png) + ![图片](images/Database/819dbbcd31605b3a692576932f25d325.png) + 为了解决上述的问题,我们需要给`LRU`链表做冷热数据分离设计,把`LRU`链表按一定比例,分为冷热区域,热区域称为`young`区域,冷区域称为`old`区域。 -## 失效场景 -**场景一:where语句中包含or时,可能会导致索引失效** +**以7:3为例,young区域70%,old区域30%** -使用or并不是一定会使索引失效,你需要看or左右两边的查询列是否命中相同的索引。 +![图片](images/Database/0f2c2610773fb9fe304c374fc37af4ac.png) -```sql --- 假设user表中的user_id列有索引,age列没有索引 --- 能命中索引 -select * from user where user_id = 1 or user_id = 2; --- 无法命中索引 -select * from user where user_id = 1 or age = 20; --- 假设age列也有索引的话,依然是无法命中索引的 -select * from user where user_id = 1 or age = 20; -``` +如上图所示,数据页第一次加载进缓存页的时候,是先放入冷数据区域的头部,如果1秒后再次访问缓存页,则会移动到热区域的头部。这样就保证了**预读机制**与**全表扫描**加载的数据都在链表队尾。 -可以根据情况尽量使用union all或者in来代替,这两个语句的执行效率也比or好些。 +- `young`区域其实还可以做一个小优化,为了防止`young`区域节点频繁移动到表头 +- `young`区域前面`1/4`被访问不会移动到链表头部,只有后面的`3/4`被访问了才会 +记住是按照某个比例将`LRU`链表分成两部分,不是某些节点固定是`young`区域的,某些节点固定是`old`区域的,随着程序的运行,某个节点所属的区域也可能发生变化。 -**场景二:where语句中索引列使用了负向查询,可能会导致索引失效** +- InnoDB在LRU列表中引入了midpoint参数。新读取的页并不会直接放在LRU列表的首部,而是放在LRU列表的midpoint位置,即 innodb_old_blocks_pct这个点的设置。默认是37%,最小是5,最大是95;如果内存比较大的话,可以将这个数值调低,通常会调成20,也就是说20%的是冷数据块。目的是为了保护热区数据不被刷出内存。 +- InnoDB还引入了innodb_old_blocks_time参数,控制成为热数据的所需时间,默认是1000ms,也就是1s,也就是数据在1s内没有被刷走,就调入热区。 -负向查询包括:NOT、!=、<>、!<、!>、NOT IN、NOT LIKE等。其实负向查询并不绝对会索引失效,这要看MySQL优化器的判断,全表扫描或者走索引哪个成本低了。 +### Change Buffer -**场景三:索引字段可以为null,使用is null或is not null时,可能会导致索引失效** +**可变缓冲区(Change Buffer)**存在于内存中,当对辅助索引(secondary index) 进行DML操作时,Buffer Pool没有其相应的Page,会将这些变更缓存到Change Buffer中。当Change Buffer里的Page被read的时候,会被合并到Buffer Pool中。**当脏页超过一定比例时,会将其flush磁盘中。** Change Buffer的内存默认占Buffer Pool的25%。 -其实单个索引字段,使用is null或is not null时,是可以命中索引的。 +原本修改Buffer Pool不存在的Page需要先从磁盘读取(一次I/O操作)到内存,然后写redo log。引入Change Buffer后,会先缓存在Change Buffer,然后写redo log。**由于Change Buffer的存在,避免了从磁盘读取辅助索引到缓冲池所需的大量随机访问I/O。** -**场景四:在索引列上使用内置函数,一定会导致索引失效** +**应用场景** -比如下面语句中索引列login_time上使用了函数,会索引失效: +**① 适合写多读少场景** -```sql -select * from user where DATE_ADD(login_time, INTERVAL 1 DAY) = 7; -``` +对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 Change Buffer 的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。 +**② 不适合写后立即查询场景** +写入之后马上会做查询,那么即使满足了条件,将更新先记录在 Change Buffer,但之后由于马上要访问这个数据页,会立即触发 merge 过程。这样随机访问 I/O 的次数不会减少,反而增加了 Change Buffer 的维护代价。所以,对于这种业务模式来说,Change Buffer 反而起到了副作用。 -**场景五:隐式类型转换导致的索引失效** -如下面语句中索引列user_id为varchar类型,不会命中索引: -```mysql -select * from user where user_id = 12; -``` +### 自适应哈希索引 +**自适应哈希索引(Adaptive Hash Index)**对InnoDB在Buffer Pool的查询有很大的优化。针对Buffer Pool中**热点页数据**,构建索引(一般使用索引键的前缀构建哈希索引)。因为Hash索引的**等值查询**效率远高于B+Tree,所以当查询命中Hash,就能很快返回结果,不用再去遍历B+Tree。 -**场景六:对索引列进行运算,一定会导致索引失效** -运算如+,-,\*,/等,如下: +### Log Buffer -```mysql -select * from user where age - 1 = 10; -``` +**日志缓冲区(Log Buffer)**用来存储那些即将被**刷入到磁盘**文件中的日志(如RedoLog)。默认为16MB。参数设置如下: -优化的话,要把运算放在值上,或者在应用程序中直接算好,比如: +- `innodb_log_buffer_size`:设置大小 +- `innodb_flush_log_at_trx_commit`:控制如何将日志缓冲区的内容写入并刷新到磁盘 + - `0`:日志每秒刷新到磁盘。 未刷新日志的事务会在mysql崩溃中丢失 + - `1`:为默认值。每次提交事务时,写入并刷新日志到磁盘 + - `2`:每次提交事务后写入日志,并每秒刷新一次磁盘。 未刷新日志的事务可能会在mysql崩溃中丢失 +- `innodb_flush_log_at_timeout`:控制日志刷新频率。每几秒刷新日志,取值范围 [1,2700] (second) -```sql -select * from user where age = 10 - 1; -``` +## 磁盘结构 -**场景七:like通配符可能会导致索引失效** +InnoDB磁盘主要包含Tablespaces,InnoDB Data Dictionary,Doublewrite Buffer、Redo Log和Undo Logs。 -like查询以%开头时,会导致索引失效。解决办法有两种: +### 表空间(Tablespaces) -- 将%移到后面,如: +#### The System Tablespace -```sql -select * from user where `name` like '李%'; -``` +**The System Tablespace** 是Doublewrite Buffer和Change buffer的储存区域,也有用户创建的表和索引数据。该空间的数据文件通过参数`innodb_data_file_path`控制,默认值是`ibdata1:12M:autoextend`(文件名为ibdata1,大小略大于12MB,自动扩展)。**8.0之后InnoDB将元数据(以前的.frm文件,存表结构)存在该区域的数据字典中(data dictionary)**。 -- 利用覆盖索引来命中索引: -```sql -select name from user where `name` like '%李%'; -``` +#### File-Per-Table Tablespaces +**File-Per-Table Tablespaces** 默认开启,为每个表都独立建一个.ibd文件。 通过参数`innodb_file_per_tabl` 可以设置关闭,这样的话所有表数据是都存在The System Tablespace的ibdata。 -**场景八:联合索引中,where中索引列违背最左匹配原则,一定会导致索引失效** -当创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。比如下面的语句就不会命中索引: -```sql -select * from t where k2=2; -select * from t where k3=3; -select * from t where k2=2 and k3=3; -``` +#### General Tablespaces -下面的语句只会命中索引(k1): +**General Tablespaces** 是通过`CREATE TABLESPACE`创建的共享表空间。 -```sql -select * from t where k1=1 and k3=3; -``` +#### Undo Tablespaces -# MySQL日志 +**Undo Tablespaces**保存的是undo log ,用于回滚事务。 + 该表空间有rollback segments,**rollback segments**是用于存 **undo log segments**, 而**undo log segments**存的就是undo logs。MySQL启动的时候,默认初始两个undo tablespace。因为sql执行前必须要有rollback segments。而两个undo tablespace才支持**automated truncation of undo**。 -**生产优化** -- 在生产上,建议 innodb_flush_log_at_trx_commit 设置成 1,可以让每次事务的 redo log 都持久化到磁盘上。保证异常重启后,redo log 不丢失 -- 建议 sync_binlog 设置成 1,可以让每次事务的 binlog 都持久化到磁盘上。保证异常重启后,binlog 不丢失 +#### Temporary Tablespaces +InnoDB把 **Temporary Tablespaces**分为两种,**session temporary tablespaces** 和**global temporary tablespace**。 + **session temporary tablespaces**存储的是用户创建的临时表和内部的临时表,一个session最多有两个表空间(用户临时表和内部临时表)。**global temporary tablespace**储存用户临时表的回滚段(rollback segments )。 -**IO性能优化** -- `binlog_group_commit_sync_delay`:表示延迟多少微秒后,再执行 `fsync` -- `binlog_group_commit_sync_no_delay_count`:表示累计多少次后,在调用 `fsync` +### 数据字典(InnoDB Data Dictionary) -当 `MySQL` 出现了 `IO` 的性能问题,可以考虑下面的优化策略: -- 设置 `binlog_group_commit_sync_delay` 和 `binlog_group_commit_sync_no_delay_count`。可以使用故意等待来减少,`binlog` 的写盘次数,没有数据丢失的风险,但是会有客户端响应变慢的风险 -- 设置 `sync_binlog` 设置为 `100~1000` 之间的某个值。这样做存在的风险是可能造成 `binlog` 丢失 -- 设置 `innodb_flush_log_at_trx_commit = 2`,可能会丢数据 +### 双写缓冲区(Doublewrite Buffer) -## 重做日志(redo log) +### 重做日志(Redo Log) -重做日志(redo log)是InnoDB引擎层的日志,用来记录事务操作引起数据的变化,记录的是数据页的物理修改。 +redo log记录的DML操作的日志,可以用来宕机后的数据前滚。(在log buffer的redo log日志会在宕机中丢失) -在MySQL里,如果我们要执行一条更新语句。执行完成之后,数据不会立马写入磁盘,因为这样对磁盘IO的开销比较大。MySQL里面有一种叫做WAL(Write-Ahead Logging),就是先写日志再写磁盘。就是当有一条记录需要更新的时候,InnoDB 会先写redo log 里面,并更新内存,这个时候更新的操作就算完成了。之后,MySQL会在合适的时候将操作记录 flush 到磁盘上面。当然 flush 的条件可能是系统比较空闲,或者是 redo log 空间不足时。redo log 文件的大小是固定的,比如可以是由4个1GB文件组成的集合。 -### 作用 -确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。 +### 撤销日志(Undo Logs) +**undo log**记录数据更改前的快照(感觉就是备份),在数据需要回滚就可以根据undo log恢复。 +那些undo log 记录关于在global temporary tablespace 的用户临时表的回滚信息,不会在回滚中恢复。 -### 写入流程 -为了控制 redo log 的写入策略,innodb_flush_log_at_trx_commit 会有下面 3 中取值: -- **0:每次提交事务只写在 redo log buffer 中** -- **1:每次提交事务持久化到磁盘** -- **2:每次提交事务写到 文件系统的 page cache 中** +# 索引机制 +索引是为了加速对表中数据行的检索而创建的一种分散存储的(不连续的)数据结构,硬盘级的。 +**索引意义**:索引能极大的减少存储引擎需要扫描的数据量,索引可以把随机IO变成顺序IO。索引可以帮助我们在进行分组、排序等操作时,避免使用临时表。正确的创建合适的索引是提升数据库查询性能的基础。 -### 刷盘场景 -redo log 实际的触发 fsync 操作写盘包含以下几个场景: -- **后台每隔 1 秒钟的线程轮询** -- **innodb_flush_log_at_trx_commit 设置成 1 时,事务提交时触发** -- **innodb_log_buffer_size 是设置 redo log 大小的参数**。当 redo log buffer 达到 innodb_log_buffer_size / 2 时,也会触发一次 fsync +**优势** +- **大大减少服务器需要扫描的数据量** +- **可以提高数据检索的效率(将随机I/O变成顺序I/O),降低数据库的I/O成本** +- **通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗** +**劣势** -## 二进制日志(bin log) +- **索引会占据磁盘空间** +- **索引虽然会提高查询效率,但是会降低更新表的效率**。比如每次对表进行增删改操作,MySQL不仅要保存数据,还有保存或者更新对应的索引文件 -`binlog` 用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。`binlog` 是 `mysql`的逻辑日志,并且由 `Server` 层进行记录,使用任何存储引擎的 `mysql` 数据库都会记录 `binlog` 日志。 -- **逻辑日志**:可以简单理解为记录的就是sql语句 。 -- **物理日志**:`mysql` 数据最终是保存在数据页中的,物理日志记录的就是数据页变更 。 -`binlog` 是通过追加的方式进行写入的,可以通过`max_binlog_size` 参数设置每个 `binlog`文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。 +## 数据结构 -### 作用 +### Hash索引 -- 用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步 -- 用于数据库的基于时间点的还原 +![Hash索引](images/Database/Hash索引.png) +**原理** +- 事先将索引通过 hash算法后得到的hash值(即磁盘文件指针)存到hash表中 +- 在进行查询时,将索引通过hash算法,得到hash值,与hash表中的hash值比对。通过磁盘文件指针,只要**一次磁盘IO**就能找到要的值 -### 使用场景 +例如:在第一个表中,要查找col=6的值。hash(6) 得到值,比对hash表,就能得到89。性能非常高。 -在实际应用中, `binlog` 的主要使用场景有两个,分别是 **主从复制** 和 **数据恢复** 。 -- **主从复制** :在 `Master` 端开启 `binlog` ,然后将 `binlog`发送到各个 `Slave` 端, `Slave` 端重放 `binlog` 从而达到主从数据一致 -- **数据恢复** :通过使用 `mysqlbinlog` 工具来恢复数据 +**优点** +- 快速查询:参与索引的字段只要进行Hash运算之后就可以快速定位到该记录,时间复杂度约为1 -### 刷盘时机 +**缺点** -对于 `InnoDB` 存储引擎而言,只有在事务提交时才会记录`biglog` ,此时记录还在内存中,那么 `biglog`是什么时候刷到磁盘中的呢?`mysql` 通过 `sync_binlog` 参数控制 `biglog` 的刷盘时机,取值范围是 `0-N`: +- 哈希索引只包含哈希值和行指针,所以不能用索引中的值来避免读取行 +- 哈希索引数据并不是按照索引值顺序存储的,所以也就无法用于排序和范围查询 +- 哈希索引也不支持部分索引列查询,因为哈希索引始终是使用索引列的全部数据进行哈希计算的 +- 哈希索引只支持等值比较查询,如=,IN(),<=>操作 +- 如果哈希冲突较多,一些索引的维护操作的代价也会更高 -- `sync_binlog=0`:不去强制要求,由系统自行判断何时写入磁盘; -- `sync_binlog=1`:每次 `commit` 的时候都要将 `binlog` 写入磁盘; -- `sync_binlog=N(N>1)`:每N个事务,才会将 `binlog` 写入磁盘。 -从上面可以看出, `sync_binlog` 最安全的是设置是 `1` ,这也是`MySQL 5.7.7`之后版本的默认值。但是设置一个大一些的值可以提升数据库性能,因此实际情况下也可以将值适当调大,牺牲一定的一致性来获取更好的性能。 +### R-Tree索引 +空间数据索引。 -### 日志格式 -`binlog` 日志有三种格式,分别为 `STATMENT` 、 `ROW` 和 `MIXED`。 -在 `MySQL 5.7.7` 之前,默认的格式是 `STATEMENT` , `MySQL 5.7.7` 之后,默认值是 `ROW`。日志格式通过 `binlog-format` 指定。 +### B-Tree索引 -- `STATMENT`:基于`SQL` 语句的复制( `statement-based replication, SBR` ),每一条会修改数据的sql语句会记录到`binlog` 中 。 +**背景**:二叉查找树查询的时间复杂度是O(logN),查找速度最快和比较次数较少。但用于数据库索引,当数据量过大,不可能将所有索引加载进内存,使用二叉树会导致磁盘IO过于频繁,最坏的情况下磁盘IO的次数由树的高度来决定。 -- - 优点:不需要记录每一行的变化,减少了 binlog 日志量,节约了 IO , 从而提高了性能; - - 缺点:在某些情况下会导致主从数据不一致,比如执行sysdate() 、 slepp() 等 。 +B-Tree(平衡多路查找树)对二叉树进行了横向扩展,能很好解决红黑树中遗留的高度问题,使树结构更加**矮胖**,使得一次IO能加载更多关键字,对比在内存中完成,减少了磁盘IO次数,更适用于大型数据库,但是为了保持自平衡,插入或者删除元素都会导致节点发生裂变反应,有时候会非常麻烦。 -- `ROW`:基于行的复制(`row-based replication, RBR` ),不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了 。 +![索引-B树结构](images/Database/索引-B树结构.png) -- - 优点:不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题 ; - - 缺点:会产生大量的日志,尤其是` alter table ` 的时候会让日志暴涨 -- `MIXED`:基于`STATMENT` 和 `ROW` 两种模式的混合复制(`mixed-based replication, MBR` ),一般的复制使用`STATEMENT` 模式保存 `binlog` ,对于 `STATEMENT` 模式无法复制的操作使用 `ROW` 模式保存 `binlog` +**案例分析**:模拟下查找key为10的data的过程 +![B-Tree案例分析](images/Database/B-Tree案例分析.png) -## 回滚日志(undo log) +- 根据根结点指针读取文件目录的根磁盘块1。【磁盘IO操作第**1次**】 +- 磁盘块1存储15,45和三个指针数据。我们发现10<15,因此我们找到指针p1 +- 根据p1指针,我们定位并读取磁盘块2。【磁盘IO操作**2次**】 +- 磁盘块2存储7,12和三个指针数据。我们发现7<10<12,因此我们找到指针p2 +- 根据p2指针,我们定位并读取磁盘块6。【磁盘IO操作**3次**】 +- 磁盘块6中存储8,10。我们找到10,获取10所对应的数据data -### 作用 -保证数据的原子性,保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。 +**存在问题** +- **不支持范围查询的快速查找**。可以用B+Tree解决 -## 错误日志(error log) +- **每行数据量很大时,会导致B-Tree深度较大,进而影响查询效率**。可以用B+Tree解决 -## 慢查询日志(slow query log) -## 一般查询日志(general log) -## 中继日志(relay log) +### B+Tree索引 +B+树是B-树的变体,也是一种多路搜索树。其定义基本与B-树相同,除了: +- 非叶子结点的子树指针与关键字个数相同 +- 非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B-树是开区间) +- 为所有叶子结点增加一个链指针 +- 所有关键字都在叶子结点出现 -# InnoDB +![索引-B+Tree结构](images/Database/索引-B+Tree结构.png) -## 线程 +**案例分析:等值查询** -## 数据页 +假如我们查询值等于9的数据。查询路径磁盘块1->磁盘块2->磁盘块6。 -数据页主要是用来存储表中记录的,它在磁盘中是用双向链表相连的,方便查找,能够非常快速得从一个数据页,定位到另一个数据页。 +![B+树索引-等值查询](images/Database/B+树索引-等值查询.png) -- 写操作时,先将数据写到内存的某个批次中,然后再将该批次的数据一次性刷到磁盘上。如下图所示: +- 第一次磁盘IO:将磁盘块1加载到内存中,在内存中从头遍历比较,9<15,走左路,到磁盘寻址磁盘块2 +- 第二次磁盘IO:将磁盘块2加载到内存中,在内存中从头遍历比较,7<9<12,到磁盘中寻址定位到磁盘块6 +- 第三次磁盘IO:将磁盘块6加载到内存中,在内存中从头遍历比较,在第三个索引中找到9,取出data,如果data存储的行记录,取出data,查询结束。如果存储的是磁盘地址,还需要根据磁盘地址到磁盘中取出数据,查询终止。(这里需要区分的是在InnoDB中Data存储的为行数据,而MyIsam中存储的是磁盘地址。) - ![InnoDB-数据页-写操作](images/Database/InnoDB-数据页-写操作.jpg) -- 读操作时,从磁盘上一次读一批数据,然后加载到内存当中,以后就在内存中操作。如下图所示: - ![InnoDB-数据页-读操作](images/Database/InnoDB-数据页-读操作.jpg) +**案例分析:范围查询** +假如我们想要查找9和26之间的数据。查找路径是磁盘块1->磁盘块2->磁盘块6->磁盘块7。 -磁盘中各数据页的整体结构如下图所示: +![案例分析-B+树-范围查询](images/Database/案例分析-B+树-范围查询.png) -![InnoDB-数据页](images/Database/InnoDB-数据页.jpg) +- 首先查找值等于9的数据,将值等于9的数据缓存到结果集。这一步和前面等值查询流程一样,发生了三次磁盘IO +- 查找到15之后,底层的叶子节点是一个有序列表,我们从磁盘块6,键值9开始向后遍历筛选所有符合筛选条件的数据 +- 第四次磁盘IO:根据磁盘6后继指针到磁盘中寻址定位到磁盘块7,将磁盘7加载到内存中,在内存中从头遍历比较,9<25<26,9<26<=26,将data缓存到结果集。 +- 主键具备唯一性(后面不会有<=26的数据),不需再向后查找,查询终止。将结果集返回给用户 -通常情况下,单个数据页默认的大小是`16kb`。当然,我们也可以通过参数:`innodb_page_size`,来重新设置大小。不过,一般情况下,用它的默认值就够了。单个数据页包含内容如下: -![InnoDB-单个数据页内容](images/Database/InnoDB-单个数据页内容.jpg) -### 文件头部 +**优点** -通过前面介绍的行记录中`下一条记录的位置`和`页目录`,innodb能非常快速的定位某一条记录。但有个前提条件,就是用户记录必须在同一个数据页当中。 +- 单次请求涉及的磁盘IO次数少(出度d大,且非叶子节点不包含表数据,树的高度小) +- 查询效率稳定(任何关键字的查询必须走从根结点到叶子结点,查询路径长度相同) +- 遍历效率高(从符合条件的某个叶子节点开始遍历即可) -如果用户记录非常多,在第一个数据页找不到我们想要的数据,需要到另外一页找该怎么办呢?这时就需要使用`文件头部`了。它里面包含了多个信息,但我只列出了其中4个最关键的信息: +**缺点** -- 页号 -- 上一页页号 -- 下一页页号 -- 页类型 +B+树最大的性能问题在于会产生大量的随机IO,主要存在以下两种情况: -顾名思义,innodb是通过页号、上一页页号和下一页页号来串联不同数据页的。如下图所示: +- 主键不是有序递增的,导致每次插入数据产生大量的数据迁移和空间碎片 +- 即使主键是有序递增的,大量写请求的分布仍是随机的 -![InnoDB-文件头部](images/Database/InnoDB-文件头部.jpg) -不同的数据页之间,通过上一页页号和下一页页号构成了双向链表。这样就能从前向后,一页页查找所有的数据了。此外,页类型也是一个非常重要的字段,它包含了多种类型,其中比较出名的有:数据页、索引页(目录项页)、溢出页、undo日志页等。 +### B*Tree +是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针; -### 页头部 +![索引机制-B星树](images/Database/索引机制-B星树.jpg) -比如一页数据到底保存了多条记录,或者页目录到底使用了多个槽等。这些信息是实时统计,还是事先统计好了,保存到某个地方?为了性能考虑,上面的这些统计数据,当然是先统计好,保存到一个地方。后面需要用到该数据时,再读取出来会更好。这个保存统计数据的地方,就是`页头部`。当然页头部不仅仅只保存:槽的数量、记录条数等信息。它还记录了: +B*树定义了非叶子结点关键字个数至少为(2/3)M,即块的最低使用率为2/3(代替B+树的1/2)。 -- 已删除记录所占的字节数 -- 最后插入记录的位置 -- 最大事务id -- 索引id -- 索引层级 +**B+树的分裂** +当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针。 +**B*树的分裂** -### 最大和最小记录 +当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;所以,B*树分配新结点的概率比B+树要低,空间使用率更高。 -在一个数据页当中,如果存在多条用户记录,它们是通过`下一条记录的位置`相连的。不过有个问题:如果才能快速找到最大的记录和最小的记录呢?这就需要在保存用户记录的同时,也保存最大和最小记录了。最大记录保存到Supremum记录中。最小记录保存在Infimum记录中。 -在保存用户记录时,数据库会自动创建两条额外的记录:Supremum 和 Infimum。它们之间的关系,如下图所示: -![InnoDB-最大和最小记录](images/Database/InnoDB-最大和最小记录.jpg) +## 索引类型 -从图中可以看出用户数据是从最小记录开始,通过下一条记录的位置,从小到大,一步步查找,最后找到最大记录为止。 +### 普通索引 +**单列索引是最基本的索引,它没有任何限制,允许在定义索引的列中插入重复值和空值。** +```sql +-- 直接创建索引 +CREATE INDEX index_name ON table_name(col_name); +-- 修改表结构的方式添加索引 +ALTER TABLE table_name ADD INDEX index_name(col_name); -### 用户记录 +-- 创建表的时候同时创建索引 +CREATE TABLE `news` ( + `id` int(11) NOT NULL AUTO_INCREMENT , + `title` varchar(255) NOT NULL , + `content` varchar(255) NULL , + `time` varchar(20) NULL DEFAULT NULL , + PRIMARY KEY (`id`), + INDEX index_name (title(255)) +) -对于新申请的数据页,用户记录是空的。当插入数据时,innodb会将一部分`空闲空间`分配给用户记录。用户记录是innodb的重中之重,我们平时保存到数据库中的数据,就存储在它里面。其实在innodb支持的数据行格式有四种: +-- 删除索引 +DROP INDEX index_name ON table_name; +-- 或 +alter table `表名` drop index 索引名; +``` -- compact行格式 -- redundant行格式 -- dynamic行格式 -- compressed行格式 +### 唯一索引 -以compact行格式为例: +**索引列中的值必须是唯一的,但是允许为空值(只允许存在一条空值)。** + +```mysql +-- 创建单个索引 +CREATE UNIQUE INDEX index_name ON table_name(col_name); +-- 创建多个索引 +CREATE UNIQUE INDEX index_name on table_name(col_name,...); + +-- 修改表结构 +-- 单个 +ALTER TABLE table_name ADD UNIQUE index index_name(col_name); +-- 多个 +ALTER TABLE table_name ADD UNIQUE index index_name(col_name,...); +``` + + + +### 主键索引 + +**索引列中的值必须是唯一的,不允许有空值。** + +```sql +-- 主键索引(创建表时添加) +CREATE TABLE `news` ( + `id` int(11) NOT NULL AUTO_INCREMENT , + `title` varchar(255) NOT NULL , + `content` varchar(255) NULL , + `time` varchar(20) NULL DEFAULT NULL , + PRIMARY KEY (`id`) +) + +-- 主键索引(创建表后添加) +CREATE TABLE `order` ( + `orderId` varchar(36) NOT NULL, + `productId` varchar(36) NOT NULL , + `time` varchar(20) NULL DEFAULT NULL +) +alter table `order` add primary key(`orderId`); +``` + + + +### 组合索引 + +**在多个字段上创建的索引**。组合索引遵守“**最左前缀**”原则,即在查询条件中使用了复合索引的第一个字段,索引才会被使用。因此,在复合索引中索引列的顺序至关重要。 + +```sql +-- 创建一个复合索引 +create index index_name on table_name(col_name1,col_name2,...); + +-- 修改表结构的方式添加索引 +alter table table_name add index index_name(col_name,col_name2,...); +``` + + + +### 全文索引 + +**只能在文本类型 `CHAR`、`VARCHAR`、`TEXT` 类型字段上创建全文索引**。字段长度比较大时,如果创建普通索引,在进行like模糊查询时效率比较低,这时可以创建全文索引。 MyISAM和InnoDB中都可以使用全文索引。 + +```mysql +-- 创建表的适合添加全文索引 +CREATE TABLE `news` ( + `id` int(11) NOT NULL AUTO_INCREMENT , + `title` varchar(255) NOT NULL , + `content` text NOT NULL , + `time` varchar(20) NULL DEFAULT NULL , + PRIMARY KEY (`id`), + FULLTEXT (content) +) + +-- 修改表结构添加全文索引 +ALTER TABLE table_name ADD FULLTEXT index_fulltext_content(col_name); +``` + + + +## 索引实现 + +### MyIsam索引 + +MyISAM的数据文件和索引文件是分开存储的。MyISAM使用B+树构建索引树时,叶子节点中存储的键值为索引列的值,数据为索引所在行的磁盘地址。 + +#### 主键索引 + +![在这里插入图片描述](images/Database/20201024114325883.png) + +表user的索引存储在索引文件`user.MYI`中,数据文件存储在数据文件 `user.MYD`中。简单分析下查询时的磁盘IO情况: + + + +**场景一:根据主键等值查询数据** +![在这里插入图片描述](images/Database/20201024114404727.png) + +- 先在主键树中从根节点开始检索,将根节点加载到内存,比较28<75,走左路。(1次磁盘IO) +- 将左子树节点加载到内存中,比较16<28<47,向下检索。(1次磁盘IO) +- 检索到叶节点,将节点加载到内存中遍历,比较16<28,18<28,28=28。查找到值等于30的索引项。(1次磁盘IO) +- 从索引项中获取磁盘地址,然后到数据文件user.MYD中获取对应整行记录。(1次磁盘IO) +- 将记录返给客户端。 + +**磁盘IO次数:3次索引检索+记录数据检索。** + + + +**场景二:根据主键范围查询数据** + +![在这里插入图片描述](images/Database/20201024114510253.png) + +- 先在主键树中从根节点开始检索,将根节点加载到内存,比较28<75,走左路。(1次磁盘IO) +- 将左子树节点加载到内存中,比较16<28<47,向下检索。(1次磁盘IO) +- 检索到叶节点,将节点加载到内存中遍历比较16<28,18<28,28=28<47。查找到值等于28的索引项。 + - 根据磁盘地址从数据文件中获取行记录缓存到结果集中。(1次磁盘IO) + - 我们的查询语句时范围查找,需要向后遍历底层叶子链表,直至到达最后一个不满足筛选条件。 + +- 向后遍历底层叶子链表,将下一个节点加载到内存中,遍历比较,28<47=47,根据磁盘地址从数据文件中获取行记录缓存到结果集中。(1次磁盘IO) +- 最后得到两条符合筛选条件,将查询结果集返给客户端。 + +**磁盘IO次数:4次索引检索+记录数据检索。** + +**备注:**以上分析仅供参考,MyISAM在查询时,会将索引节点缓存在MySQL缓存中,而数据缓存依赖于操作系统自身的缓存,所以并不是每次都是走的磁盘,这里只是为了分析索引的使用过程。 + + + +#### 辅助索引 + +在 MyISAM 中,辅助索引和主键索引的结构是一样的,没有任何区别,叶子节点的数据存储的都是行记录的磁盘地址。只是主键索引的键值是唯一的,而辅助索引的键值可以重复。查询数据时,由于辅助索引的键值不唯一,可能存在多个拥有相同的记录,所以即使是等值查询,也需要按照范围查询的方式在辅助索引树中检索数据。 + + +### InnoDB索引 + +#### 主键索引 + +每个InnoDB表都有一个主键索引(聚簇索引) ,聚簇索引使用B+树构建,叶子节点存储的数据是整行记录。一般情况下,聚簇索引等同于主键索引,当一个表没有创建主键索引时,InnoDB会自动创建一个ROWID字段来构建聚簇索引。InnoDB创建索引的具体规则如下: + +- 在表上定义主键PRIMARY KEY,InnoDB将主键索引用作聚簇索引 +- 如果表没有定义主键,InnoDB会选择第一个不为NULL的唯一索引列用作聚簇索引 +- 如果以上两个都没有,InnoDB 会使用一个6 字节长整型的隐式字段 ROWID字段构建聚簇索引。该ROWID字段会在插入新行时自动递增 + +除聚簇索引之外的所有索引都称为辅助索引。在中InnoDB,辅助索引中的叶子节点存储的数据是该行的主键值都。 在检索时,InnoDB使用此主键值在聚簇索引中搜索行记录。 +主键索引的叶子节点会存储数据行,辅助索引只会存储主键值。 + +![在这里插入图片描述](images/Database/202010241146330.png) + + + +**场景一:等值查询数据** + +- 先在主键树中从根节点开始检索,将根节点加载到内存,比较28<75,走左路。(1次磁盘IO) +- 将左子树节点加载到内存中,比较16<28<47,向下检索。(1次磁盘IO) +- 检索到叶节点,将节点加载到内存中遍历,比较16<28,18<28,28=28。查找到值等于28的索引项,直接可以获取整行数据。将改记录返回给客户端。(1次磁盘IO) + +**磁盘IO数量:3次。** + +![在这里插入图片描述](images/Database/20201024114716460.png) + + + +#### 辅助索引 -![InnoDB-compact行格式](images/Database/InnoDB-compact行格式.jpg) +除聚簇索引之外的所有索引都称为辅助索引,InnoDB的辅助索引只会存储主键值而非磁盘地址。以表user_innodb的age列为例,age索引的索引结果如下图: -一条用户记录主要包含三部分内容: +![在这里插入图片描述](images/Database/20201024114750255.png) -- 记录额外信息:它包含了变长字段、null值列表和记录头信息 -- 隐藏列:它包含了行id、事务id和回滚点 -- 真正的数据列:包含真正的用户数据,可以有很多列 +底层叶子节点的按照(age,id)的顺序排序,先按照age列从小到大排序,age列相同时按照id列从小到大排序。使用辅助索引需要检索两遍索引:首先检索辅助索引获得主键,然后使用主键到主索引中检索获得记录。 -#### 额外信息 +**场景一:等值查询的情况** -额外信息并非真正的用户数据,它是为了辅助存数据用的。 +```sql +select * from t_user_innodb where age=19; +``` -- **变长字段列表** +![在这里插入图片描述](images/Database/2020102411481097.png) - 有些数据如果直接存会有问题,比如:如果某个字段是varchar或text类型,它的长度不固定,可以根据存入数据的长度不同,而随之变化。如果不在一个地方记录数据真正的长度,innodb很可能不知道要分配多少空间。假如都按某个固定长度分配空间,但实际数据又没占多少空间,岂不是会浪费?所以,需要在变长字段中记录某个变长字段占用的字节数,方便按需分配空间。 +根据在辅助索引树中获取的主键id,到主键索引树检索数据的过程称为**回表**查询。 -- **null值列表** +**磁盘IO数:辅助索引3次+获取记录回表3次** - 数据库中有些字段的值允许为null,如果把每个字段的null值,都保存到用户记录中,显然有些浪费存储空间。有没有办法只简单的标记一下,不存储实际的null值呢?答案:将为null的字段保存到null值列表。在列表中用二进制的值1,表示该字段允许为null,用0表示不允许为null。它只占用了1位,就能表示某个字符是否为null,确实可以节省很多存储空间。 -- **记录头信息** - 记录头信息用于描述一些特殊的属性。它主要包含: +#### 组合索引 - - deleted_flag:即删除标记,用于标记该记录是否被删除了 - - min_rec_flag:即最小目录标记,它是非叶子节点中的最小目录标记 - - n_owned:即拥有的记录数,记录该组索引记录的条数 - - heap_no:即堆上的位置,它表示当前记录在堆上的位置 - - record_type:即记录类型,其中0表示普通记录,1表示非叶子节点,2表示Infrimum记录, 3表示Supremum记录 - - next_record:即下一条记录的位置 +还是以自己创建的一个表为例:表 abc_innodb,id为主键索引,创建了一个联合索引idx_abc(a,b,c)。 +```sql +CREATE TABLE `abc_innodb` +( + `id` int(11) NOT NULL AUTO_INCREMENT, + `a` int(11) DEFAULT NULL, + `b` int(11) DEFAULT NULL, + `c` varchar(10) DEFAULT NULL, + `d` varchar(10) DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_abc` (`a`, `b`, `c`) +) ENGINE = InnoDB; +``` +组合索引的数据结构: -#### 隐藏列 +![在这里插入图片描述](images/Database/20201024114900213.png) -数据库在保存一条用户记录时,会自动创建一些隐藏列。如下图所示: +**组合索引的查询过程:** -![InnoDB-隐藏列](images/Database/InnoDB-隐藏列.jpg) +```sql +select * from abc_innodb where a = 13 and b = 16 and c = 4; +``` -目前innodb自动创建的隐藏列有三种: +![在这里插入图片描述](images/Database/20201024115012887.png) -- db_row_id,即行id,它是一条记录的唯一标识。 -- db_trx_id,即事务id,它是事务的唯一标识。 -- db_roll_ptr,即回滚点,它用于事务回滚。 +**最左匹配原则** -如果表中有主键,则用主键做行id,无需额外创建。如果表中没有主键,假如有不为null的unique唯一键,则用它做为行id,同样无需额外创建。如果表中既没有主键,又没有唯一键,则数据库会自动创建行id。也就是说在innodb中,隐藏列中`事务id`和`回滚点`是一定会被创建的,但行id要根据实际情况决定。 +最左前缀匹配原则和联合索引的索引存储结构和检索方式是有关系的。 +在组合索引树中,最底层的叶子节点按照第一列a列从左到右递增排列,但是b列和c列是无序的,b列只有在a列值相等的情况下小范围内递增有序,而c列只能在a,b两列相等的情况下小范围内递增有序。 +就像上面的查询,B+树会先比较a列来确定下一步应该搜索的方向,往左还是往右。如果a列相同再比较b列。但是如果查询条件没有a列,B+树就不知道第一步应该从哪个节点查起。 -#### 真正数据列 +可以说创建的idx_abc(a,b,c)索引,相当于创建了(a)、(a,b)(a,b,c)三个索引。、 -真正的数据列中存储了用户的真实数据,它可以包含很多列的数据。 +组合索引的最左前缀匹配原则:使用组合索引查询时,mysql会一直向右匹配直至遇到范围查询(>、<、between、like)就停止匹配。 -### 页目录 +#### 覆盖索引 -从上面可以看出,如果我们要查询某条记录的话,数据库会从最小记录开始,一条条查找所有记录。如果中途找到了,则直接返回该记录。如果一直找到最大记录,还没有找到想要的记录,则返回空。 +覆盖索引并不是说是索引结构,覆盖索引是一种很常用的优化手段。因为在使用辅助索引的时候,我们只可以拿到主键值,相当于获取数据还需要再根据主键查询主键索引再获取到数据。但是试想下这么一种情况,在上面abc_innodb表中的组合索引查询时,如果我只需要abc字段的,那是不是意味着我们查询到组合索引的叶子节点就可以直接返回了,而不需要回表。这种情况就是覆盖索引。可以看一下执行计划: -但效率会不会有点低?这不是要对整页用户数据进行扫描吗? +**覆盖索引的情况:** -这就需要使用`页目录`了。说白了,就是把一页用户记录分为若干组,每一组的最大记录都保存到一个地方,这个地方就是`页目录`。每一组的最大记录叫做`槽`。由此可见,页目录是有多个槽组成的。所下图所示: +![在这里插入图片描述](images/Database/20201024115203337.png)**未使用到覆盖索引:** -![InnoDB-页目录](images/Database/InnoDB-页目录.jpg) +![在这里插入图片描述](images/Database/20201024115218222.png) -假设一页的数据分为4组,这样在页目录中,就对应了4个槽,每个槽中都保存了该组数据的最大值。这样就能通过二分查找,比较槽中的记录跟需要找到的记录的大小。如果用户需要查找的记录,小于当前槽中的记录,则向上查找上一个槽。如果用户需要查找的记录,大于当前槽中的记录,则向下查找下一个槽。如此一来,就能通过二分查找,快速的定位需要查找的记录了。 +## 失效场景 -### 文件尾部 +**场景一:where语句中包含or时,可能会导致索引失效** -数据库的数据是以数据页为单位,加载到内存中,如果数据有更新的话,需要刷新到磁盘上。但如果某一天比较倒霉,程序在刷新到磁盘的过程中,出现了异常,比如:进程被kill掉了,或者服务器被重启了。这时候数据可能只刷新了一部分,如何判断上次刷盘的数据是完整的呢?这就需要用到`文件尾部`。它里面记录了页面的`校验和`。 +使用or并不是一定会使索引失效,你需要看or左右两边的查询列是否命中相同的索引。 -在数据刷新到磁盘之前,会先计算一个页面的校验和。后面如果数据有更新的话,会计算一个新值。文件头部中也会记录这个校验和,由于文件头部在前面,会先被刷新到磁盘上。 +```sql +-- 假设user表中的user_id列有索引,age列没有索引 +-- 能命中索引 +select * from user where user_id = 1 or user_id = 2; +-- 无法命中索引 +select * from user where user_id = 1 or age = 20; +-- 假设age列也有索引的话,依然是无法命中索引的 +select * from user where user_id = 1 or age = 20; +``` -接下来,刷新用户记录到磁盘的时候,假设刷新了一部分,恰好程序出现异常了。这时,文件尾部的校验和,还是一个旧值。数据库会去校验,文件尾部的校验和,不等于文件头部的新值,说明该数据页的数据是不完整的。 +可以根据情况尽量使用union all或者in来代替,这两个语句的执行效率也比or好些。 -## Buffer Pool +**场景二:where语句中索引列使用了负向查询,可能会导致索引失效** -`InnoDB`为了解决磁盘`IO`问题,`MySQL`需要申请一块内存空间,这块内存空间称为`Buffer Pool`。 +负向查询包括:NOT、!=、<>、!<、!>、NOT IN、NOT LIKE等。其实负向查询并不绝对会索引失效,这要看MySQL优化器的判断,全表扫描或者走索引哪个成本低了。 -![Buffer-Pool](images/Database/Buffer-Pool.png) -### 缓存页 -`Buffer Pool`申请下来后,`Buffer Pool`里面放什么,要怎么规划? +**场景三:索引字段可以为null,使用is null或is not null时,可能会导致索引失效** -`MySQL`数据是以页为单位,每页默认`16KB`,称为数据页,在`Buffer Pool`里面会划分出若干**个缓存页**与数据页对应。 +其实单个索引字段,使用is null或is not null时,是可以命中索引的。 -![Buffer-Pool-缓存页](images/Database/Buffer-Pool-缓存页.png) +**场景四:在索引列上使用内置函数,一定会导致索引失效** -### 描述数据 +比如下面语句中索引列login_time上使用了函数,会索引失效: -如何知道缓存页对应那个数据页呢? +```sql +select * from user where DATE_ADD(login_time, INTERVAL 1 DAY) = 7; +``` -所以还需要缓存页的元数据信息,可以称为**描述数据**,它与缓存页一一对应,包含一些所属表空间、数据页的编号、`Buffer Pool`中的地址等等。 -![Buffer-Pool-描述数据](images/Database/Buffer-Pool-描述数据.png) -后续对数据的增删改查都是在`Buffer Pool`里操作 +**场景五:隐式类型转换导致的索引失效** -- 查询:从磁盘加载到缓存,后续直接查缓存 -- 插入:直接写入缓存 -- 更新删除:缓存中存在直接更新,不存在加载数据页到缓存更新 +如下面语句中索引列user_id为varchar类型,不会命中索引: +```mysql +select * from user where user_id = 12; +``` -`MySQL`宕机数据不就全丢了吗? -`InnoDB`提供了`WAL`技术(Write-Ahead Logging),通过`redo log`让`MySQL`拥有了崩溃恢复能力。再配合空闲时,会有异步线程做缓存页刷盘,保证数据的持久性与完整性。 +**场景六:对索引列进行运算,一定会导致索引失效** -![Buffer-Pool-Write-Ahead-Logging](images/Database/Buffer-Pool-Write-Ahead-Logging.png) +运算如+,-,\*,/等,如下: -直接更新数据的缓存页称为**脏页**,缓存页刷盘后称为**干净页**。 +```mysql +select * from user where age - 1 = 10; +``` +优化的话,要把运算放在值上,或者在应用程序中直接算好,比如: +```sql +select * from user where age = 10 - 1; +``` -### Free链表 -`MySQL`数据库启动时,按照设置的`Buffer Pool`大小,去找操作系统申请一块内存区域,作为`Buffer Pool`(**假设申请了512MB**)。申请完毕后,会按照默认缓存页的`16KB`以及对应的`800Byte`的描述数据,在`Buffer Pool`中划分出来一个一个的缓存页和它们对应的描述数据。 -![Buffer-Pool-Free链表](images/Database/Buffer-Pool-Free链表.png) +**场景七:like通配符可能会导致索引失效** -`MySQL`运行起来后,会不停的执行增删改查,需要从磁盘读取一个一个的数据页放入`Buffer Pool`对应的缓存页里,把数据缓存起来,以后就可以在内存里执行增删改查。 +like查询以%开头时,会导致索引失效。解决办法有两种: -![Buffer-Pool-Free链表-增删改查](images/Database/Buffer-Pool-Free链表-增删改查.png) +- 将%移到后面,如: -但是这个过程必然涉及一个问题,**哪些缓存页是空闲的**? +```sql +select * from user where `name` like '李%'; +``` -为了解决这个问题,我们使用链表结构,把空闲缓存页的**描述数据**放入链表中,这个链表称为`free`链表。针对`free`链表我们要做如下设计: +- 利用覆盖索引来命中索引: -![Buffer-Pool-Free链表设计](images/Database/Buffer-Pool-Free链表设计.png) +```sql +select name from user where `name` like '%李%'; +``` -- 新增`free`基础节点 -- 描述数据添加`free`节点指针 -最终呈现出来的,是由空闲缓存页的**描述数据**组成的`free`链表。 -![Buffer-Pool-Free链表组成](images/Database/Buffer-Pool-Free链表组成.png) +**场景八:联合索引中,where中索引列违背最左匹配原则,一定会导致索引失效** -有了`free`链表之后,我们只需要从`free`链表获取一个**描述数据**,就可以获取到对应的缓存页。 +当创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。比如下面的语句就不会命中索引: -![Buffer-Pool-Free链表-获取描述数据](images/Database/Buffer-Pool-Free链表-获取描述数据.png) +```sql +select * from t where k2=2; +select * from t where k3=3; +select * from t where k2=2 and k3=3; +``` -往**描述数据**与**缓存页**写入数据后,就将该**描述数据**移出`free`链表。 +下面的语句只会命中索引(k1): +```sql +select * from t where k1=1 and k3=3; +``` -### 缓存页哈希表 -查询数据时,如何在`Buffer Pool`里快速定位到对应的缓存页呢?难道需要一个**非空闲的描述数据**链表,再通过**表空间号+数据页编号**遍历查找吗?这样做也可以实现,但是效率不太高,时间复杂度是`O(N)`。 +# MySQL日志 -![Buffer-Pool-缓存页哈希表](images/Database/Buffer-Pool-缓存页哈希表.png) +**生产优化** -所以我们可以换一个结构,使用哈希表来缓存它们间的映射关系,时间复杂度是`O(1)`。 +- 在生产上,建议 innodb_flush_log_at_trx_commit 设置成 1,可以让每次事务的 redo log 都持久化到磁盘上。保证异常重启后,redo log 不丢失 +- 建议 sync_binlog 设置成 1,可以让每次事务的 binlog 都持久化到磁盘上。保证异常重启后,binlog 不丢失 -![Buffer-Pool-缓存页哈希表-复杂度](images/Database/Buffer-Pool-缓存页哈希表-复杂度.png) -**表空间号+数据页号**,作为一个`key`,然后缓存页的地址作为`value`。每次加载数据页到空闲缓存页时,就写入一条映射关系到**缓存页哈希表**中。 -![Buffer-Pool-缓存页哈希表-映射关系](images/Database/Buffer-Pool-缓存页哈希表-映射关系.png) +**IO性能优化** -后续的查询,就可以通过**缓存页哈希表**路由定位了。 +- `binlog_group_commit_sync_delay`:表示延迟多少微秒后,再执行 `fsync` +- `binlog_group_commit_sync_no_delay_count`:表示累计多少次后,在调用 `fsync` -### Flush链表 +当 `MySQL` 出现了 `IO` 的性能问题,可以考虑下面的优化策略: -还记得之前有说过「**空闲时会有异步线程做缓存页刷盘,保证数据的持久性与完整性**」吗? +- 设置 `binlog_group_commit_sync_delay` 和 `binlog_group_commit_sync_no_delay_count`。可以使用故意等待来减少,`binlog` 的写盘次数,没有数据丢失的风险,但是会有客户端响应变慢的风险 +- 设置 `sync_binlog` 设置为 `100~1000` 之间的某个值。这样做存在的风险是可能造成 `binlog` 丢失 +- 设置 `innodb_flush_log_at_trx_commit = 2`,可能会丢数据 -新问题来了,难道每次把`Buffer Pool`里所有的缓存页都刷入磁盘吗?当然不能这样做,磁盘`IO`开销太大了,应该把**脏页**刷入磁盘才对(更新过的缓存页)。可是我们怎么知道,那些缓存页是**脏页**?很简单,参照`free`链表,弄个`flush`链表出来就好了,只要缓存页被更新,就将它的**描述数据**加入`flush`链表。针对`flush`链表我们要做如下设计: -- 新增`flush`基础节点 -- 描述数据添加`flush`节点指针 -![Buffer-Pool-Flush链表](images/Database/Buffer-Pool-Flush链表.png) +**重做日志(redo log)、二进制日志(bin log)、回滚日志(undo log)、错误日志(error log)、慢查询日志(slow query log)、一般查询日志(general log)、中继日志(relay log)**。 -最终呈现出来的,是由更新过数据的缓存页**描述数据**组成的`flush`链表。 -![Buffer-Pool-Flush链表-缓存页](images/Database/Buffer-Pool-Flush链表-缓存页.png) -后续异步线程都从`flush`链表刷缓存页,当`Buffer Pool`内存不足时,也会优先刷`flush`链表里的缓存页。 +## 二进制日志(bin log) +`binlog` 用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。`binlog` 是 `mysql`的逻辑日志,并且由 `Server` 层进行记录,使用任何存储引擎的 `mysql` 数据库都会记录 `binlog` 日志。 +- **逻辑日志**:可以简单理解为记录的就是sql语句 。 +- **物理日志**:`mysql` 数据最终是保存在数据页中的,物理日志记录的就是数据页变更 。 -### LRU链表 +`binlog` 是通过追加的方式进行写入的,可以通过`max_binlog_size` 参数设置每个 `binlog`文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。 -目前看来`Buffer Pool`的功能已经比较完善了。 +### 作用 -![Buffer-Pool-LRU链表](images/Database/Buffer-Pool-LRU链表.png) +- 用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步 +- 用于数据库的基于时间点的还原 -但是仔细思考下,发现还有一个问题没处理。`MySQL`数据库随着系统的运行会不停的把磁盘上的数据页加载到空闲的缓存页里去,因此`free`链表中的空闲缓存页会越来越少,直到没有,最后磁盘的数据页无法加载。 -![Buffer-Pool-LRU链表-无法加载](images/Database/Buffer-Pool-LRU链表-无法加载.png) -为了解决这个问题,我们需要淘汰缓存页,腾出空闲缓存页。可是我们要优先淘汰哪些缓存页?总不能一股脑直接全部淘汰吧?这里就要借鉴`LRU`算法思想,把最近最少使用的缓存页淘汰(命中率低),提供`LRU`链表出来。针对`LRU`链表我们要做如下设计: +### 使用场景 -- 新增`LRU`基础节点 -- 描述数据添加`LRU`节点指针 +在实际应用中, `binlog` 的主要使用场景有两个,分别是 **主从复制** 和 **数据恢复** 。 -![Buffer-Pool-LRU链表-结构](images/Database/Buffer-Pool-LRU链表-结构.png) +- **主从复制** :在 `Master` 端开启 `binlog` ,然后将 `binlog`发送到各个 `Slave` 端, `Slave` 端重放 `binlog` 从而达到主从数据一致 +- **数据恢复** :通过使用 `mysqlbinlog` 工具来恢复数据 -实现思路也很简单,只要是查询或修改过缓存页,就把该缓存页的描述数据放入链表头部,也就说近期访问的数据一定在链表头部。 -![Buffer-Pool-LRU链表-节点](images/Database/Buffer-Pool-LRU链表-节点.png) -当`free`链表为空的时候,直接淘汰`LRU`链表尾部缓存页即可。 +### 刷盘时机 +对于 `InnoDB` 存储引擎而言,只有在事务提交时才会记录`biglog` ,此时记录还在内存中,那么 `biglog`是什么时候刷到磁盘中的呢?`mysql` 通过 `sync_binlog` 参数控制 `biglog` 的刷盘时机,取值范围是 `0-N`: +- `sync_binlog=0`:不去强制要求,由系统自行判断何时写入磁盘; +- `sync_binlog=1`:每次 `commit` 的时候都要将 `binlog` 写入磁盘; +- `sync_binlog=N(N>1)`:每N个事务,才会将 `binlog` 写入磁盘。 -### LRU链表优化 +从上面可以看出, `sync_binlog` 最安全的是设置是 `1` ,这也是`MySQL 5.7.7`之后版本的默认值。但是设置一个大一些的值可以提升数据库性能,因此实际情况下也可以将值适当调大,牺牲一定的一致性来获取更好的性能。 -麻雀虽小五脏俱全,基本`Buffer Pool`里与缓存页相关的组件齐全了。 -![Buffer-Pool-LRU链表优化](images/Database/Buffer-Pool-LRU链表优化.png) -但是缓存页淘汰这里还有点问题,如果仅仅只是使用`LRU`链表的机制,有两个场景会让**热点数据**被淘汰。 +### 日志格式 -- **预读机制** +`binlog` 日志有三种格式,分别为 `STATMENT` 、 `ROW` 和 `MIXED`。 - InnoDB使用两种预读算法来提高I/O性能:线性预读(linear read-ahead)和随机预读(randomread-ahead)。 +在 `MySQL 5.7.7` 之前,默认的格式是 `STATEMENT` , `MySQL 5.7.7` 之后,默认值是 `ROW`。日志格式通过 `binlog-format` 指定。 -- **全表扫描** +- `STATMENT`:基于`SQL` 语句的复制( `statement-based replication, SBR` ),每一条会修改数据的sql语句会记录到`binlog` 中 。 - 预读机制是指`MySQL`加载数据页时,可能会把它相邻的数据页一并加载进来(局部性原理)。这样会带来一个问题,预读进来的数据页,其实我们没有访问,但是它却排在前面。 +- - 优点:不需要记录每一行的变化,减少了 binlog 日志量,节约了 IO , 从而提高了性能; + - 缺点:在某些情况下会导致主从数据不一致,比如执行sysdate() 、 slepp() 等 。 - ![Buffer-Pool-LRU链表优化-全表扫描](images/Database/Buffer-Pool-LRU链表优化-全表扫描.png) +- `ROW`:基于行的复制(`row-based replication, RBR` ),不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了 。 - 正常来说,淘汰缓存页时,应该把这个预读的淘汰,结果却把尾部的淘汰了,这是不合理的。我们接着来看第二个场景全表扫描,如果**表数据量大**,大量的数据页会把空闲缓存页用完。最终`LRU`链表前面都是全表扫描的数据,之前频繁访问的热点数据全部到队尾了,淘汰缓存页时就把**热点数据页**给淘汰了。 +- - 优点:不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题 ; + - 缺点:会产生大量的日志,尤其是` alter table ` 的时候会让日志暴涨 - ![图片](images/Database/819dbbcd31605b3a692576932f25d325.png) +- `MIXED`:基于`STATMENT` 和 `ROW` 两种模式的混合复制(`mixed-based replication, MBR` ),一般的复制使用`STATEMENT` 模式保存 `binlog` ,对于 `STATEMENT` 模式无法复制的操作使用 `ROW` 模式保存 `binlog` - 为了解决上述的问题,我们需要给`LRU`链表做冷热数据分离设计,把`LRU`链表按一定比例,分为冷热区域,热区域称为`young`区域,冷区域称为`old`区域。 +## 重做日志(redo log) -**以7:3为例,young区域70%,old区域30%** +重做日志(redo log)是InnoDB引擎层的日志,用来记录事务操作引起数据的变化,记录的是数据页的物理修改。 -![图片](images/Database/0f2c2610773fb9fe304c374fc37af4ac.png) +在MySQL里,如果我们要执行一条更新语句。执行完成之后,数据不会立马写入磁盘,因为这样对磁盘IO的开销比较大。MySQL里面有一种叫做WAL(Write-Ahead Logging),就是先写日志再写磁盘。就是当有一条记录需要更新的时候,InnoDB 会先写redo log 里面,并更新内存,这个时候更新的操作就算完成了。之后,MySQL会在合适的时候将操作记录 flush 到磁盘上面。当然 flush 的条件可能是系统比较空闲,或者是 redo log 空间不足时。redo log 文件的大小是固定的,比如可以是由4个1GB文件组成的集合。 -如上图所示,数据页第一次加载进缓存页的时候,是先放入冷数据区域的头部,如果1秒后再次访问缓存页,则会移动到热区域的头部。这样就保证了**预读机制**与**全表扫描**加载的数据都在链表队尾。 +### 作用 -- `young`区域其实还可以做一个小优化,为了防止`young`区域节点频繁移动到表头 +确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。 -- `young`区域前面`1/4`被访问不会移动到链表头部,只有后面的`3/4`被访问了才会 -记住是按照某个比例将`LRU`链表分成两部分,不是某些节点固定是`young`区域的,某些节点固定是`old`区域的,随着程序的运行,某个节点所属的区域也可能发生变化。 -- InnoDB在LRU列表中引入了midpoint参数。新读取的页并不会直接放在LRU列表的首部,而是放在LRU列表的midpoint位置,即 innodb_old_blocks_pct这个点的设置。默认是37%,最小是5,最大是95;如果内存比较大的话,可以将这个数值调低,通常会调成20,也就是说20%的是冷数据块。目的是为了保护热区数据不被刷出内存。 -- InnoDB还引入了innodb_old_blocks_time参数,控制成为热数据的所需时间,默认是1000ms,也就是1s,也就是数据在1s内没有被刷走,就调入热区。 +### 写入流程 +为了控制 redo log 的写入策略,innodb_flush_log_at_trx_commit 会有下面 3 中取值: +- **0:每次提交事务只写在 redo log buffer 中** +- **1:每次提交事务持久化到磁盘** +- **2:每次提交事务写到 文件系统的 page cache 中** -## Change Buffer -可变缓冲区(Change Buffer),在内存中,可变缓冲区是InnoDB缓冲池的一部分,在磁盘上,它是系统表空间的一部分,因此即使在数据库重新启动之后,索引更改也会保持缓冲状态。 -可变缓冲区是一种特殊的数据结构,当受影响的页不在缓冲池中时,缓存对辅助索引页的更改。 +### 刷盘场景 +redo log 实际的触发 fsync 操作写盘包含以下几个场景: +- **后台每隔 1 秒钟的线程轮询** +- **innodb_flush_log_at_trx_commit 设置成 1 时,事务提交时触发** +- **innodb_log_buffer_size 是设置 redo log 大小的参数**。当 redo log buffer 达到 innodb_log_buffer_size / 2 时,也会触发一次 fsync -## Log Buffer -日志缓冲区(Log Buffer ),主要保存写到redo log(重放日志)的数据。周期性的将缓冲区内的数据写入redo日志中。将内存中的数据写入磁盘的行为由innodb_log_at_trx_commit 和 innodb_log_at_timeout 调节。较大的redo日志缓冲区允许大型事务在事务提交前不进行写磁盘操作。 -变量:innodb_log_buffer_size (默认 16M) -## InnoDB日志 +## 回滚日志(undo log) -### Redo Log(重做日志) +### 作用 +保证数据的原子性,保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。 -### Undo Log