diff --git a/Database.md b/Database.md index 2c2a092..1774fcb 100644 --- a/Database.md +++ b/Database.md @@ -1303,1103 +1303,980 @@ SELECT * FROM products WHERE id LIKE '3' FOR UPDATE; -# MySQL日志 - -**生产优化** - -- 在生产上,建议 innodb_flush_log_at_trx_commit 设置成 1,可以让每次事务的 redo log 都持久化到磁盘上。保证异常重启后,redo log 不丢失 -- 建议 sync_binlog 设置成 1,可以让每次事务的 binlog 都持久化到磁盘上。保证异常重启后,binlog 不丢失 - - - -**IO性能优化** - -- `binlog_group_commit_sync_delay`:表示延迟多少微秒后,再执行 `fsync` -- `binlog_group_commit_sync_no_delay_count`:表示累计多少次后,在调用 `fsync` - - - -当 `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`,可能会丢数据 - - - -## 重做日志(redo log) - -重做日志(redo log)是InnoDB引擎层的日志,用来记录事务操作引起数据的变化,记录的是数据页的物理修改。 - -在MySQL里,如果我们要执行一条更新语句。执行完成之后,数据不会立马写入磁盘,因为这样对磁盘IO的开销比较大。MySQL里面有一种叫做WAL(Write-Ahead Logging),就是先写日志再写磁盘。就是当有一条记录需要更新的时候,InnoDB 会先写redo log 里面,并更新内存,这个时候更新的操作就算完成了。之后,MySQL会在合适的时候将操作记录 flush 到磁盘上面。当然 flush 的条件可能是系统比较空闲,或者是 redo log 空间不足时。redo log 文件的大小是固定的,比如可以是由4个1GB文件组成的集合。 - -### 作用 - -确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。 - - - -### 写入流程 - -为了控制 redo log 的写入策略,innodb_flush_log_at_trx_commit 会有下面 3 中取值: - -- **0:每次提交事务只写在 redo log buffer 中** -- **1:每次提交事务持久化到磁盘** -- **2:每次提交事务写到 文件系统的 page cache 中** - - - -### 刷盘场景 - -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 +# 索引机制 +索引是为了加速对表中数据行的检索而创建的一种分散存储的(不连续的)数据结构,硬盘级的。 +**索引意义**:索引能极大的减少存储引擎需要扫描的数据量,索引可以把随机IO变成顺序IO。索引可以帮助我们在进行分组、排序等操作时,避免使用临时表。正确的创建合适的索引是提升数据库查询性能的基础。 -## 二进制日志(bin log) - -`binlog` 用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。`binlog` 是 `mysql`的逻辑日志,并且由 `Server` 层进行记录,使用任何存储引擎的 `mysql` 数据库都会记录 `binlog` 日志。 - -- **逻辑日志**:可以简单理解为记录的就是sql语句 。 -- **物理日志**:`mysql` 数据最终是保存在数据页中的,物理日志记录的就是数据页变更 。 - -`binlog` 是通过追加的方式进行写入的,可以通过`max_binlog_size` 参数设置每个 `binlog`文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。 - -### 作用 - -- 用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步 -- 用于数据库的基于时间点的还原 - - - -### 使用场景 - -在实际应用中, `binlog` 的主要使用场景有两个,分别是 **主从复制** 和 **数据恢复** 。 - -- **主从复制** :在 `Master` 端开启 `binlog` ,然后将 `binlog`发送到各个 `Slave` 端, `Slave` 端重放 `binlog` 从而达到主从数据一致 -- **数据恢复** :通过使用 `mysqlbinlog` 工具来恢复数据 - +## 索引结构 +### 平衡多路查找树(B-Tree) -### 刷盘时机 +二叉查找树查询的时间复杂度是O(logN),查找速度最快和比较次数较少。但用于数据库索引,当数据量过大,不可能将所有索引加载进内存,使用二叉树会导致磁盘IO过于频繁,最坏的情况下磁盘IO的次数由树的高度来决定。 -对于 `InnoDB` 存储引擎而言,只有在事务提交时才会记录`biglog` ,此时记录还在内存中,那么 `biglog`是什么时候刷到磁盘中的呢?`mysql` 通过 `sync_binlog` 参数控制 `biglog` 的刷盘时机,取值范围是 `0-N`: +B-Tree对二叉树进行了横向扩展,使树结构更加**矮胖**,使得一次IO能加载更多关键字,对比在内存中完成,减少了磁盘IO次数,更适用于大型数据库,但是为了保持自平衡,插入或者删除元素都会导致节点发生裂变反应,有时候会非常麻烦。 -- `sync_binlog=0`:不去强制要求,由系统自行判断何时写入磁盘; -- `sync_binlog=1`:每次 `commit` 的时候都要将 `binlog` 写入磁盘; -- `sync_binlog=N(N>1)`:每N个事务,才会将 `binlog` 写入磁盘。 +![索引机制-B-树](images/Database/索引机制-B-树.jpg) -从上面可以看出, `sync_binlog` 最安全的是设置是 `1` ,这也是`MySQL 5.7.7`之后版本的默认值。但是设置一个大一些的值可以提升数据库性能,因此实际情况下也可以将值适当调大,牺牲一定的一致性来获取更好的性能。 +B-树的特性: +- 关键字集合分布在整颗树中 +- 任何一个关键字出现且只出现在一个结点中 +- 搜索有可能在非叶子结点结束 +- 其搜索性能等价于在关键字全集内做一次二分查找 +- 自动层次控制 -### 日志格式 - -`binlog` 日志有三种格式,分别为 `STATMENT` 、 `ROW` 和 `MIXED`。 - -在 `MySQL 5.7.7` 之前,默认的格式是 `STATEMENT` , `MySQL 5.7.7` 之后,默认值是 `ROW`。日志格式通过 `binlog-format` 指定。 - -- `STATMENT`:基于`SQL` 语句的复制( `statement-based replication, SBR` ),每一条会修改数据的sql语句会记录到`binlog` 中 。 -- - 优点:不需要记录每一行的变化,减少了 binlog 日志量,节约了 IO , 从而提高了性能; - - 缺点:在某些情况下会导致主从数据不一致,比如执行sysdate() 、 slepp() 等 。 +### B+Tree -- `ROW`:基于行的复制(`row-based replication, RBR` ),不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了 。 +B+树是B-树的变体,也是一种多路搜索树。其定义基本与B-树相同,除了: -- - 优点:不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题 ; - - 缺点:会产生大量的日志,尤其是` alter table ` 的时候会让日志暴涨 +- 非叶子结点的子树指针与关键字个数相同 +- 非叶子结点的子树指针P[i],指向关键字值属于[K[i], K[i+1])的子树(B-树是开区间) +- 为所有叶子结点增加一个链指针 +- 所有关键字都在叶子结点出现 -- `MIXED`:基于`STATMENT` 和 `ROW` 两种模式的混合复制(`mixed-based replication, MBR` ),一般的复制使用`STATEMENT` 模式保存 `binlog` ,对于 `STATEMENT` 模式无法复制的操作使用 `ROW` 模式保存 `binlog` +![索引机制-B+树](images/Database/索引机制-B+树.jpg) +B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找。 +B+的特性: -## 回滚日志(undo log) +- 所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的 +- 不可能在非叶子结点命中 +- 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层 +- 更适合文件索引系统 -### 作用 -保证数据的原子性,保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。 +### B*Tree +是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针; -## 错误日志(error log) +![索引机制-B星树](images/Database/索引机制-B星树.jpg) -## 慢查询日志(slow query log) +B*树定义了非叶子结点关键字个数至少为(2/3)M,即块的最低使用率为2/3(代替B+树的1/2)。 -## 一般查询日志(general log) +**B+树的分裂** -## 中继日志(relay log) +当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向兄弟的指针。 +**B*树的分裂** +当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父结点增加新结点的指针;所以,B*树分配新结点的概率比B+树要低,空间使用率更高。 -# InnoDB - -## 线程 +### 二叉树 -## 数据页 +**特点** -数据页主要是用来存储表中记录的,它在磁盘中是用双向链表相连的,方便查找,能够非常快速得从一个数据页,定位到另一个数据页。 +1. 左子节点值 < 节点值 +2. 右子节点值 > 节点值 +3. 当数据量非常大时,要查找的数据又非常靠后,和没有索引相比,那么二叉树结构的查询优势将非常明显 -- 写操作时,先将数据写到内存的某个批次中,然后再将该批次的数据一次性刷到磁盘上。如下图所示: +**存在问题** - ![InnoDB-数据页-写操作](images/Database/InnoDB-数据页-写操作.jpg) +如下图,可以看出,二叉树出现单边增长时,二叉树变成了“链”,这样查找一个数的时候,速度并没有得到很大的优化。 -- 读操作时,从磁盘上一次读一批数据,然后加载到内存当中,以后就在内存中操作。如下图所示: +![索引结构-二叉树](images/Database/索引结构-二叉树.png) - ![InnoDB-数据页-读操作](images/Database/InnoDB-数据页-读操作.jpg) -磁盘中各数据页的整体结构如下图所示: -![InnoDB-数据页](images/Database/InnoDB-数据页.jpg) +### 红黑树 -通常情况下,单个数据页默认的大小是`16kb`。当然,我们也可以通过参数:`innodb_page_size`,来重新设置大小。不过,一般情况下,用它的默认值就够了。单个数据页包含内容如下: +**特点** -![InnoDB-单个数据页内容](images/Database/InnoDB-单个数据页内容.jpg) +1. 节点是红色或者黑色 +2. 根节点是黑色 +3. 每个叶子的节点都是黑色的空节点(NULL) +4. 每个红色节点的两个子节点都是黑色的 +5. 从任意节点到其每个叶子的所有路径都包含相同的黑色节点 -### 文件头部 +![索引结构-红黑树](images/Database/索引结构-红黑树.png) -通过前面介绍的行记录中`下一条记录的位置`和`页目录`,innodb能非常快速的定位某一条记录。但有个前提条件,就是用户记录必须在同一个数据页当中。 +**存在的问题** -如果用户记录非常多,在第一个数据页找不到我们想要的数据,需要到另外一页找该怎么办呢?这时就需要使用`文件头部`了。它里面包含了多个信息,但我只列出了其中4个最关键的信息: +**红黑树虽然和二叉树相比,一定程度上缓解了单边过长的问题,但是它依旧存储高度问题。**  -- 页号 -- 上一页页号 -- 下一页页号 -- 页类型 +假设现在数据量有100万,那么红黑树的高度大概为 100,0000 = 2^n, n大概为 20。那么,至少要20次的磁盘IO,这样,性能将很受影响。如果数据量更大,IO次数更多,性能损耗更大。**所以红黑树依旧不是最佳方案。** -顾名思义,innodb是通过页号、上一页页号和下一页页号来串联不同数据页的。如下图所示: -![InnoDB-文件头部](images/Database/InnoDB-文件头部.jpg) -不同的数据页之间,通过上一页页号和下一页页号构成了双向链表。这样就能从前向后,一页页查找所有的数据了。此外,页类型也是一个非常重要的字段,它包含了多种类型,其中比较出名的有:数据页、索引页(目录项页)、溢出页、undo日志页等。 +**思考:针对上面的红黑树结构,我们能否优化一下呢?** +上述红黑树默认一个节点就存了一个 (索引+磁盘地址),我们设想一个节点存多个 (索引+磁盘地址),这样就可以降低红黑树的高度了。 **实际上我们设想的这种结构就是 B-Tree**。 -### 页头部 -比如一页数据到底保存了多条记录,或者页目录到底使用了多个槽等。这些信息是实时统计,还是事先统计好了,保存到某个地方?为了性能考虑,上面的这些统计数据,当然是先统计好,保存到一个地方。后面需要用到该数据时,再读取出来会更好。这个保存统计数据的地方,就是`页头部`。当然页头部不仅仅只保存:槽的数量、记录条数等信息。它还记录了: +### Hash -- 已删除记录所占的字节数 -- 最后插入记录的位置 -- 最大事务id -- 索引id -- 索引层级 +**原理** +1. 事先将索引通过 hash算法后得到的hash值(即磁盘文件指针)存到hash表中 +2. 在进行查询时,将索引通过hash算法,得到hash值,与hash表中的hash值比对。通过磁盘文件指针,只要**一次磁盘IO**就能找到要的值 +例如:在第一个表中,要查找col=6的值。hash(6) 得到值,比对hash表,就能得到89。性能非常高。 -### 最大和最小记录 +**存在问题** -在一个数据页当中,如果存在多条用户记录,它们是通过`下一条记录的位置`相连的。不过有个问题:如果才能快速找到最大的记录和最小的记录呢?这就需要在保存用户记录的同时,也保存最大和最小记录了。最大记录保存到Supremum记录中。最小记录保存在Infimum记录中。 + 但是hash表索引存在问题,如果要查询带范围的条件时,hash索引就歇菜了。 -在保存用户记录时,数据库会自动创建两条额外的记录:Supremum 和 Infimum。它们之间的关系,如下图所示: +```mysql +select *from t where col1>=6; +``` -![InnoDB-最大和最小记录](images/Database/InnoDB-最大和最小记录.jpg) + -从图中可以看出用户数据是从最小记录开始,通过下一条记录的位置,从小到大,一步步查找,最后找到最大记录为止。 +### B-Tree +**特点** +B-Tree索引能很好解决红黑树中遗留的高度问题,B-Tree 是一种平衡的多路查找(又称排序)树,在文件系统中和数据库系统有所应用,主要用作文件的索引,其中的B就表示平衡(Balance)。 -### 用户记录 +为了描述B-Tree,首先定义一条数据记录为一个二元组 [key, data],key为记录的键值key,对于不同数据记录,key是互不相同的;**data为数据记录除以key外的数据 (这里指的是聚集索引)**。那么B-Tree是满足下列条件的数据结构: -对于新申请的数据页,用户记录是空的。当插入数据时,innodb会将一部分`空闲空间`分配给用户记录。用户记录是innodb的重中之重,我们平时保存到数据库中的数据,就存储在它里面。其实在innodb支持的数据行格式有四种: +1. d 为大于1的一个正整数,称为BTree的度 +2. h为一个正整数,称为BTree的高度 +3. key和指针互相间隔,节点两端是指针 +4. 叶子节点具有相同的深度,叶子节点的指针为空,节点中数据索引(下图中的key)从左往右递增排列 -- compact行格式 -- redundant行格式 -- dynamic行格式 -- compressed行格式 +**说明**:下图都是以主键索引为例,至于非主键索引(非聚集索引),无非就是data里存的内容不同。 +![索引结构-B-Tree指针](images/Database/索引结构-B-Tree指针.png) +![索引结构-B-Tree](images/Database/索引结构-B-Tree.png) -以compact行格式为例: +**分析** -![InnoDB-compact行格式](images/Database/InnoDB-compact行格式.jpg) +模拟下查找key为29的data的过程: -一条用户记录主要包含三部分内容: +1. 根据根结点指针读取文件目录的根磁盘块1。【磁盘IO操作第**1次**】 +2. 磁盘块1存储17,35和三个指针数据。我们发现17<29<35,因此我们找到指针p2 +3. 根据p2指针,我们定位并读取磁盘块3。【磁盘IO操作**2次**】 +4. 磁盘块3存储26,30和三个指针数据。我们发现26<29<30,因此我们找到指针p2 +5. 根据p2指针,我们定位并读取磁盘块8。【磁盘IO操作**3次**】 +6. 磁盘块8中存储28,29。我们找到29,获取29所对应的数据data -- 记录额外信息:它包含了变长字段、null值列表和记录头信息 -- 隐藏列:它包含了行id、事务id和回滚点 -- 真正的数据列:包含真正的用户数据,可以有很多列 +**存在问题** +1. 比如,下面查询语句,那么不但需要叶子节点>20的值,也需要非叶子节点在右边节点的值。即下图画圈的两部分, **B-Tree似乎在范围查找没有更简便的方法,为了解决这一问题。我们可以用B+Tree。** + ```mysql + select *from t where col1 > 20; + ``` -#### 额外信息 + ![索引结构-B-Tree](images/Database/索引结构-B-Tree问题.png) -额外信息并非真正的用户数据,它是为了辅助存数据用的。 +2. 深度问题 -- **变长字段列表** + 从图上可以看到,每个节点中不仅包含数据的key值,还有data值。而每一个节点的存储空间是有限的(mysql默认设置一个节点的大小为16K),如果data中存放的数据较大时,将会导致每个节点(即一个页)能存储的key的数量(索引的数量)很小,所以当数据量很多,且每行数据量很大的时候,同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。所以引入B+Tree - 有些数据如果直接存会有问题,比如:如果某个字段是varchar或text类型,它的长度不固定,可以根据存入数据的长度不同,而随之变化。如果不在一个地方记录数据真正的长度,innodb很可能不知道要分配多少空间。假如都按某个固定长度分配空间,但实际数据又没占多少空间,岂不是会浪费?所以,需要在变长字段中记录某个变长字段占用的字节数,方便按需分配空间。 -- **null值列表** - 数据库中有些字段的值允许为null,如果把每个字段的null值,都保存到用户记录中,显然有些浪费存储空间。有没有办法只简单的标记一下,不存储实际的null值呢?答案:将为null的字段保存到null值列表。在列表中用二进制的值1,表示该字段允许为null,用0表示不允许为null。它只占用了1位,就能表示某个字符是否为null,确实可以节省很多存储空间。 +### B+Tree -- **记录头信息** +**特点** - 记录头信息用于描述一些特殊的属性。它主要包含: +`B+Tree`是在`B-Tree`基础上的一种优化,使其更适合实现外存储索引结构。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。 - - deleted_flag:即删除标记,用于标记该记录是否被删除了 - - min_rec_flag:即最小目录标记,它是非叶子节点中的最小目录标记 - - n_owned:即拥有的记录数,记录该组索引记录的条数 - - heap_no:即堆上的位置,它表示当前记录在堆上的位置 - - record_type:即记录类型,其中0表示普通记录,1表示非叶子节点,2表示Infrimum记录, 3表示Supremum记录 - - next_record:即下一条记录的位置 +1. 非叶子节点不存储data,只存储索引,可以存放更多索引 +2. 叶子节点不存储指针 +3. 顺序访问指针,提高区间访问性能 +4. 非叶子节点中的索引最终还是会在叶子节点上存储一份,也就是叶子节点会包含非叶子节点上的所有索引 +5. 一个父节点,它的**左侧**子节点都**小于**父节点的值,**右侧**的子节点都**大于等于**父节点的值 +6. 每一层节点从左往右都是递增排列,无论是数值型还是字符型 +**注意**:MySQL索引默认的存储结构使用的就是B+Tree。 +**![索引结构-B+Tree指针](images/Database/索引结构-B+Tree指针.png)** -#### 隐藏列 +![索引结构-B+Tree](images/Database/索引结构-B+Tree.png) -数据库在保存一条用户记录时,会自动创建一些隐藏列。如下图所示: +**剖析**:如上图,在叶子节点上注意是MySQL已经有成双向箭头(原生B+Tree是单向的),而且从左到右是递增顺序的,所以很好的解决了 > 和 < 这类查找问题。 -![InnoDB-隐藏列](images/Database/InnoDB-隐藏列.jpg) +**分析** -目前innodb自动创建的隐藏列有三种: +假如:**以一个高度为3的B+Tree为例**,B+Tree的表都存满了,能存储多少数据? -- db_row_id,即行id,它是一条记录的唯一标识。 -- db_trx_id,即事务id,它是事务的唯一标识。 -- db_roll_ptr,即回滚点,它用于事务回滚。 +**首先,**查看MySQL默认一个节点页的大小: -如果表中有主键,则用主键做行id,无需额外创建。如果表中没有主键,假如有不为null的unique唯一键,则用它做为行id,同样无需额外创建。如果表中既没有主键,又没有唯一键,则数据库会自动创建行id。也就是说在innodb中,隐藏列中`事务id`和`回滚点`是一定会被创建的,但行id要根据实际情况决定。 +```mysql +SHOW GLOBAL STATUS like 'Innodb_page_size'; +``` +如下图:大小为16K。 +![索引结构-B+Tree案例](images/Database/索引结构-B+Tree案例.png) -#### 真正数据列 +然后,假设主键Id为bigint类型,那么长度就是8B,指针在Innodb源码中大小为6B,所以一共就是14B,再假设最后一层,存放的数据data为1k 大小(能存很多内容了),那么: -真正的数据列中存储了用户的真实数据,它可以包含很多列的数据。 +1. 第一层最大节点数为: 16k / (8B + 6B) = 1170 (个) +2. 第二层最大节点数也应为:1170个 +3. 第三层最大节点数为:16k / 1k = 16 (个) +则,一张B+Tree的表最多存放 1170 * 1170 * 16 = 21902400 ≈ 2千万。所以,通过分析,我们可以得出,B+Tree结构的表可以容纳千万数据量的查询。而且**一般来说,MySQL会把 B+Tree 根节点放在内存中**,那只需要**两次磁盘IO(第二层1次,第三层1次)**就行。 +**扩展** -### 页目录 +数据库中的B+Tree索引可以分为聚集索引(clustered index,也叫主键索引)和辅助索引(secondary index,也叫非聚集索引)。上面的B+Tree示例图在数据库中的实现对应的是聚集索引,聚集索引的B+Tree中的叶子节点存放的是整张表的行记录数据(除主键以外的所有数据),辅助索引与聚集索引的区别在于辅助索引的叶子节点并不包含行记录的全部数据,而是存储相应行数据的对应的聚集索引键,即主键。当通过辅助索引来查询数据时,InnoDB存储引擎会遍历辅助索引找到主键,然后再通过主键在聚集索引中找到完整的行记录数据。 -从上面可以看出,如果我们要查询某条记录的话,数据库会从最小记录开始,一条条查找所有记录。如果中途找到了,则直接返回该记录。如果一直找到最大记录,还没有找到想要的记录,则返回空。 -但效率会不会有点低?这不是要对整页用户数据进行扫描吗? -这就需要使用`页目录`了。说白了,就是把一页用户记录分为若干组,每一组的最大记录都保存到一个地方,这个地方就是`页目录`。每一组的最大记录叫做`槽`。由此可见,页目录是有多个槽组成的。所下图所示: -![InnoDB-页目录](images/Database/InnoDB-页目录.jpg) -假设一页的数据分为4组,这样在页目录中,就对应了4个槽,每个槽中都保存了该组数据的最大值。这样就能通过二分查找,比较槽中的记录跟需要找到的记录的大小。如果用户需要查找的记录,小于当前槽中的记录,则向上查找上一个槽。如果用户需要查找的记录,大于当前槽中的记录,则向下查找下一个槽。如此一来,就能通过二分查找,快速的定位需要查找的记录了。 +## 索引类型 +### 普通索引 +**普通索引(单列索引)**:单列索引是最基本的索引,它没有任何限制。 -### 文件尾部 +```sql +-- 直接创建索引 +CREATE INDEX index_name ON table_name(col_name); +-- 修改表结构的方式添加索引 +ALTER TABLE table_name ADD INDEX index_name(col_name); -数据库的数据是以数据页为单位,加载到内存中,如果数据有更新的话,需要刷新到磁盘上。但如果某一天比较倒霉,程序在刷新到磁盘的过程中,出现了异常,比如:进程被kill掉了,或者服务器被重启了。这时候数据可能只刷新了一部分,如何判断上次刷盘的数据是完整的呢?这就需要用到`文件尾部`。它里面记录了页面的`校验和`。 +-- 创建表的时候同时创建索引 +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)) +) -在数据刷新到磁盘之前,会先计算一个页面的校验和。后面如果数据有更新的话,会计算一个新值。文件头部中也会记录这个校验和,由于文件头部在前面,会先被刷新到磁盘上。 +-- 删除索引 +DROP INDEX index_name ON table_name; +-- 或 +alter table `表名` drop index 索引名; +``` -接下来,刷新用户记录到磁盘的时候,假设刷新了一部分,恰好程序出现异常了。这时,文件尾部的校验和,还是一个旧值。数据库会去校验,文件尾部的校验和,不等于文件头部的新值,说明该数据页的数据是不完整的。 +### 唯一索引 -## Buffer Pool +唯一索引和普通索引类似,主要的区别在于,**唯一索引限制列的值必须唯一,但允许存在空值(只允许存在一条空值)**。如果在已经有数据的表上添加唯一性索引的话: -`InnoDB`为了解决磁盘`IO`问题,`MySQL`需要申请一块内存空间,这块内存空间称为`Buffer Pool`。 +```mysql +-- 创建单个索引 +CREATE UNIQUE INDEX index_name ON table_name(col_name); +-- 创建多个索引 +CREATE UNIQUE INDEX index_name on table_name(col_name,...); -![Buffer-Pool](images/Database/Buffer-Pool.png) +-- 修改表结构 +-- 单个 +ALTER TABLE table_name ADD UNIQUE index index_name(col_name); +-- 多个 +ALTER TABLE table_name ADD UNIQUE index index_name(col_name,...); +``` -### 缓存页 -`Buffer Pool`申请下来后,`Buffer Pool`里面放什么,要怎么规划? -`MySQL`数据是以页为单位,每页默认`16KB`,称为数据页,在`Buffer Pool`里面会划分出若干**个缓存页**与数据页对应。 +### 主键索引 -![Buffer-Pool-缓存页](images/Database/Buffer-Pool-缓存页.png) +主键索引是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引: +```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`); +``` -### 描述数据 -如何知道缓存页对应那个数据页呢? -所以还需要缓存页的元数据信息,可以称为**描述数据**,它与缓存页一一对应,包含一些所属表空间、数据页的编号、`Buffer Pool`中的地址等等。 +### 组合索引 -![Buffer-Pool-描述数据](images/Database/Buffer-Pool-描述数据.png) +**复合索引(组合索引)**:复合索引是在多个字段上创建的索引。复合索引遵守“**最左前缀**”原则**,**即在查询条件中使用了复合索引的第一个字段,索引才会被使用。因此,在复合索引中索引列的顺序至关重要。 -后续对数据的增删改查都是在`Buffer Pool`里操作 +```sql +-- 创建一个复合索引 +create index index_name on table_name(col_name1,col_name2,...); -- 查询:从磁盘加载到缓存,后续直接查缓存 -- 插入:直接写入缓存 -- 更新删除:缓存中存在直接更新,不存在加载数据页到缓存更新 +-- 修改表结构的方式添加索引 +alter table table_name add index index_name(col_name,col_name2,...); +``` -`MySQL`宕机数据不就全丢了吗? +### 全文索引 -`InnoDB`提供了`WAL`技术(Write-Ahead Logging),通过`redo log`让`MySQL`拥有了崩溃恢复能力。再配合空闲时,会有异步线程做缓存页刷盘,保证数据的持久性与完整性。 +在一般情况下,模糊查询都是通过 like 的方式进行查询。但是,对于海量数据,这并不是一个好办法,在 like "value%" 可以使用索引,但是对于 like "%value%" 这样的方式,执行全表查询,这在数据量小的表,不存在性能问题,但是对于海量数据,全表扫描是非常可怕的事情,所以 like 进行模糊匹配性能很差。 -![Buffer-Pool-Write-Ahead-Logging](images/Database/Buffer-Pool-Write-Ahead-Logging.png) +这种情况下,需要考虑使用全文搜索的方式进行优化。全文搜索在 MySQL 中是一个 FULLTEXT 类型索引。**FULLTEXT 索引在 MySQL 5.6 版本之后支持 InnoDB,而之前的版本只支持 MyISAM 表**。目前只有char、varchar,text 列上可以创建全文索引。 -直接更新数据的缓存页称为**脏页**,缓存页刷盘后称为**干净页**。 +```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); +``` -### Free链表 -`MySQL`数据库启动时,按照设置的`Buffer Pool`大小,去找操作系统申请一块内存区域,作为`Buffer Pool`(**假设申请了512MB**)。申请完毕后,会按照默认缓存页的`16KB`以及对应的`800Byte`的描述数据,在`Buffer Pool`中划分出来一个一个的缓存页和它们对应的描述数据。 +## 失效场景 -![Buffer-Pool-Free链表](images/Database/Buffer-Pool-Free链表.png) +**场景一:where语句中包含or时,可能会导致索引失效** -`MySQL`运行起来后,会不停的执行增删改查,需要从磁盘读取一个一个的数据页放入`Buffer Pool`对应的缓存页里,把数据缓存起来,以后就可以在内存里执行增删改查。 +使用or并不是一定会使索引失效,你需要看or左右两边的查询列是否命中相同的索引。 -![Buffer-Pool-Free链表-增删改查](images/Database/Buffer-Pool-Free链表-增删改查.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; +``` -但是这个过程必然涉及一个问题,**哪些缓存页是空闲的**? +可以根据情况尽量使用union all或者in来代替,这两个语句的执行效率也比or好些。 -为了解决这个问题,我们使用链表结构,把空闲缓存页的**描述数据**放入链表中,这个链表称为`free`链表。针对`free`链表我们要做如下设计: -![Buffer-Pool-Free链表设计](images/Database/Buffer-Pool-Free链表设计.png) -- 新增`free`基础节点 -- 描述数据添加`free`节点指针 +**场景二:where语句中索引列使用了负向查询,可能会导致索引失效** -最终呈现出来的,是由空闲缓存页的**描述数据**组成的`free`链表。 +负向查询包括:NOT、!=、<>、!<、!>、NOT IN、NOT LIKE等。其实负向查询并不绝对会索引失效,这要看MySQL优化器的判断,全表扫描或者走索引哪个成本低了。 -![Buffer-Pool-Free链表组成](images/Database/Buffer-Pool-Free链表组成.png) -有了`free`链表之后,我们只需要从`free`链表获取一个**描述数据**,就可以获取到对应的缓存页。 -![Buffer-Pool-Free链表-获取描述数据](images/Database/Buffer-Pool-Free链表-获取描述数据.png) +**场景三:索引字段可以为null,使用is null或is not null时,可能会导致索引失效** -往**描述数据**与**缓存页**写入数据后,就将该**描述数据**移出`free`链表。 +其实单个索引字段,使用is null或is not null时,是可以命中索引的。 -### 缓存页哈希表 +**场景四:在索引列上使用内置函数,一定会导致索引失效** -查询数据时,如何在`Buffer Pool`里快速定位到对应的缓存页呢?难道需要一个**非空闲的描述数据**链表,再通过**表空间号+数据页编号**遍历查找吗?这样做也可以实现,但是效率不太高,时间复杂度是`O(N)`。 +比如下面语句中索引列login_time上使用了函数,会索引失效: -![Buffer-Pool-缓存页哈希表](images/Database/Buffer-Pool-缓存页哈希表.png) +```sql +select * from user where DATE_ADD(login_time, INTERVAL 1 DAY) = 7; +``` -所以我们可以换一个结构,使用哈希表来缓存它们间的映射关系,时间复杂度是`O(1)`。 -![Buffer-Pool-缓存页哈希表-复杂度](images/Database/Buffer-Pool-缓存页哈希表-复杂度.png) -**表空间号+数据页号**,作为一个`key`,然后缓存页的地址作为`value`。每次加载数据页到空闲缓存页时,就写入一条映射关系到**缓存页哈希表**中。 +**场景五:隐式类型转换导致的索引失效** -![Buffer-Pool-缓存页哈希表-映射关系](images/Database/Buffer-Pool-缓存页哈希表-映射关系.png) +如下面语句中索引列user_id为varchar类型,不会命中索引: -后续的查询,就可以通过**缓存页哈希表**路由定位了。 +```mysql +select * from user where user_id = 12; +``` -### Flush链表 +**场景六:对索引列进行运算,一定会导致索引失效** -还记得之前有说过「**空闲时会有异步线程做缓存页刷盘,保证数据的持久性与完整性**」吗? +运算如+,-,\*,/等,如下: -新问题来了,难道每次把`Buffer Pool`里所有的缓存页都刷入磁盘吗?当然不能这样做,磁盘`IO`开销太大了,应该把**脏页**刷入磁盘才对(更新过的缓存页)。可是我们怎么知道,那些缓存页是**脏页**?很简单,参照`free`链表,弄个`flush`链表出来就好了,只要缓存页被更新,就将它的**描述数据**加入`flush`链表。针对`flush`链表我们要做如下设计: +```mysql +select * from user where age - 1 = 10; +``` -- 新增`flush`基础节点 -- 描述数据添加`flush`节点指针 +优化的话,要把运算放在值上,或者在应用程序中直接算好,比如: -![Buffer-Pool-Flush链表](images/Database/Buffer-Pool-Flush链表.png) +```sql +select * from user where age = 10 - 1; +``` -最终呈现出来的,是由更新过数据的缓存页**描述数据**组成的`flush`链表。 -![Buffer-Pool-Flush链表-缓存页](images/Database/Buffer-Pool-Flush链表-缓存页.png) -后续异步线程都从`flush`链表刷缓存页,当`Buffer Pool`内存不足时,也会优先刷`flush`链表里的缓存页。 +**场景七:like通配符可能会导致索引失效** +like查询以%开头时,会导致索引失效。解决办法有两种: +- 将%移到后面,如: -### LRU链表 +```sql +select * from user where `name` like '李%'; +``` -目前看来`Buffer Pool`的功能已经比较完善了。 +- 利用覆盖索引来命中索引: -![Buffer-Pool-LRU链表](images/Database/Buffer-Pool-LRU链表.png) +```sql +select name from user where `name` like '%李%'; +``` -但是仔细思考下,发现还有一个问题没处理。`MySQL`数据库随着系统的运行会不停的把磁盘上的数据页加载到空闲的缓存页里去,因此`free`链表中的空闲缓存页会越来越少,直到没有,最后磁盘的数据页无法加载。 -![Buffer-Pool-LRU链表-无法加载](images/Database/Buffer-Pool-LRU链表-无法加载.png) -为了解决这个问题,我们需要淘汰缓存页,腾出空闲缓存页。可是我们要优先淘汰哪些缓存页?总不能一股脑直接全部淘汰吧?这里就要借鉴`LRU`算法思想,把最近最少使用的缓存页淘汰(命中率低),提供`LRU`链表出来。针对`LRU`链表我们要做如下设计: +**场景八:联合索引中,where中索引列违背最左匹配原则,一定会导致索引失效** -- 新增`LRU`基础节点 -- 描述数据添加`LRU`节点指针 +当创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。比如下面的语句就不会命中索引: -![Buffer-Pool-LRU链表-结构](images/Database/Buffer-Pool-LRU链表-结构.png) +```sql +select * from t where k2=2; +select * from t where k3=3; +select * from t where k2=2 and k3=3; +``` -实现思路也很简单,只要是查询或修改过缓存页,就把该缓存页的描述数据放入链表头部,也就说近期访问的数据一定在链表头部。 +下面的语句只会命中索引(k1): -![Buffer-Pool-LRU链表-节点](images/Database/Buffer-Pool-LRU链表-节点.png) +```sql +select * from t where k1=1 and k3=3; +``` -当`free`链表为空的时候,直接淘汰`LRU`链表尾部缓存页即可。 +# MySQL日志 -### LRU链表优化 +**生产优化** -麻雀虽小五脏俱全,基本`Buffer Pool`里与缓存页相关的组件齐全了。 +- 在生产上,建议 innodb_flush_log_at_trx_commit 设置成 1,可以让每次事务的 redo log 都持久化到磁盘上。保证异常重启后,redo log 不丢失 +- 建议 sync_binlog 设置成 1,可以让每次事务的 binlog 都持久化到磁盘上。保证异常重启后,binlog 不丢失 -![Buffer-Pool-LRU链表优化](images/Database/Buffer-Pool-LRU链表优化.png) -但是缓存页淘汰这里还有点问题,如果仅仅只是使用`LRU`链表的机制,有两个场景会让**热点数据**被淘汰。 -- **预读机制** +**IO性能优化** - InnoDB使用两种预读算法来提高I/O性能:线性预读(linear read-ahead)和随机预读(randomread-ahead)。 +- `binlog_group_commit_sync_delay`:表示延迟多少微秒后,再执行 `fsync` +- `binlog_group_commit_sync_no_delay_count`:表示累计多少次后,在调用 `fsync` -- **全表扫描** - 预读机制是指`MySQL`加载数据页时,可能会把它相邻的数据页一并加载进来(局部性原理)。这样会带来一个问题,预读进来的数据页,其实我们没有访问,但是它却排在前面。 - ![Buffer-Pool-LRU链表优化-全表扫描](images/Database/Buffer-Pool-LRU链表优化-全表扫描.png) +当 `MySQL` 出现了 `IO` 的性能问题,可以考虑下面的优化策略: - 正常来说,淘汰缓存页时,应该把这个预读的淘汰,结果却把尾部的淘汰了,这是不合理的。我们接着来看第二个场景全表扫描,如果**表数据量大**,大量的数据页会把空闲缓存页用完。最终`LRU`链表前面都是全表扫描的数据,之前频繁访问的热点数据全部到队尾了,淘汰缓存页时就把**热点数据页**给淘汰了。 +- 设置 `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`,可能会丢数据 - ![图片](images/Database/819dbbcd31605b3a692576932f25d325.png) - 为了解决上述的问题,我们需要给`LRU`链表做冷热数据分离设计,把`LRU`链表按一定比例,分为冷热区域,热区域称为`young`区域,冷区域称为`old`区域。 +## 重做日志(redo log) +重做日志(redo log)是InnoDB引擎层的日志,用来记录事务操作引起数据的变化,记录的是数据页的物理修改。 -**以7:3为例,young区域70%,old区域30%** +在MySQL里,如果我们要执行一条更新语句。执行完成之后,数据不会立马写入磁盘,因为这样对磁盘IO的开销比较大。MySQL里面有一种叫做WAL(Write-Ahead Logging),就是先写日志再写磁盘。就是当有一条记录需要更新的时候,InnoDB 会先写redo log 里面,并更新内存,这个时候更新的操作就算完成了。之后,MySQL会在合适的时候将操作记录 flush 到磁盘上面。当然 flush 的条件可能是系统比较空闲,或者是 redo log 空间不足时。redo log 文件的大小是固定的,比如可以是由4个1GB文件组成的集合。 -![图片](images/Database/0f2c2610773fb9fe304c374fc37af4ac.png) +### 作用 -如上图所示,数据页第一次加载进缓存页的时候,是先放入冷数据区域的头部,如果1秒后再次访问缓存页,则会移动到热区域的头部。这样就保证了**预读机制**与**全表扫描**加载的数据都在链表队尾。 +确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。 -- `young`区域其实还可以做一个小优化,为了防止`young`区域节点频繁移动到表头 -- `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日志缓冲区允许大型事务在事务提交前不进行写磁盘操作。 +## 二进制日志(bin log) -变量:innodb_log_buffer_size (默认 16M) +`binlog` 用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。`binlog` 是 `mysql`的逻辑日志,并且由 `Server` 层进行记录,使用任何存储引擎的 `mysql` 数据库都会记录 `binlog` 日志。 +- **逻辑日志**:可以简单理解为记录的就是sql语句 。 +- **物理日志**:`mysql` 数据最终是保存在数据页中的,物理日志记录的就是数据页变更 。 -## InnoDB日志 +`binlog` 是通过追加的方式进行写入的,可以通过`max_binlog_size` 参数设置每个 `binlog`文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。 -### Redo Log(重做日志) +### 作用 +- 用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步 +- 用于数据库的基于时间点的还原 -### Undo Log +### 使用场景 +在实际应用中, `binlog` 的主要使用场景有两个,分别是 **主从复制** 和 **数据恢复** 。 +- **主从复制** :在 `Master` 端开启 `binlog` ,然后将 `binlog`发送到各个 `Slave` 端, `Slave` 端重放 `binlog` 从而达到主从数据一致 +- **数据恢复** :通过使用 `mysqlbinlog` 工具来恢复数据 -# 数据切分 -## 水平切分 +### 刷盘时机 -水平切分又称为 Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。当一个表的数据不断增多时,Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。 +对于 `InnoDB` 存储引擎而言,只有在事务提交时才会记录`biglog` ,此时记录还在内存中,那么 `biglog`是什么时候刷到磁盘中的呢?`mysql` 通过 `sync_binlog` 参数控制 `biglog` 的刷盘时机,取值范围是 `0-N`: -![img](images/Database/007S8ZIlly1gjjfy33yx2j30fm05zwg9.jpg) +- `sync_binlog=0`:不去强制要求,由系统自行判断何时写入磁盘; +- `sync_binlog=1`:每次 `commit` 的时候都要将 `binlog` 写入磁盘; +- `sync_binlog=N(N>1)`:每N个事务,才会将 `binlog` 写入磁盘。 +从上面可以看出, `sync_binlog` 最安全的是设置是 `1` ,这也是`MySQL 5.7.7`之后版本的默认值。但是设置一个大一些的值可以提升数据库性能,因此实际情况下也可以将值适当调大,牺牲一定的一致性来获取更好的性能。 -## 垂直切分 -垂直切分是将一张表按列分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直气氛将经常被使用的列喝不经常被使用的列切分到不同的表中。在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不通的库中,例如将原来电商数据部署库垂直切分称商品数据库、用户数据库等。 +### 日志格式 -![img](images/Database/007S8ZIlly1gjjfy5yoatj30cy09l776.jpg) +`binlog` 日志有三种格式,分别为 `STATMENT` 、 `ROW` 和 `MIXED`。 +在 `MySQL 5.7.7` 之前,默认的格式是 `STATEMENT` , `MySQL 5.7.7` 之后,默认值是 `ROW`。日志格式通过 `binlog-format` 指定。 +- `STATMENT`:基于`SQL` 语句的复制( `statement-based replication, SBR` ),每一条会修改数据的sql语句会记录到`binlog` 中 。 -## Sharding策略 +- - 优点:不需要记录每一行的变化,减少了 binlog 日志量,节约了 IO , 从而提高了性能; + - 缺点:在某些情况下会导致主从数据不一致,比如执行sysdate() 、 slepp() 等 。 -- 哈希取模:hash(key)%N -- 范围:可以是 ID 范围也可以是时间范围 -- 映射表:使用单独的一个数据库来存储映射关系 +- `ROW`:基于行的复制(`row-based replication, RBR` ),不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了 。 +- - 优点:不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题 ; + - 缺点:会产生大量的日志,尤其是` alter table ` 的时候会让日志暴涨 +- `MIXED`:基于`STATMENT` 和 `ROW` 两种模式的混合复制(`mixed-based replication, MBR` ),一般的复制使用`STATEMENT` 模式保存 `binlog` ,对于 `STATEMENT` 模式无法复制的操作使用 `ROW` 模式保存 `binlog` -## Sharding存在的问题 -- **事务问题**:使用分布式事务来解决,比如 XA 接口 -- **连接**:可以将原来的连接分解成多个单表查询,然后在用户程序中进行连接。 +## 回滚日志(undo log) -- **唯一性** - - 使用全局唯一 ID (GUID) - - 为每个分片指定一个 ID 范围 - - 分布式 ID 生成器(如 Twitter 的 Snowflake 算法) +### 作用 +保证数据的原子性,保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。 -# 索引 -建立索引的目的是加快对表中记录的查找或排序。索引只是提高效率的一个因素,如果你的MySQL有大数据量的表,就需要花时间研究建立最优秀的索引,或优化查询语句。因此应该只为最经常查询和最经常排序的数据列建立索引。MySQL里同一个数据表里的索引总数限制为16个。 +## 错误日志(error log) -**优点** +## 慢查询日志(slow query log) -- 索引大大减小了服务器需要扫描的数据量 -- 索引可以帮助服务器避免排序和临时表 -- 索引可以将随机IO变成顺序IO -- 索引对于InnoDB(对索引支持行级锁)非常重要,因为它可以让查询锁更少的元组 -- 关于InnoDB、索引和锁:InnoDB在二级索引上使用共享锁(读锁),但访问主键索引需要排他锁(写锁) +## 一般查询日志(general log) -**缺点** +## 中继日志(relay log) -- 虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存索引文件 -- 建立索引会占用磁盘空间的索引文件。一般情况这个问题不太严重,但如果你在一个大表上创建了多种组合索引,索引文件的会膨胀很快 -- 如果某个数据列包含许多重复的内容,为它建立索引就没有太大的实际效果 -- 对于非常小的表,大部分情况下简单的全表扫描更高效 +# InnoDB -**索引规范** +## 线程 -- 索引的数量要控制 - - **单张表** 中索引数量不超过 **5** 个 - - **单个索引** 中的字段数不超过 **5** 个(字段超过5个时,实际已经起不到有效过滤数据的作用了) - - 对字符串使⽤ **前缀索引**,前缀索引长度不超过 **8** 个字符,必要时可添加伪列并建立索引 -- 禁止在 **更新十分频繁**、**区分度不高** 的属性上建立索引 +## 数据页 - - 更新会变更B+树,更新频繁的字段建立索引会大大降低数据库性能 - - “性别”这种区分度不大的属性,建立索引是没有意义的,其不能有效过滤数据,性能与全表扫描类似 +数据页主要是用来存储表中记录的,它在磁盘中是用双向链表相连的,方便查找,能够非常快速得从一个数据页,定位到另一个数据页。 -- 不在索引列进行 **数学运算** 和 **函数运算** +- 写操作时,先将数据写到内存的某个批次中,然后再将该批次的数据一次性刷到磁盘上。如下图所示: -- 建立组合索引:必须 **把区分度高的字段放在前面**(能够更加有效的过滤数据) + ![InnoDB-数据页-写操作](images/Database/InnoDB-数据页-写操作.jpg) -- 重要的SQL必须被索引,比如: +- 读操作时,从磁盘上一次读一批数据,然后加载到内存当中,以后就在内存中操作。如下图所示: - - **UPDATE**、**DELETE** 语句的 **WHERE** 条件列 - - **ORDER BY**、**GROUP BY**、**DISTINCT** 的字段 + ![InnoDB-数据页-读操作](images/Database/InnoDB-数据页-读操作.jpg) -- **多表JOIN** 的字段注意以下(优化准则) +磁盘中各数据页的整体结构如下图所示: - - **区分度最大** 的字段放在前面 - - 核⼼SQL优先考虑 **覆盖索引** - - 避免 **冗余** 和 **重复** 索引 - - 索引要综合评估数据 **密度** 和 **分布**以及考虑 **查询** 和 **更新** 比例 +![InnoDB-数据页](images/Database/InnoDB-数据页.jpg) -- 索引命名 +通常情况下,单个数据页默认的大小是`16kb`。当然,我们也可以通过参数:`innodb_page_size`,来重新设置大小。不过,一般情况下,用它的默认值就够了。单个数据页包含内容如下: - - 索引名称必须 **全部小写** - - 唯一所以必须以 **uniq _ 字段1 _ 字段2** 命名 - - 非唯一索引必须以 **idx _ 字段1 _ 字段2** 命名 +![InnoDB-单个数据页内容](images/Database/InnoDB-单个数据页内容.jpg) -- 新建的 **唯一索引** 必须不能和主键重复 +### 文件头部 -- 索引字段的默认值不能为 **NULL** (NULL非常影响索引的查询效率) +通过前面介绍的行记录中`下一条记录的位置`和`页目录`,innodb能非常快速的定位某一条记录。但有个前提条件,就是用户记录必须在同一个数据页当中。 -- 反复查看与表相关的SQL,符合 **最左前缀** 的特点建立索引 +如果用户记录非常多,在第一个数据页找不到我们想要的数据,需要到另外一页找该怎么办呢?这时就需要使用`文件头部`了。它里面包含了多个信息,但我只列出了其中4个最关键的信息: - 多条件字段重复的语句,要修改语句条件字段的顺序,为其建立一条 **联合索引**,减少索引数量 +- 页号 +- 上一页页号 +- 下一页页号 +- 页类型 -- **优先使用唯一索引**:能使用唯一索引就要使用唯一索引,提高查询效率 +顾名思义,innodb是通过页号、上一页页号和下一页页号来串联不同数据页的。如下图所示: -- 研发要经常使用 **explain**,如果发现索引选择性差,必须让他们学会使用hint +![InnoDB-文件头部](images/Database/InnoDB-文件头部.jpg) +不同的数据页之间,通过上一页页号和下一页页号构成了双向链表。这样就能从前向后,一页页查找所有的数据了。此外,页类型也是一个非常重要的字段,它包含了多种类型,其中比较出名的有:数据页、索引页(目录项页)、溢出页、undo日志页等。 -## 索引结构 -### 二叉树 +### 页头部 -**特点** +比如一页数据到底保存了多条记录,或者页目录到底使用了多个槽等。这些信息是实时统计,还是事先统计好了,保存到某个地方?为了性能考虑,上面的这些统计数据,当然是先统计好,保存到一个地方。后面需要用到该数据时,再读取出来会更好。这个保存统计数据的地方,就是`页头部`。当然页头部不仅仅只保存:槽的数量、记录条数等信息。它还记录了: -1. 左子节点值 < 节点值 -2. 右子节点值 > 节点值 -3. 当数据量非常大时,要查找的数据又非常靠后,和没有索引相比,那么二叉树结构的查询优势将非常明显 +- 已删除记录所占的字节数 +- 最后插入记录的位置 +- 最大事务id +- 索引id +- 索引层级 -**存在问题** -如下图,可以看出,二叉树出现单边增长时,二叉树变成了“链”,这样查找一个数的时候,速度并没有得到很大的优化。 -![索引结构-二叉树](images/Database/索引结构-二叉树.png) +### 最大和最小记录 +在一个数据页当中,如果存在多条用户记录,它们是通过`下一条记录的位置`相连的。不过有个问题:如果才能快速找到最大的记录和最小的记录呢?这就需要在保存用户记录的同时,也保存最大和最小记录了。最大记录保存到Supremum记录中。最小记录保存在Infimum记录中。 +在保存用户记录时,数据库会自动创建两条额外的记录:Supremum 和 Infimum。它们之间的关系,如下图所示: -### 红黑树 +![InnoDB-最大和最小记录](images/Database/InnoDB-最大和最小记录.jpg) -**特点** +从图中可以看出用户数据是从最小记录开始,通过下一条记录的位置,从小到大,一步步查找,最后找到最大记录为止。 -1. 节点是红色或者黑色 -2. 根节点是黑色 -3. 每个叶子的节点都是黑色的空节点(NULL) -4. 每个红色节点的两个子节点都是黑色的 -5. 从任意节点到其每个叶子的所有路径都包含相同的黑色节点 -![索引结构-红黑树](images/Database/索引结构-红黑树.png) -**存在的问题** +### 用户记录 -**红黑树虽然和二叉树相比,一定程度上缓解了单边过长的问题,但是它依旧存储高度问题。**  +对于新申请的数据页,用户记录是空的。当插入数据时,innodb会将一部分`空闲空间`分配给用户记录。用户记录是innodb的重中之重,我们平时保存到数据库中的数据,就存储在它里面。其实在innodb支持的数据行格式有四种: -假设现在数据量有100万,那么红黑树的高度大概为 100,0000 = 2^n, n大概为 20。那么,至少要20次的磁盘IO,这样,性能将很受影响。如果数据量更大,IO次数更多,性能损耗更大。**所以红黑树依旧不是最佳方案。** +- compact行格式 +- redundant行格式 +- dynamic行格式 +- compressed行格式 -**思考:针对上面的红黑树结构,我们能否优化一下呢?** +以compact行格式为例: -上述红黑树默认一个节点就存了一个 (索引+磁盘地址),我们设想一个节点存多个 (索引+磁盘地址),这样就可以降低红黑树的高度了。 **实际上我们设想的这种结构就是 B-Tree**。 +![InnoDB-compact行格式](images/Database/InnoDB-compact行格式.jpg) +一条用户记录主要包含三部分内容: +- 记录额外信息:它包含了变长字段、null值列表和记录头信息 +- 隐藏列:它包含了行id、事务id和回滚点 +- 真正的数据列:包含真正的用户数据,可以有很多列 -### Hash -**原理** -1. 事先将索引通过 hash算法后得到的hash值(即磁盘文件指针)存到hash表中 -2. 在进行查询时,将索引通过hash算法,得到hash值,与hash表中的hash值比对。通过磁盘文件指针,只要**一次磁盘IO**就能找到要的值 +#### 额外信息 -例如:在第一个表中,要查找col=6的值。hash(6) 得到值,比对hash表,就能得到89。性能非常高。 +额外信息并非真正的用户数据,它是为了辅助存数据用的。 -**存在问题** +- **变长字段列表** - 但是hash表索引存在问题,如果要查询带范围的条件时,hash索引就歇菜了。 + 有些数据如果直接存会有问题,比如:如果某个字段是varchar或text类型,它的长度不固定,可以根据存入数据的长度不同,而随之变化。如果不在一个地方记录数据真正的长度,innodb很可能不知道要分配多少空间。假如都按某个固定长度分配空间,但实际数据又没占多少空间,岂不是会浪费?所以,需要在变长字段中记录某个变长字段占用的字节数,方便按需分配空间。 -```mysql -select *from t where col1>=6; -``` +- **null值列表** - + 数据库中有些字段的值允许为null,如果把每个字段的null值,都保存到用户记录中,显然有些浪费存储空间。有没有办法只简单的标记一下,不存储实际的null值呢?答案:将为null的字段保存到null值列表。在列表中用二进制的值1,表示该字段允许为null,用0表示不允许为null。它只占用了1位,就能表示某个字符是否为null,确实可以节省很多存储空间。 -### B-Tree +- **记录头信息** -**特点** + 记录头信息用于描述一些特殊的属性。它主要包含: -B-Tree索引能很好解决红黑树中遗留的高度问题,B-Tree 是一种平衡的多路查找(又称排序)树,在文件系统中和数据库系统有所应用,主要用作文件的索引,其中的B就表示平衡(Balance)。 + - deleted_flag:即删除标记,用于标记该记录是否被删除了 + - min_rec_flag:即最小目录标记,它是非叶子节点中的最小目录标记 + - n_owned:即拥有的记录数,记录该组索引记录的条数 + - heap_no:即堆上的位置,它表示当前记录在堆上的位置 + - record_type:即记录类型,其中0表示普通记录,1表示非叶子节点,2表示Infrimum记录, 3表示Supremum记录 + - next_record:即下一条记录的位置 -为了描述B-Tree,首先定义一条数据记录为一个二元组 [key, data],key为记录的键值key,对于不同数据记录,key是互不相同的;**data为数据记录除以key外的数据 (这里指的是聚集索引)**。那么B-Tree是满足下列条件的数据结构: -1. d 为大于1的一个正整数,称为BTree的度 -2. h为一个正整数,称为BTree的高度 -3. key和指针互相间隔,节点两端是指针 -4. 叶子节点具有相同的深度,叶子节点的指针为空,节点中数据索引(下图中的key)从左往右递增排列 -**说明**:下图都是以主键索引为例,至于非主键索引(非聚集索引),无非就是data里存的内容不同。 +#### 隐藏列 -![索引结构-B-Tree指针](images/Database/索引结构-B-Tree指针.png) +数据库在保存一条用户记录时,会自动创建一些隐藏列。如下图所示: -![索引结构-B-Tree](images/Database/索引结构-B-Tree.png) +![InnoDB-隐藏列](images/Database/InnoDB-隐藏列.jpg) -**分析** +目前innodb自动创建的隐藏列有三种: -模拟下查找key为29的data的过程: +- db_row_id,即行id,它是一条记录的唯一标识。 +- db_trx_id,即事务id,它是事务的唯一标识。 +- db_roll_ptr,即回滚点,它用于事务回滚。 -1. 根据根结点指针读取文件目录的根磁盘块1。【磁盘IO操作第**1次**】 -2. 磁盘块1存储17,35和三个指针数据。我们发现17<29<35,因此我们找到指针p2 -3. 根据p2指针,我们定位并读取磁盘块3。【磁盘IO操作**2次**】 -4. 磁盘块3存储26,30和三个指针数据。我们发现26<29<30,因此我们找到指针p2 -5. 根据p2指针,我们定位并读取磁盘块8。【磁盘IO操作**3次**】 -6. 磁盘块8中存储28,29。我们找到29,获取29所对应的数据data +如果表中有主键,则用主键做行id,无需额外创建。如果表中没有主键,假如有不为null的unique唯一键,则用它做为行id,同样无需额外创建。如果表中既没有主键,又没有唯一键,则数据库会自动创建行id。也就是说在innodb中,隐藏列中`事务id`和`回滚点`是一定会被创建的,但行id要根据实际情况决定。 -**存在问题** -1. 比如,下面查询语句,那么不但需要叶子节点>20的值,也需要非叶子节点在右边节点的值。即下图画圈的两部分, **B-Tree似乎在范围查找没有更简便的方法,为了解决这一问题。我们可以用B+Tree。** - ```mysql - select *from t where col1 > 20; - ``` +#### 真正数据列 - ![索引结构-B-Tree](images/Database/索引结构-B-Tree问题.png) +真正的数据列中存储了用户的真实数据,它可以包含很多列的数据。 -2. 深度问题 - 从图上可以看到,每个节点中不仅包含数据的key值,还有data值。而每一个节点的存储空间是有限的(mysql默认设置一个节点的大小为16K),如果data中存放的数据较大时,将会导致每个节点(即一个页)能存储的key的数量(索引的数量)很小,所以当数据量很多,且每行数据量很大的时候,同样会导致B-Tree的深度较大,增大查询时的磁盘I/O次数,进而影响查询效率。所以引入B+Tree +### 页目录 +从上面可以看出,如果我们要查询某条记录的话,数据库会从最小记录开始,一条条查找所有记录。如果中途找到了,则直接返回该记录。如果一直找到最大记录,还没有找到想要的记录,则返回空。 -### B+Tree +但效率会不会有点低?这不是要对整页用户数据进行扫描吗? -**特点** +这就需要使用`页目录`了。说白了,就是把一页用户记录分为若干组,每一组的最大记录都保存到一个地方,这个地方就是`页目录`。每一组的最大记录叫做`槽`。由此可见,页目录是有多个槽组成的。所下图所示: -`B+Tree`是在`B-Tree`基础上的一种优化,使其更适合实现外存储索引结构。在B+Tree中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息,这样可以大大加大每个节点存储的key值数量,降低B+Tree的高度。 +![InnoDB-页目录](images/Database/InnoDB-页目录.jpg) -1. 非叶子节点不存储data,只存储索引,可以存放更多索引 -2. 叶子节点不存储指针 -3. 顺序访问指针,提高区间访问性能 -4. 非叶子节点中的索引最终还是会在叶子节点上存储一份,也就是叶子节点会包含非叶子节点上的所有索引 -5. 一个父节点,它的**左侧**子节点都**小于**父节点的值,**右侧**的子节点都**大于等于**父节点的值 -6. 每一层节点从左往右都是递增排列,无论是数值型还是字符型 +假设一页的数据分为4组,这样在页目录中,就对应了4个槽,每个槽中都保存了该组数据的最大值。这样就能通过二分查找,比较槽中的记录跟需要找到的记录的大小。如果用户需要查找的记录,小于当前槽中的记录,则向上查找上一个槽。如果用户需要查找的记录,大于当前槽中的记录,则向下查找下一个槽。如此一来,就能通过二分查找,快速的定位需要查找的记录了。 -**注意**:MySQL索引默认的存储结构使用的就是B+Tree。 -**![索引结构-B+Tree指针](images/Database/索引结构-B+Tree指针.png)** -![索引结构-B+Tree](images/Database/索引结构-B+Tree.png) +### 文件尾部 -**剖析**:如上图,在叶子节点上注意是MySQL已经有成双向箭头(原生B+Tree是单向的),而且从左到右是递增顺序的,所以很好的解决了 > 和 < 这类查找问题。 +数据库的数据是以数据页为单位,加载到内存中,如果数据有更新的话,需要刷新到磁盘上。但如果某一天比较倒霉,程序在刷新到磁盘的过程中,出现了异常,比如:进程被kill掉了,或者服务器被重启了。这时候数据可能只刷新了一部分,如何判断上次刷盘的数据是完整的呢?这就需要用到`文件尾部`。它里面记录了页面的`校验和`。 -**分析** +在数据刷新到磁盘之前,会先计算一个页面的校验和。后面如果数据有更新的话,会计算一个新值。文件头部中也会记录这个校验和,由于文件头部在前面,会先被刷新到磁盘上。 -假如:**以一个高度为3的B+Tree为例**,B+Tree的表都存满了,能存储多少数据? +接下来,刷新用户记录到磁盘的时候,假设刷新了一部分,恰好程序出现异常了。这时,文件尾部的校验和,还是一个旧值。数据库会去校验,文件尾部的校验和,不等于文件头部的新值,说明该数据页的数据是不完整的。 -**首先,**查看MySQL默认一个节点页的大小: -```mysql -SHOW GLOBAL STATUS like 'Innodb_page_size'; -``` -如下图:大小为16K。 +## Buffer Pool -![索引结构-B+Tree案例](images/Database/索引结构-B+Tree案例.png) +`InnoDB`为了解决磁盘`IO`问题,`MySQL`需要申请一块内存空间,这块内存空间称为`Buffer Pool`。 -然后,假设主键Id为bigint类型,那么长度就是8B,指针在Innodb源码中大小为6B,所以一共就是14B,再假设最后一层,存放的数据data为1k 大小(能存很多内容了),那么: +![Buffer-Pool](images/Database/Buffer-Pool.png) -1. 第一层最大节点数为: 16k / (8B + 6B) = 1170 (个) -2. 第二层最大节点数也应为:1170个 -3. 第三层最大节点数为:16k / 1k = 16 (个) +### 缓存页 -则,一张B+Tree的表最多存放 1170 * 1170 * 16 = 21902400 ≈ 2千万。所以,通过分析,我们可以得出,B+Tree结构的表可以容纳千万数据量的查询。而且**一般来说,MySQL会把 B+Tree 根节点放在内存中**,那只需要**两次磁盘IO(第二层1次,第三层1次)**就行。 +`Buffer Pool`申请下来后,`Buffer Pool`里面放什么,要怎么规划? -**扩展** +`MySQL`数据是以页为单位,每页默认`16KB`,称为数据页,在`Buffer Pool`里面会划分出若干**个缓存页**与数据页对应。 -数据库中的B+Tree索引可以分为聚集索引(clustered index,也叫主键索引)和辅助索引(secondary index,也叫非聚集索引)。上面的B+Tree示例图在数据库中的实现对应的是聚集索引,聚集索引的B+Tree中的叶子节点存放的是整张表的行记录数据(除主键以外的所有数据),辅助索引与聚集索引的区别在于辅助索引的叶子节点并不包含行记录的全部数据,而是存储相应行数据的对应的聚集索引键,即主键。当通过辅助索引来查询数据时,InnoDB存储引擎会遍历辅助索引找到主键,然后再通过主键在聚集索引中找到完整的行记录数据。 +![Buffer-Pool-缓存页](images/Database/Buffer-Pool-缓存页.png) -## 索引类型 +### 描述数据 -### 普通索引 +如何知道缓存页对应那个数据页呢? -**普通索引(单列索引)**:单列索引是最基本的索引,它没有任何限制。 +所以还需要缓存页的元数据信息,可以称为**描述数据**,它与缓存页一一对应,包含一些所属表空间、数据页的编号、`Buffer Pool`中的地址等等。 -- 直接创建索引 +![Buffer-Pool-描述数据](images/Database/Buffer-Pool-描述数据.png) -```mysql -CREATE INDEX index_name ON table_name(col_name); -``` +后续对数据的增删改查都是在`Buffer Pool`里操作 -- 修改表结构的方式添加索引 +- 查询:从磁盘加载到缓存,后续直接查缓存 +- 插入:直接写入缓存 +- 更新删除:缓存中存在直接更新,不存在加载数据页到缓存更新 -```mysql -ALTER TABLE table_name ADD INDEX index_name(col_name); -``` -- 创建表的时候同时创建索引 -```mysql -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)) -) -``` +`MySQL`宕机数据不就全丢了吗? -- 删除索引 +`InnoDB`提供了`WAL`技术(Write-Ahead Logging),通过`redo log`让`MySQL`拥有了崩溃恢复能力。再配合空闲时,会有异步线程做缓存页刷盘,保证数据的持久性与完整性。 -```mysql -DROP INDEX index_name ON table_name; -# 或 -alter table `表名` drop index 索引名; -``` +![Buffer-Pool-Write-Ahead-Logging](images/Database/Buffer-Pool-Write-Ahead-Logging.png) +直接更新数据的缓存页称为**脏页**,缓存页刷盘后称为**干净页**。 -### 复合索引 -**复合索引(组合索引)**:复合索引是在多个字段上创建的索引。复合索引遵守“**最左前缀**”原则**,**即在查询条件中使用了复合索引的第一个字段,索引才会被使用。因此,在复合索引中索引列的顺序至关重要。 +### Free链表 -- 创建一个复合索引 +`MySQL`数据库启动时,按照设置的`Buffer Pool`大小,去找操作系统申请一块内存区域,作为`Buffer Pool`(**假设申请了512MB**)。申请完毕后,会按照默认缓存页的`16KB`以及对应的`800Byte`的描述数据,在`Buffer Pool`中划分出来一个一个的缓存页和它们对应的描述数据。 -```mysql -create index index_name on table_name(col_name1,col_name2,...); -``` +![Buffer-Pool-Free链表](images/Database/Buffer-Pool-Free链表.png) -- 修改表结构的方式添加索引 +`MySQL`运行起来后,会不停的执行增删改查,需要从磁盘读取一个一个的数据页放入`Buffer Pool`对应的缓存页里,把数据缓存起来,以后就可以在内存里执行增删改查。 -```mysql -alter table table_name add index index_name(col_name,col_name2,...); -``` +![Buffer-Pool-Free链表-增删改查](images/Database/Buffer-Pool-Free链表-增删改查.png) +但是这个过程必然涉及一个问题,**哪些缓存页是空闲的**? +为了解决这个问题,我们使用链表结构,把空闲缓存页的**描述数据**放入链表中,这个链表称为`free`链表。针对`free`链表我们要做如下设计: -### 唯一索引 +![Buffer-Pool-Free链表设计](images/Database/Buffer-Pool-Free链表设计.png) -**唯一索引**:唯一索引和普通索引类似,主要的区别在于,**唯一索引限制列的值必须唯一,但允许存在空值(只允许存在一条空值)**。如果在已经有数据的表上添加唯一性索引的话: +- 新增`free`基础节点 +- 描述数据添加`free`节点指针 -- 如果添加索引的列的值存在两个或者两个以上的空值,则不能创建唯一性索引会失败。(一般在创建表的时候,要对自动设置唯一性索引,需要在字段上加上 not null) -- 如果添加索引的列的值存在两个或者两个以上null值,还是可以创建唯一性索引,只是后面创建的数据不能再插入null值 ,并且严格意义上此列并不是唯一的,因为存在多个null值 +最终呈现出来的,是由空闲缓存页的**描述数据**组成的`free`链表。 -对于多个字段创建唯一索引规定列值的组合必须唯一。比如:在order表创建orderId字段和 productId字段 的唯一性索引,那么这两列的组合值必须唯一: +![Buffer-Pool-Free链表组成](images/Database/Buffer-Pool-Free链表组成.png) -```mysql -“空值” 和”NULL”的概念: -1:空值是不占用空间的 . -2: MySQL中的NULL其实是占用空间的. +有了`free`链表之后,我们只需要从`free`链表获取一个**描述数据**,就可以获取到对应的缓存页。 -长度验证:注意空值的之间是没有空格的。 +![Buffer-Pool-Free链表-获取描述数据](images/Database/Buffer-Pool-Free链表-获取描述数据.png) -> select length(''),length(null),length(' '); -+------------+--------------+-------------+ -| length('') | length(null) | length(' ') | -+------------+--------------+-------------+ -| 0 | NULL | 1 | -+------------+--------------+-------------+ -``` +往**描述数据**与**缓存页**写入数据后,就将该**描述数据**移出`free`链表。 -- 创建唯一索引 -```mysql -# 创建单个索引 -CREATE UNIQUE INDEX index_name ON table_name(col_name); -# 创建多个索引 -CREATE UNIQUE INDEX index_name on table_name(col_name,...); -``` -- 修改表结构 +### 缓存页哈希表 -```mysql -# 单个 -ALTER TABLE table_name ADD UNIQUE index index_name(col_name); -# 多个 -ALTER TABLE table_name ADD UNIQUE index index_name(col_name,...); -``` +查询数据时,如何在`Buffer Pool`里快速定位到对应的缓存页呢?难道需要一个**非空闲的描述数据**链表,再通过**表空间号+数据页编号**遍历查找吗?这样做也可以实现,但是效率不太高,时间复杂度是`O(N)`。 -- 创建表的时候直接指定索引 +![Buffer-Pool-缓存页哈希表](images/Database/Buffer-Pool-缓存页哈希表.png) -```mysql -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`), - UNIQUE index_name_unique(title) -) -``` +所以我们可以换一个结构,使用哈希表来缓存它们间的映射关系,时间复杂度是`O(1)`。 +![Buffer-Pool-缓存页哈希表-复杂度](images/Database/Buffer-Pool-缓存页哈希表-复杂度.png) +**表空间号+数据页号**,作为一个`key`,然后缓存页的地址作为`value`。每次加载数据页到空闲缓存页时,就写入一条映射关系到**缓存页哈希表**中。 -### 主键索引 +![Buffer-Pool-缓存页哈希表-映射关系](images/Database/Buffer-Pool-缓存页哈希表-映射关系.png) -主键索引是一种特殊的唯一索引,一个表只能有一个主键,不允许有空值。一般是在建表的时候同时创建主键索引: +后续的查询,就可以通过**缓存页哈希表**路由定位了。 -- 主键索引(创建表时添加) -```mysql -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`) -) -``` -- 主键索引(创建表后添加) +### Flush链表 -```mysql -alter table tbl_name add primary key(col_name); -``` +还记得之前有说过「**空闲时会有异步线程做缓存页刷盘,保证数据的持久性与完整性**」吗? -```mysql -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`); -``` +新问题来了,难道每次把`Buffer Pool`里所有的缓存页都刷入磁盘吗?当然不能这样做,磁盘`IO`开销太大了,应该把**脏页**刷入磁盘才对(更新过的缓存页)。可是我们怎么知道,那些缓存页是**脏页**?很简单,参照`free`链表,弄个`flush`链表出来就好了,只要缓存页被更新,就将它的**描述数据**加入`flush`链表。针对`flush`链表我们要做如下设计: +- 新增`flush`基础节点 +- 描述数据添加`flush`节点指针 +![Buffer-Pool-Flush链表](images/Database/Buffer-Pool-Flush链表.png) -### 全文索引 +最终呈现出来的,是由更新过数据的缓存页**描述数据**组成的`flush`链表。 -在一般情况下,模糊查询都是通过 like 的方式进行查询。但是,对于海量数据,这并不是一个好办法,在 like "value%" 可以使用索引,但是对于 like "%value%" 这样的方式,执行全表查询,这在数据量小的表,不存在性能问题,但是对于海量数据,全表扫描是非常可怕的事情,所以 like 进行模糊匹配性能很差。 +![Buffer-Pool-Flush链表-缓存页](images/Database/Buffer-Pool-Flush链表-缓存页.png) -这种情况下,需要考虑使用全文搜索的方式进行优化。全文搜索在 MySQL 中是一个 FULLTEXT 类型索引。**FULLTEXT 索引在 MySQL 5.6 版本之后支持 InnoDB,而之前的版本只支持 MyISAM 表**。 +后续异步线程都从`flush`链表刷缓存页,当`Buffer Pool`内存不足时,也会优先刷`flush`链表里的缓存页。 -全文索引主要用来查找文本中的关键字,而不是直接与索引中的值相比较。fulltext索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的where语句的参数匹配。fulltext索引配合match against操作使用,而不是一般的where语句加like。目前只有char、varchar,text 列上可以创建全文索引。 +### LRU链表 -**小技巧** -在数据量较大时候,先将数据放入一个没有全局索引的表中,然后再用CREATE index创建fulltext索引,要比先为一张表建立fulltext然后再将数据写入的速度快很多。 +目前看来`Buffer Pool`的功能已经比较完善了。 -- 创建表的适合添加全文索引 +![Buffer-Pool-LRU链表](images/Database/Buffer-Pool-LRU链表.png) -```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) -) -``` +但是仔细思考下,发现还有一个问题没处理。`MySQL`数据库随着系统的运行会不停的把磁盘上的数据页加载到空闲的缓存页里去,因此`free`链表中的空闲缓存页会越来越少,直到没有,最后磁盘的数据页无法加载。 -- 修改表结构添加全文索引 +![Buffer-Pool-LRU链表-无法加载](images/Database/Buffer-Pool-LRU链表-无法加载.png) -```mysql -ALTER TABLE table_name ADD FULLTEXT index_fulltext_content(col_name) -``` +为了解决这个问题,我们需要淘汰缓存页,腾出空闲缓存页。可是我们要优先淘汰哪些缓存页?总不能一股脑直接全部淘汰吧?这里就要借鉴`LRU`算法思想,把最近最少使用的缓存页淘汰(命中率低),提供`LRU`链表出来。针对`LRU`链表我们要做如下设计: -- 直接创建索引 +- 新增`LRU`基础节点 +- 描述数据添加`LRU`节点指针 -```mysql -CREATE FULLTEXT INDEX index_fulltext_content ON table_name(col_name) -``` +![Buffer-Pool-LRU链表-结构](images/Database/Buffer-Pool-LRU链表-结构.png) -**注意**: 默认MySQL不支持中文全文检索!MySQL 全文搜索只是一个临时方案,对于全文搜索场景,更专业的做法是使用全文搜索引擎,例如 ElasticSearch 或 Solr。 +实现思路也很简单,只要是查询或修改过缓存页,就把该缓存页的描述数据放入链表头部,也就说近期访问的数据一定在链表头部。 -- 索引的查询和删除 +![Buffer-Pool-LRU链表-节点](images/Database/Buffer-Pool-LRU链表-节点.png) -```mysql --- 查看: -show indexes from `表名`; --- 或 -show keys from `表名`; +当`free`链表为空的时候,直接淘汰`LRU`链表尾部缓存页即可。 --- 删除 -alter table `表名` drop index 索引名; -``` -**注意**:MySQl的客户端工具也可以进索引的创建、查询和删除,如 Navicat Premium! +### LRU链表优化 +麻雀虽小五脏俱全,基本`Buffer Pool`里与缓存页相关的组件齐全了。 -## 索引优缺点 +![Buffer-Pool-LRU链表优化](images/Database/Buffer-Pool-LRU链表优化.png) -### 索引优点 +但是缓存页淘汰这里还有点问题,如果仅仅只是使用`LRU`链表的机制,有两个场景会让**热点数据**被淘汰。 -- 提高数据检索的效率,降低检索过程中必须要读取得数据量,降低数据库IO成本 -- 降低数据库的排序成本。因为索引就是对字段数据进行排序后存储的,如果待排序的字段与索引键字段一致,就在取出数据后不用再次排序了,因为通过索引取得的数据已满足排序要求。另外,分组操作是先排序后分组,所以索引同样可以省略分组的排序操作,降低内存与CPU资源的消耗 +- **预读机制** + InnoDB使用两种预读算法来提高I/O性能:线性预读(linear read-ahead)和随机预读(randomread-ahead)。 +- **全表扫描** -### 索引缺点 + 预读机制是指`MySQL`加载数据页时,可能会把它相邻的数据页一并加载进来(局部性原理)。这样会带来一个问题,预读进来的数据页,其实我们没有访问,但是它却排在前面。 -- 索引会增加 增、删、改操作所带来的IO量与调整索引的计算量 -- 索引要占用空间,随着数据量的不断增大,索引还会带来存储空间的消耗 + ![Buffer-Pool-LRU链表优化-全表扫描](images/Database/Buffer-Pool-LRU链表优化-全表扫描.png) + 正常来说,淘汰缓存页时,应该把这个预读的淘汰,结果却把尾部的淘汰了,这是不合理的。我们接着来看第二个场景全表扫描,如果**表数据量大**,大量的数据页会把空闲缓存页用完。最终`LRU`链表前面都是全表扫描的数据,之前频繁访问的热点数据全部到队尾了,淘汰缓存页时就把**热点数据页**给淘汰了。 + ![图片](images/Database/819dbbcd31605b3a692576932f25d325.png) -## 失效场景 + 为了解决上述的问题,我们需要给`LRU`链表做冷热数据分离设计,把`LRU`链表按一定比例,分为冷热区域,热区域称为`young`区域,冷区域称为`old`区域。 -**场景一:where语句中包含or时,可能会导致索引失效** -使用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; -``` +**以7:3为例,young区域70%,old区域30%** -可以根据情况尽量使用union all或者in来代替,这两个语句的执行效率也比or好些。 +![图片](images/Database/0f2c2610773fb9fe304c374fc37af4ac.png) +如上图所示,数据页第一次加载进缓存页的时候,是先放入冷数据区域的头部,如果1秒后再次访问缓存页,则会移动到热区域的头部。这样就保证了**预读机制**与**全表扫描**加载的数据都在链表队尾。 +- `young`区域其实还可以做一个小优化,为了防止`young`区域节点频繁移动到表头 -**场景二:where语句中索引列使用了负向查询,可能会导致索引失效** +- `young`区域前面`1/4`被访问不会移动到链表头部,只有后面的`3/4`被访问了才会 -负向查询包括:NOT、!=、<>、!<、!>、NOT IN、NOT LIKE等。其实负向查询并不绝对会索引失效,这要看MySQL优化器的判断,全表扫描或者走索引哪个成本低了。 +记住是按照某个比例将`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内没有被刷走,就调入热区。 -**场景三:索引字段可以为null,使用is null或is not null时,可能会导致索引失效** -其实单个索引字段,使用is null或is not null时,是可以命中索引的。 +## Change Buffer +可变缓冲区(Change Buffer),在内存中,可变缓冲区是InnoDB缓冲池的一部分,在磁盘上,它是系统表空间的一部分,因此即使在数据库重新启动之后,索引更改也会保持缓冲状态。 +可变缓冲区是一种特殊的数据结构,当受影响的页不在缓冲池中时,缓存对辅助索引页的更改。 -**场景四:在索引列上使用内置函数,一定会导致索引失效** -比如下面语句中索引列login_time上使用了函数,会索引失效: -```sql -select * from user where DATE_ADD(login_time, INTERVAL 1 DAY) = 7; -``` +## Log Buffer +日志缓冲区(Log Buffer ),主要保存写到redo log(重放日志)的数据。周期性的将缓冲区内的数据写入redo日志中。将内存中的数据写入磁盘的行为由innodb_log_at_trx_commit 和 innodb_log_at_timeout 调节。较大的redo日志缓冲区允许大型事务在事务提交前不进行写磁盘操作。 +变量:innodb_log_buffer_size (默认 16M) -**场景五:隐式类型转换导致的索引失效** -如下面语句中索引列user_id为varchar类型,不会命中索引: +## InnoDB日志 -```mysql -select * from user where user_id = 12; -``` +### Redo Log(重做日志) -**场景六:对索引列进行运算,一定会导致索引失效** +### Undo Log -运算如+,-,\*,/等,如下: -```mysql -select * from user where age - 1 = 10; -``` -优化的话,要把运算放在值上,或者在应用程序中直接算好,比如: -```sql -select * from user where age = 10 - 1; -``` +# 数据切分 +## 水平切分 -**场景七:like通配符可能会导致索引失效** +水平切分又称为 Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。当一个表的数据不断增多时,Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。 -like查询以%开头时,会导致索引失效。解决办法有两种: +![img](images/Database/007S8ZIlly1gjjfy33yx2j30fm05zwg9.jpg) -- 将%移到后面,如: -```sql -select * from user where `name` like '李%'; -``` -- 利用覆盖索引来命中索引: +## 垂直切分 -```sql -select name from user where `name` like '%李%'; -``` +垂直切分是将一张表按列分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直气氛将经常被使用的列喝不经常被使用的列切分到不同的表中。在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不通的库中,例如将原来电商数据部署库垂直切分称商品数据库、用户数据库等。 +![img](images/Database/007S8ZIlly1gjjfy5yoatj30cy09l776.jpg) -**场景八:联合索引中,where中索引列违背最左匹配原则,一定会导致索引失效** -当创建一个联合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。比如下面的语句就不会命中索引: +## Sharding策略 -```sql -select * from t where k2=2; -select * from t where k3=3; -select * from t where k2=2 and k3=3; -``` +- 哈希取模:hash(key)%N +- 范围:可以是 ID 范围也可以是时间范围 +- 映射表:使用单独的一个数据库来存储映射关系 -下面的语句只会命中索引(k1): -```sql -select * from t where k1=1 and k3=3; -``` +## Sharding存在的问题 +- **事务问题**:使用分布式事务来解决,比如 XA 接口 -## 优化建议 +- **连接**:可以将原来的连接分解成多个单表查询,然后在用户程序中进行连接。 -- **禁止在更新十分频繁、区分度不高的属性上建立索引** - - 更新会变更B+树,更新频繁的字段建立索引会大大降低数据库性能。 - - “性别”这种区分度不大的属性,建立索引是没有什么意义的,不能有效过滤数据,性能与全表扫描类似。 -- **建立组合索引,必须把区分度高的字段放在前面** +- **唯一性** + - 使用全局唯一 ID (GUID) + - 为每个分片指定一个 ID 范围 + - 分布式 ID 生成器(如 Twitter 的 Snowflake 算法)