|
|
|
|
# MySQL 架构
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 1. 系统是怎么和 MySQL 打交道 ?
|
|
|
|
|
- MySQL不仅仅是CRUD
|
|
|
|
|
|
|
|
|
|
### 1.1 MySQL 驱动
|
|
|
|
|
- mysql 驱动是跟数据库进行的一个网络连接建立工具
|
|
|
|
|
- 不同的语言有不同的驱动使用
|
|
|
|
|
|
|
|
|
|
### 1.2 数据库连接池
|
|
|
|
|
- 作用是减少连接的重复建立和销毁,让线程去执行SQL语句后, 不要销毁这个数据库连接,而是放在池子中进行复用
|
|
|
|
|
- 常见的数据库连接池有: DBCP,C3P0,Druid
|
|
|
|
|
|
|
|
|
|
## 2. 执行 SQL 语句, MySQL用了什么架构设计?
|
|
|
|
|
- 一个不变的原则: 网络连接必须让线程来处理
|
|
|
|
|
![MySQL架构设计](pic/MySQL架构设计.png)
|
|
|
|
|
- SQL 接口: 负责处理接收到的SQL语句
|
|
|
|
|
- 监听请求以及读取请求数据的线程, 把解析好的SQL语句v转交给SQL接口去执行
|
|
|
|
|
- 查询解析器: 按照估计的SQL语法, 对我们发送的 SQL 语句进行解析, 理解其要进行的操作
|
|
|
|
|
- 查询优化器: 生成查询路径树, 然后从里面选择一条最优的查询路径
|
|
|
|
|
- 调用存储引擎, 真正的执行 SQL 语句
|
|
|
|
|
- MySQL 的设计架构中, SQL接口, SQL解析器, 查询优化器其实都是通用的,就是一套组件
|
|
|
|
|
- 存储引擎可以供我们去选择, 选择那种存储引擎来执行SQL。常见的存储引擎: InnoDB, MyISAM, Memory等。
|
|
|
|
|
- 执行器: 执行器会根据我们的优化器生成一套执行计划, 然后不停的调用存储引擎的各种接口去完成SQL语句的执行计划
|
|
|
|
|
|
|
|
|
|
## 3. 用一次数据更新流程, 了解 InnoDB 引擎的架构设计
|
|
|
|
|
|
|
|
|
|
### 3.1 InnoDB 的重要内存结构: 缓冲池
|
|
|
|
|
![缓冲池](pic/缓冲池.png)
|
|
|
|
|
- InnoDB 的很重要放在内存里面的组件, 里面会缓冲很多数据, 在查询时候, 如果查的是内存中的数据, 就不用去查盘了
|
|
|
|
|
![缓冲池加载](pic/缓冲池加载.png)
|
|
|
|
|
- 引擎在更新语句的时候, 比如 "id=10" 这行数据是否在缓冲池中, 如果不在就从磁盘中加载到缓冲池, 而且还会对这行数据加独占锁
|
|
|
|
|
|
|
|
|
|
### 3.2 undo日志文件: 如何让你更新的数据可以回滚?
|
|
|
|
|
![undo日志](pic/undo日志.png)
|
|
|
|
|
- 我们开发中可以轻松地利用事务对数据提交的过程进行回滚操作,是因为会把更新前的值写入到undo日志文件中
|
|
|
|
|
|
|
|
|
|
### 3.3 更新 buffer pool 中的缓存数据
|
|
|
|
|
- 当我们要把更新的那行记录从磁盘文件加载到缓冲池, 同时对他加锁, 而且还把更新前的旧值写入undo日志文件之后, 我们才正式开始更新
|
|
|
|
|
这行记录, 更新的时候, 先会更新缓冲池中的记录, 此时这个数据是脏数据
|
|
|
|
|
- 这里所谓的更新内存缓冲池里的数据, 意思是把内存里的 "id=10" 这行数据的name修改为 "xxx"
|
|
|
|
|
- 为什么说是脏数据呢?
|
|
|
|
|
- 因为这个时候磁盘上 "id=10" 这行数据的name字段还是以前的值, 但是内存里面这行数据已经改变了, 所以称为脏数据
|
|
|
|
|
![更新buffer-pool中的缓存数据](pic/更新bufferpool中的缓存数据.png)
|
|
|
|
|
|
|
|
|
|
### 3.4 Redo Log Buffer: 万一系统宕机, 如何避免数据丢失?
|
|
|
|
|
- 在 3.3 中如果此时宕机, 会导致内存中改过的数据丢失, 怎么办?
|
|
|
|
|
- 这个时候,就必须把对内存的修改写到一个Redo Log Buffer 中, 这也是内存里的一个缓冲区, 是用来存放 redo 日志的
|
|
|
|
|
- 所谓 redo 日志,就是记录你对数据做了什么修改, 比如对 "id=10这行记录修改了name字段的值为xxx", 这就是一个日志
|
|
|
|
|
![redo日志](pic/redo日志.png)
|
|
|
|
|
- 这个 redo 日志其实是用来在 MySQL 突然宕机的时候, 用来恢复你更新过的数据, 现在redo日志还仅仅停留在内存缓冲里
|
|
|
|
|
|
|
|
|
|
### 3.5 如果还没提交事务, MySQL 宕机了怎么办?
|
|
|
|
|
- 执行一条SQL语句, 其实也可以是一个独立的事务, 当你提交事务后, SQL语句才算执行结束
|
|
|
|
|
- 此时还没有提交事务, 如果此时 MySQL 崩溃, 必然导致内存里 Buffer Pool 中的修改过的数据都丢失,
|
|
|
|
|
同时你写入 Redo Log Buffer 中的redo日志也会丢失<br/>
|
|
|
|
|
![没提交事务宕机](pic/没提交事务宕机.png)
|
|
|
|
|
- 此时, 丢数据不要紧, 因为你一条更新语句, 没提交事务, 就代表他没执行成功, 磁盘上的数据没有改变, mysql 重启后, 你的数据无任何变化
|
|
|
|
|
|
|
|
|
|
### 3.6 提交事务的时候将redo日志写入磁盘中
|
|
|
|
|
- 我们提交一个事务, 此时会根据一定的策略把redo日志从 redo log buffer里刷入到磁盘文件里去
|
|
|
|
|
- 这个策略是通过 innodb_flush_log_at_trx_commit 来配置的
|
|
|
|
|
- 当这个值为0时候, 你提交事务的时候,不会把redo log buffer里面的数据刷入磁盘文件,此时可能你都提交事务了, 结果MySQL宕机了, 然后此时内存里的数据全部丢失
|
|
|
|
|
相当于你提交事务成功了, 但是由于MySQL突然宕机, 导致内存中的数据和redo日志都丢失了<br/>
|
|
|
|
|
![redo写磁盘0](pic/redo写磁盘0.png)
|
|
|
|
|
- 当这个参数值为1的时候, 你提交事务的时候, 就必须把redo log从内存刷入到磁盘文件中去, 只要事务提交成功, 那么redo log就必然在磁盘里了
|
|
|
|
|
![redo写磁盘1](pic/redo写磁盘1.png)
|
|
|
|
|
- 只要提交事务成功, redo日志一定在磁盘文件中了, 此时你肯定会有一条redo日志说了, "我对什么数据进行了一个什么操作"
|
|
|
|
|
- 然后哪怕此时buffer pool中更新过的数据还没刷到磁盘里面去,此时内存中的数据是已经更新过的"name=xxx",然后磁盘上的数据是还没更新过的"name=zhangsan"
|
|
|
|
|
![redo写磁盘3](pic/redo写磁盘3.png)
|
|
|
|
|
- 此时不会丢数据, 因为redo日志中已经记录了操作
|
|
|
|
|
- 所以此时mysql重启后, 可以根据redo日志去恢复之前做的修改
|
|
|
|
|
![redo写磁盘4](pic/redo写磁盘4.png)
|
|
|
|
|
- 如果 innodb_flush_log_at_trx_commit 值为2
|
|
|
|
|
- 意思是说, 提交事务的时候, 把redo日志写入磁盘文件对应的os cache缓存里去, 而不是直接进入磁盘文件, 可能1秒后才会把 os cache 里的数据写入到磁盘文件中去
|
|
|
|
|
- 这种模式下,你提交了事务, redo log可能仅仅停留在os cache内存缓存中, 没实际进入磁盘文件, 万一此时你要是机器宕机了, 那么os cache里的redo log就会丢失
|
|
|
|
|
同样让你感觉提交了事务, 数据丢了
|
|
|
|
|
![redo写磁盘5](pic/redo写磁盘5.png)
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
- 总结: 对于redo日志的三种刷盘策略, 我们的通常建议是1, 保证事务提交后, 数据绝对不能丢失
|
|
|
|
|
## 4. 聊聊binlog是什么?
|
|
|
|
|
|
|
|
|
|
### 4.1 binlog 日志概念
|
|
|
|
|
- binlog 日志叫做归档日志, 他里面记录的是偏向于逻辑性的日志, 类似于“对users表中的id=10的一行数据做了更新操
|
|
|
|
|
作,更新以后的值是什么”
|
|
|
|
|
- binlog不是InnoDB存储引擎特有的日志文件,是属于mysql server自己的日志文件。
|
|
|
|
|
|
|
|
|
|
### 4.2 提交事务, 同时会写入binlog日志
|
|
|
|
|
![写入binlog日志](pic/写入binlog日志.png)
|
|
|
|
|
- 跟InnoDB存储引擎进行交互的组件加入了之前提过的执行器这个组件,
|
|
|
|
|
他会负责跟InnoDB进行交互,包括从磁盘里加载数据到Buffer Pool中进行缓存,包括写入undo日志,包括更新
|
|
|
|
|
Buffer Pool里的数据,以及写入redo log buffer,redo log刷入磁盘,写binlog,等等。
|
|
|
|
|
- 执行器是非常核心的一个组件,负责跟存储引擎配合完成一个SQL语句在磁盘与内存层面的全部数据更新操
|
|
|
|
|
作。
|
|
|
|
|
- 把一次更新语句的执行,拆分为了两个阶段,上图中的1、2、3、4几个步骤,其实本质是你执行这个更新语句的时候干的事。
|
|
|
|
|
- 5和6两个步骤,是从你提交事务开始的,属于提交事务的阶段了。
|
|
|
|
|
|
|
|
|
|
### 4.3 binlog日志的刷盘策略分析
|
|
|
|
|
- binlog日志,其实也有不同的刷盘策略,有一个**sync_binlog**参数可以控制binlog的刷盘策略,他的默认值是0,
|
|
|
|
|
此时你把binlog写入磁盘的时候,其实不是直接进入磁盘文件,而是进入os cache内存缓存
|
|
|
|
|
- 如果此时机器宕机,那么你在os cache里的binlog日志是会丢失的
|
|
|
|
|
![binlog日志刷盘](pic/binlog日志刷盘.png)
|
|
|
|
|
- 如果要是把sync_binlog参数设置为1的话,那么此时会强制在提交事务的时候,把binlog直接写入到磁盘文件里去,
|
|
|
|
|
那么这样提交事务之后,哪怕机器宕机,磁盘上的binlog是不会丢失的
|
|
|
|
|
![binlog日志刷盘1](pic/binlog日志刷盘1.png)
|
|
|
|
|
|
|
|
|
|
### 4.3 基于binlog和redo log完成事务的提交
|
|
|
|
|
- 当我们把binlog写入磁盘文件之后,接着就会完成最终的事务提交,此时会把**本次更新对应的binlog文件名称**和这次
|
|
|
|
|
**更新的binlog日志在文件里的位置**,都写入到redo log日志文件里去,同时在redo log日志文件里写入一个**commit标记**。
|
|
|
|
|
|
|
|
|
|
### 4.4 最后一步在redo日志中写入commit标记的意义是什么?
|
|
|
|
|
- 用来保持redo log日志与binlog日志一致的
|
|
|
|
|
- 完整的事物提交成功, 必须是在redo log中写入最终的事务commit标记了,而且redo log里有本次更新对应的日 志,binlog里也有本次
|
|
|
|
|
更新对应的日志 ,redo log和binlog完全是一致的。
|
|
|
|
|
|
|
|
|
|
### 4.5 后台IO线程随机将内存更新后的脏数据刷回磁盘
|
|
|
|
|
- 已经提交事务, 他已经把内存里的 buffer pool中的缓存数据更新了,同时磁盘里有redo日志和binlog日志都记录的新值,但是磁盘上可能还是旧值?
|
|
|
|
|
- 因为MySQL有一个后台的IO线程,会在之后某个时间里,随机的把内存buffer pool中的修改后的脏数据给刷回到磁 盘上的数据文件里去
|
|
|
|
|
![IO线程随机更新](pic/IO线程随机更新.png)
|
|
|
|
|
- 在你IO线程把脏数据刷回磁盘之前,哪怕mysql宕机崩溃也没关系,因为重启之后,会根据redo日志恢复之前提交事 务做过的修改到内存里去,
|
|
|
|
|
就是id=10的数据的name修改为了xxx,然后等适当时机,IO线程自然还是会把这个修改后的数据刷到磁盘上的数据文件里去的
|
|
|
|
|
|
|
|
|
|
### 4.6 总结
|
|
|
|
|
- InnoDB存储引擎: buffer pool、redo log buffer等内存里的缓存数据, undo日志文件, redo日志文件, 同时mysql server自己还有 binlog日志文件
|
|
|
|
|
- 在你执行更新的时候,每条SQL语句,都会对应修改buffer pool里的缓存数据、写undo日志、写redo log buffer几个步骤
|
|
|
|
|
- 当你提交事务的时候,一定会把redo log刷入磁盘,binlog刷入磁盘,完成redo log中的事务commit标记;最后后台的IO线程会随机的把buffer pool里的脏数据刷入磁盘里去。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## 生产经验: 真实生产环境下的数据库机器配置如何规划?
|
|
|
|
|
|
|
|
|
|
### 普通的Java应用系统部署在机器上能抗多少并发?
|
|
|
|
|
- Java应用系统部署的时候常选用的机器配置大致是2核4G和4核8G的较多一些,数据库部署的时候常选用的机器配置最低在8核16G以上,正常在16核32G
|
|
|
|
|
- 一台机器能抗下每秒多少请求,往往是跟你每个请求处理耗费多长时间是关联的
|
|
|
|
|
- 4核8G的机器部署普通的Java应用系统,每秒大致就是抗下几百的并发访问,从每秒一两百请求到每秒七八百请求,都是有可能的,关键是看你每个请求处理需要耗费多长时间。
|
|
|
|
|
|
|
|
|
|
### 高并发场景下,数据库应该用什么样的机器?
|
|
|
|
|
- 往往对一个数据库而言,都是选用8核16G的机器作为起步,最好是选用16核32G的机器更加合适一些,因为数据库需要执行大量的磁盘IO操作,
|
|
|
|
|
他的每个请求都比较耗时一些,所以机器的配置自然需要高一些了
|
|
|
|
|
- 一般8核16G的机器部署的MySQL数据库,每秒抗个一两千并发请求是没问题的,但是如果你 的并发量再高一些,假设每秒有几千并发请求,那么可能数据库就会有点危险了,因为数据库的CPU、磁盘、IO、内存的负载
|
|
|
|
|
都会很高,弄不数据库压力过大就会宕机。
|
|
|
|
|
- 对于16核32G的机器部署的MySQL数据库而言,每秒抗个两三千,甚至三四千的并发请求也都是可以的,但是如果你达到每秒上万请求,
|
|
|
|
|
那么数据库的CPU、磁盘、IO、内存的负载瞬间都会飙升到很高,数据库也是可能会扛不住宕机的。
|
|
|
|
|
- 对于数据库而言,如果可以的话,最好是采用SSD固态硬盘而不是普通的机械硬盘,因为数据库最大的复杂就在于大量的 磁盘IO,他需要大量的读写磁盘文件,
|
|
|
|
|
所以如果能使用SSD固态硬盘,那么你的数据库每秒能抗的并发请求量就会更高一些。
|
|
|
|
|
|
|
|
|
|
## 生产经验:互联网公司的生产环境数据库是如何进行性能测试的?
|
|
|
|
|
|
|
|
|
|
### QPS和TPS到底有什么区别?
|
|
|
|
|
- QPS,他的英文全称是:Query Per Second。
|
|
|
|
|
- QPS就是说,你的这个数据库每秒可以处理多少个请求,你大致可以理解为,一次请求就是一条SQL语句,也就是说这个数据库每秒可以处理多少个SQL语句
|
|
|
|
|
- TPS,他的英文全称是:Transaction Per Second
|
|
|
|
|
- 其实就是每秒可处理的事务量,这个TPS往往是用在数据库中较多一些,其实从字面意思就能看的出来,他就是说数据库每秒会处理多少次事务提交或者回滚。
|
|
|
|
|
- PS: 不同的服务或者系统, 关注的是QPS还是TPS是不一样的。
|
|
|
|
|
|
|
|
|
|
### IO相关的压测性能指标
|
|
|
|
|
- IOPS
|
|
|
|
|
- 这个指的是机器的随机IO并发处理的能力,比如机器可以达到200 IOPS,意思就是说每秒可以执行200个随机 IO读写请求
|
|
|
|
|
- 这个指标是很关键的,你在内存中更新的脏数据库,最后都会由后台IO线程在不确定的时间,刷回到磁盘里去,这就是随机IO的过程。
|
|
|
|
|
- 如果说IOPS指标太低了,那么会导致你内存里的脏数据刷回磁盘的效率就会不高。
|
|
|
|
|
- 吞吐量
|
|
|
|
|
- 这个指的是机器的磁盘存储每秒可以读写多少字节的数据量
|
|
|
|
|
- 这个指标也是很关键的,我们平时在执行各种SQL语句的时候,提交事务的时候,其实都是大量的会写redo log之类的日志的,这些日志都会直接写磁盘文件。
|
|
|
|
|
- 一台机器他的存储每秒可以读写多少字节的数据量,就决定了他每秒可以把多少redo log之类的日志写入到磁盘里去。
|
|
|
|
|
- 一般来说我们写redo log之类的日志,都是对磁盘文件进行顺序写入的,也就是一行接着一行的写,不会说进行随机的读写
|
|
|
|
|
- 一般普通磁盘的顺序写入的吞吐量每秒都可以达到200MB左右
|
|
|
|
|
- 所以通常而言,机器的磁盘吞吐量都是足够承载高并发请求的。
|
|
|
|
|
- latency
|
|
|
|
|
- 这个指标说的是往磁盘里写入一条数据的延迟
|
|
|
|
|
- 这个指标同样很重要,因为我们执行SQL语句和提交事务的时候,都需要顺序写redo log磁盘文件,所以此时你写一条日志到磁盘文件里去,
|
|
|
|
|
到底是延迟1ms,还是延迟100us,这就对你的数据库的SQL语句执行性能是有影响的。
|
|
|
|
|
- 一般来说,当然是你的磁盘读写延迟越低,那么你的数据库性能就越高,你执行每个SQL语句和事务的时候速度就会越快。
|
|
|
|
|
|
|
|
|
|
### 压测的时候要关注的其他性能指标
|
|
|
|
|
- CPU负载
|
|
|
|
|
- CPU负载是一个很重要的性能指标,因为假设你数据库压测到了每秒处理3000请求了,可能其他的性能指标都还正常,但是此时CPU负载特别高,
|
|
|
|
|
那么也说明你的数据库不能继续往下压测更高的QPS了,否则CPU是吃不消的
|
|
|
|
|
- 网络负载
|
|
|
|
|
- 这个主要是要看看你的机器带宽情况下,在压测到一定的QPS和TPS的时候,每秒钟机器的网卡会输入多少
|
|
|
|
|
MB数据,会输出多少MB数据,因为有可能你的网络带宽最多每秒传输100MB的数据,那么可能你的QPS到1000的时候,网
|
|
|
|
|
卡就打满了,已经每秒传输100MB的数据了,此时即使其他指标都还算正常,但是你也不能继续压测下去了
|
|
|
|
|
- 内存负载
|
|
|
|
|
- 这个就是看看在压测到一定情况下的时候,你的机器内存耗费了多少,如果说机器内存耗费过高了,说明也不能继续压测下去了
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 事物和锁
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 索引优化
|
|
|
|
|
|
|
|
|
|
## 64. 深入研究索引之前,先来看看磁盘数据页的存储结构
|
|
|
|
|
- 大量的数据页是按顺序一页一页存放的,然后两两相邻的数据页之间会采用双向链表的格式互相引用
|
|
|
|
|
- 上述表示在磁盘中的形态
|
|
|
|
|
- 其实一个数据页在磁盘文件里就是一段数据,可能是二进制或者别的特殊格式的数据,然后数据页里包含两个指针,一个指针指向自己上一个数据页的物理地址,
|
|
|
|
|
一个指针指向自己下一个数据页的物理地址,大概可以认为类似下面这样:
|
|
|
|
|
- DataPage: xx=xx, xx=xx, linked_list_pre_pointer=15367, linked_list_next_pointer=34126 ||
|
|
|
|
|
- DataPage: xx=xx, xx=xx, linked_list_pre_pointer=23789, linked_list_next_pointer=46589 ||
|
|
|
|
|
- DataPage: xx=xx, xx=xx, linked_list_pre_pointer=33198, linked_list_next_pointer=55681
|
|
|
|
|
- MySQL实际存储大致也是类似这样的,就是每个数据页在磁盘文件里都是连续的一段数据。
|
|
|
|
|
然后每个数据页里,可以认为就是DataPage打头一直到 || 符号的一段磁盘里的连续的数据,你可以认为每一个数据页就是磁盘文件里这么一段连续的东西。
|
|
|
|
|
每个数据页,都有一个指针指向自己上一个数据页在磁盘文件里的起始物理位置,比如 linked_list_pre_pointer=15367 就是指向了上一个数据页在磁盘文件里的起始物理位置
|
|
|
|
|
那个15367可以认为就是在磁盘文件里的position或者offset,同理,也有一个指针指向自己下一个数据页的物理位置。
|
|
|
|
|
- 然后一个数据页内部会存储一行一行的数据,也就是平时我们在一个表里插入的一行一行的数据就会存储在数据页里,然后数据页里的每一行数据都会按照主键大小进行排序存储,同时每一行数据都有指针
|
|
|
|
|
指向下一行数据的位置,组成单向链表
|
|
|
|
|
![磁盘数据页的存储结构](pic/磁盘数据页的存储结构.png)
|
|
|
|
|
- 总结: 数据页之间是组成双向链表的,然后数据页内部的数据行是组成单向链表的,而且数据行是根据主键从小到大排序的。
|
|
|
|
|
|
|
|
|
|
## 65. 假设没有任何索引,数据库是如何根据查询语句搜索数据的?
|
|
|
|
|
- 每个数据页里都会有一个页目录,里面根据数据行的主键存放了一个目录,同时数据行是被分散存储到不同的槽位里去的,所以实际上每个数据页的目录里,就是这个页里每个主键跟所在槽位的映射关系
|
|
|
|
|
![页目录1](pic/页目录1.png)
|
|
|
|
|
- 假设你要根据主键查找一条数据,而且假设此时你数据库里那个表就没几条数据,那个表总共就一个数据页,首先就会先到数据页的页目录里根据主键进行二分查找
|
|
|
|
|
- 然后通过二分查找在目录里迅速定位到主键对应的数据是在哪个槽位里,然后到那个槽位里去,遍历槽位里每一行数据,就能快速找到那个主键对应的数据了。
|
|
|
|
|
- 每个槽位里都有一组数据行,你就是在里面遍 历查找就可以了。
|
|
|
|
|
- 但是假设你要是根据非主键的其他字段查找数据呢?
|
|
|
|
|
- 此时你是没办法使用主键的那种页目录来二分查找的,只能进入到数据页里,根据单向链表依次遍历查找数据了,这就性能很差了。
|
|
|
|
|
- 假如我们有很多数据页呢?
|
|
|
|
|
- 假设你要是没有建立任何索引,那么无论是根据主键查询,还是根据其他字段来条件查询,实际上都没有什么取巧的办法
|
|
|
|
|
- 一个表里所有数据页都是组成双向链表的吧?好,有链表就好办了,直接从第一个数据页开始遍历所有数据页,从第一个数据页开始,
|
|
|
|
|
你得先把第一个数据页从磁盘上读取到内存buffer pool的缓存页里来。
|
|
|
|
|
- 然后你就在第一个数据页对应的缓存页里,按照上述办法查找
|
|
|
|
|
- 假设是根据主键查找的,你可以在数据页的页目录里二分查找,假设你要是根据其他字段查找的,只能是根据数据页内部的单向链表来遍历查找
|
|
|
|
|
![页目录2](pic/页目录2.png)
|
|
|
|
|
- 假设第一个数据页没找到你要的那条数据
|
|
|
|
|
- 只能根据数据页的双向链表去找下一个数据页,然后读取到buffer pool的缓存页里去,然后按一样的方法在一个缓存页内部查找那条数据。
|
|
|
|
|
- 如果依然还是查找不到呢?
|
|
|
|
|
- 那只能根据双向链表继续加载下一个数据页到缓存页里来了,以此类推,循环往复。
|
|
|
|
|
- 你似乎是在做一个数据库里很尴尬的操作:全表扫描?
|
|
|
|
|
|
|
|
|
|
## 66. 不断在表中插入数据时,物理存储是如何进行页分裂的?
|
|
|
|
|
- 我们在一个表里不停的插入数据的时候,会涉及到一个页分裂的过程,也就是说,这个表里是如何出现一个又一个的数据页的。
|
|
|
|
|
- 正常情况下我们在一个表里插入一些数据后,他们都会进入到一个数据页里去,在数据页内部,他们会组成一个单向链表,这个数据页内部的单向链表大致如下所示
|
|
|
|
|
![页分裂1](pic/页分裂1.png)
|
|
|
|
|
- 里面就是一行一行的数据,刚开始第一行是个起始行,他的行类型是2,就是最小的一行
|
|
|
|
|
- 他有一个指针指向了下一行数据,每一行数据都有自己每个字段的值,然后每一行通过一个指针不停的指向下一行数据
|
|
|
|
|
- 普通的数据行的类型都是0,最后一行是一个类型为3的,就是代表最大的一行。
|
|
|
|
|
- 假设你不停的在表里插入数据,那么刚开始是不是就是不停的在一个数据页插入数据?接着数据越来越多,越来越多,此时就要再搞一个数据页了
|
|
|
|
|
![页分裂2](pic/页分裂2.png)
|
|
|
|
|
- 索引运作的一个核心基础就是要求你后一个数据页的主键值都大于前面一个数据页的主键值
|
|
|
|
|
- 但是如果你的主键是自增的,那还可以保证这一点,因为你新插入后一个数据页的主键值一定都大于前一个数据页的主键值
|
|
|
|
|
- 但是有时候你的主键并不是自增长的,所以可能会出现你后一个数据页的主键值里,有的主键是小于前一个数据页的主键值的
|
|
|
|
|
- 比如在第一个数据页里有一条数据的主键是10,第二个数据页里居然有一条数据的主键值是8,那此时肯定有问题了。
|
|
|
|
|
- 所以此时就会出现一个过程,叫做页分裂
|
|
|
|
|
- 就是万一你的主键值都是你自己设置的,那么在增加一个新的数据页的时候,实际上会把前一个数据页里主键值较大的,挪动到新的数据页里来,然后把你新插入
|
|
|
|
|
的主键值较小的数据挪动到上一个数据页里去,保证新数据页里的主键值一定都比上一个数据页里的主键值大。
|
|
|
|
|
- 假设新数据页里,有两条数据的主键值明显是小于上一个数据页的主键值的
|
|
|
|
|
![页分裂3](pic/页分裂3.png)
|
|
|
|
|
- 第一个数据页里有1、5、6三条数据,第二个数据页里有2、3、4三条数据,明显第二个数据页里的数据的主键值比第一个数据页里的5和6两个主键都小,所以这个是不行的
|
|
|
|
|
- 此时就会出现页分裂的行为,把新数据页里的两条数据挪动到上一个数据页,上一个数据页里挪两条数据到新数据页里去
|
|
|
|
|
![页分裂4](pic/页分裂4.png)
|
|
|
|
|
- 这就是一个页分裂的过程,核心目标就是保证下一个数据页里的主键值都比上一个数据页里的主键值要大。
|
|
|
|
|
- 保证了每个数据页的主键值,就能为后续的 索引打下基础
|
|
|
|
|
|
|
|
|
|
## 67. 基于主键的索引是如何设计的,以及如何根据主键索引查询?
|
|
|
|
|
- 搜id=4的数据,你怎么知道在哪个数据页里?没有任何证据可以告诉你他到底是在哪个数据页里,也就只能全表扫描了
|
|
|
|
|
- 针对主键的索引实际上就是主键目录,这个主键目录呢,就是把每个数据页的页号,还有数据页里最小的主键值放在一起,组成一个索引的目录
|
|
|
|
|
![主键索引1](pic/主键索引1.png)
|
|
|
|
|
- 有了主键目录,直接就可以到主键目录里去搜索,比如你要找id=3的数据,此时就会跟每个数据页的最小主键来比,首先id=3大于了数据页2里的最小主键值1,接着小于了数据页8
|
|
|
|
|
里的最小主键值4。
|
|
|
|
|
- 直接就可以定位到id=3的数据一定是在数据页2里的
|
|
|
|
|
- 假设你有很多的数据页,在主键目录里就会有很多的数据页和最小主键值,此时你完全可以根据二分查找的方式来找你要找的id到底在哪个数据页里
|
|
|
|
|
- 数据页都是一坨一坨的连续数据放在很多磁盘文件里的,所以只要你能够根据主键索引定位到数据所在的数据页,此时假设我们有别的方式存储了数据页跟磁盘文件的对应关系,此时你
|
|
|
|
|
就可以找到一个磁盘文件。
|
|
|
|
|
- 假设数据页在磁盘文件里的位置也就是offset偏移量,你也是可以知道的,此时就可以直接通过随机读的方式定位到磁盘文件的某个offset偏移量的位置,然后就可以读取连续的一大坨数据页了
|
|
|
|
|
|
|
|
|
|
## 68. 索引的页存储物理结构,是如何用B+树来实现的?
|
|
|
|
|
- 解决几百万,几千万,甚至单表几亿条数据,所以此时可能有大量的数据页,主键目录明显不够用
|
|
|
|
|
- 在考虑这个问题的时候,实际上是采取了一种把索引数据存储在数据页里的方式来做的
|
|
|
|
|
- 也就是说,你的表的实际数据是存放在数据页里的,然后你表的索引其实也是存放在页里的,此时索引放在页里之后,就会有索引页,假设你有很多很多的数据页,那么此时你就可以有很多的索引页
|
|
|
|
|
![主键索引2](pic/主键索引2.png)
|
|
|
|
|
- 你现在有很多索引页,但是此时你需要知道,你应该到哪个索引页里去找你的主键数据,是索引页20?还是索引页28?这也是个大问题
|
|
|
|
|
- 于是接下来我们又可以把索引页多加一个层级出来,在更高的索引层级里,保存了每个索引页和索引页里的最小主键值,如下图所示
|
|
|
|
|
![主键索引3](pic/主键索引3.png)
|
|
|
|
|
- 假设我们要查找id=46的,直接先到最顶层的索引页35里去找,直接通过二分查找可以定位到下一步应该到索引页20里去找,接下来到索引页20里通过二分查找定位,也很快可以定位到数据应
|
|
|
|
|
该在数据页8里,再进入数据页8里,就可以找到id=46的那行数据了。
|
|
|
|
|
- 问题再次来了,假如你最顶层的那个索引页里存放的下层索引页的页号也太多了,怎么办呢?
|
|
|
|
|
- 此时可以再次分裂,再加一层索引页,比如下面图里那样子
|
|
|
|
|
![主键索引4](pic/主键索引4.png)
|
|
|
|
|
- 这就是一颗B+树,属于数据结构里的一种树形数据结构,所以一直说MySQL的索引是用B+树来组成的,其实就是这个意思。
|
|
|
|
|
- 当你为一个表的主键建立起来索引之后,其实这个主键的索引就是一颗B+树,然后当你要根据主键来查数据的时候,直接就是从B+树的顶层开始二分查找,一层
|
|
|
|
|
一层往下定位,最终一直定位到一个数据页里,在数据页内部的目录里二分查找,找到那条数据
|
|
|
|
|
- 这就是索引最真实的物理存储结构,采用跟数据页一样的页结构来存储,一个索引就是很多页组成的一颗B+树
|
|
|
|
|
|
|
|
|
|
## 69. 更新数据的时候,自动维护的聚簇索引到底是什么?
|
|
|
|
|
![主键索引4](pic/主键索引4.png)
|
|
|
|
|
- 首先呢,现在假设我们要搜索一个主键id对应的行,此时你就应该先去顶层的索引页88里去找,通过二分查找的方式,很容易就定位到你应该去下层哪个索引页里继续找
|
|
|
|
|
- 比如现在定位到了下层的索引页35里去继续找,此时在索引页35里也有一些索引条目的,分别都是下层各个索引页(20,28,59)和他们里面最小的主键值,
|
|
|
|
|
此时在索引页35的索引条目里继续二分查找,很容易就定位到,应该再到下层的哪个索引页里去继续找,
|
|
|
|
|
- 可能从索引页35接着就找到下层的索引页59里去了,此时索引页59里肯定也是有索引条目的,这里就存放了部分数据页页号(比如数据页2和数据页8)和每个数据页里最小的主键值
|
|
|
|
|
- 此时就在这里继续二分查找,就可以定位到应该到哪个数据页里去找
|
|
|
|
|
- 接着比如进入了数据页2,里面就有一个页目录,都存放了各行数据的主键值和行的实际物理位置
|
|
|
|
|
- 此时在这里直接二分查找,就可以快速定位到你要搜索的主键值对应行的物理位置,然后直接在数据页2里找到那条数据即可了。
|
|
|
|
|
- PS: 其实最下层的索引页,都是会有指针引用数据页的,所以实际上索引页之间跟数据页之间是有指针连接起来的
|
|
|
|
|
- PS: 其实索引页自己内部,对于一个层级内的索引页,互相之间都是基于指针组成双向链表的
|
|
|
|
|
- 总结: 假设你把索引页和数据页综合起来看, 他们都是连接在一起的,看起来就如同一颗完整的大的B+树一样,从根索引页88开始,一直到所有的
|
|
|
|
|
数据页,其实组成了一颗巨大的B+树。在这颗B+树里,最底层的一层就是数据页,数据页也就是B+树里的叶子节点了!
|
|
|
|
|
- 所以,如果一颗大的B+树索引数据结构里,叶子节点就是数据页自己本身,那么此时我们就可以称这颗B+树索引为聚簇索引
|
|
|
|
|
- 上图中所有的索引页+数据页组成的B+树就是聚簇索引!
|
|
|
|
|
- 在InnoDB存储引擎里,你在对数据增删改的时候,就是直接把你的数据页放在聚簇索引里的,数据就在聚簇索引里,聚簇索引就包含了数据!比如你插入数据,那么就是在数据页里插入数据
|
|
|
|
|
- 如果你的数据页开始进行页分裂了,他此时会调整各个数据页内部的行数据,保证数据页内的主键值都是有顺序的,下一个数据页的所有主键值大于上一个数据页的所有主键值
|
|
|
|
|
- 同时在页分裂的时候,会维护你的上层索引数据结构,在上层索引页里维护你的索引条目,不同的数据页和最小主键值
|
|
|
|
|
- 然后如果你的数据页越来越多,一个索引页放不下了,此时就会再拉出新的索引页,同时再搞一个上层的索引页,上层索引页里存放的索引条目就是下层索引页页号和最下主键值。
|
|
|
|
|
- 按照这个顺序,以此类推,如果你的数据量越大,此时可能就会多出更多的索引页层级来,不过说实话,一般索引页里可以放很多索引条目,所以通常而言,即使你是亿级的大表,基本上大表里建的索引
|
|
|
|
|
的层级也就三四层而已。
|
|
|
|
|
- 这个聚簇索引默认是按照主键来组织的,所以你在增删改数据的时候,一方面会更新数据页,一方面其实会给你自动维护B+树结构的聚簇索引,给新增和更新索引页,这个聚簇索引是默认就会给你建立的
|
|
|
|
|
|
|
|
|
|
## 70. 针对主键之外的字段建立的二级索引,又是如何运作的?
|
|
|
|
|
- 假设你要是针对其他字段建立索引,比如name、age之类的字段,这都是一样的原理,简单来说,比如你插入数据的时候,一方面会把完整数据插入到聚簇索引的叶子节点的数据页里去,同时维护
|
|
|
|
|
好聚簇索引,另一方面会为你其他字段建立的索引,重新再建立一颗B+树。
|
|
|
|
|
- 比如你基于name字段建立了一个索引,那么此时你插入数据的时候,就会重新搞一颗B+树,B+树的叶子节点也是数据页,但是这个数据页里仅仅放主键字段和name字段
|
|
|
|
|
- 独立于聚簇索引之外的另外一个索引B+树了,严格来说是name字段的索引B+树,所以在name字段的索引B+树里,叶子节点的数据页里仅仅放主键和name字段的值,至于排序规则之类
|
|
|
|
|
的,都是跟以前说的一样的。
|
|
|
|
|
- 假设你要根据name字段来搜索数据,那搜索过程简直都一样了,不就是从name字段的索引B+树里的根节点开始找,一层一层往下找,一直找到叶子节点的数据页里,定位到name字段值对应的主键值
|
|
|
|
|
- 此时针对select * from table where name='xx'这样的语句,你先根据name字段值在name字段的索引B+树里找,找到叶子节点也仅仅可以找到对应的主键值,而找不到这行数据完整的所有字段。
|
|
|
|
|
- 所以此时还需要进行“回表”,这个回表,就是说还需要根据主键值,再到聚簇索引里从根节点开始,一路找到叶子节点的数据页,定位到主键对应的完整数据行,此时才能把select *要的全部字段值都拿出来
|
|
|
|
|
- 因为我们根据name字段的索引B+树找到主键之后,还要根据主键去聚簇索引里找,所以一般把name字段这种普通字段的索引称之为二级索引,一级索引就是聚簇索引,这就是普通字段的索引的运行原理。
|
|
|
|
|
- 也可以把多个字段联合起来,建立联合索引,比如name+age
|
|
|
|
|
- 此时联合索引的运行原理也是一样的,只不过是建立一颗独立的B+树,叶子节点的数据页里放了id+name+age,然后默认按照name排序,name一样就按照age排序,不同数据页之间的name+age值
|
|
|
|
|
的排序也如此。
|
|
|
|
|
- 总结: innodb存储引擎的索引的完整实现原理了,其实大家一步一步看下来,会发现索引这块知识也没那么难,不过就是建立B+树,根据B+树一层一层二分查找罢了,然后不同的索引就是建立不同的
|
|
|
|
|
B+树,然后你增删改的时候,一方面在数据页里更新数据,一方面就是维护你所有的索引。后续查询,你就要尽量根据索引来查询。
|
|
|
|
|
|
|
|
|
|
## 71. 插入数据时到底是如何维护好不同索引的B+树的?
|
|
|
|
|
- 其实刚开始你一个表搞出来以后,其实他就一个数据页,这个数据页就是属于聚簇索引的一部分,而且目前还是空的
|
|
|
|
|
- 此时如果你插入数据,就是直接在这个数据页里插入就可以了,也没必要给他弄什么索引页
|
|
|
|
|
- 这个初始的数据页其实就是一个根页,每个数据页内部默认就有一个基于主键的页目录,所以此时你根据主键来搜索都是ok没有问题的,直接在唯一 一个数据页里根据页目录找就行了
|
|
|
|
|
- 然后你表里的数据越来越多了,此时你的数据页满了,那么就会搞一个新的数据页,然后把你根页面里的数据都拷贝过去,同时再搞一个新的数据页,根据你的主键值的大小进行挪动,让两个新的数据页根
|
|
|
|
|
据主键值排序,第二个数据页的主键值都大于第一个数据页的主键值
|
|
|
|
|
- 那么此时那个根页在哪儿呢?
|
|
|
|
|
- 此时根页就升级为索引页了,这个根页里放的是两个数据页的页号和他们里面最小的主键值,根页就成为了索引页,引用了两个数据页
|
|
|
|
|
- 接着你肯定会不停的在表里灌入数据,然后数据页不停的页分裂,分裂出来越来越多的数据页
|
|
|
|
|
- 此时你的唯一 一个索引页,也就是根页里存放的数据页索引条目越来越多,连你的索引页都放不下了,那你就让一个索引页分裂成两个索引页,然后根页继续往上走一个层级引用了两个索引页
|
|
|
|
|
- 接着就是依次类推了,你的数据页越来越多,那么根页指向的索引页也会不停分裂,分裂出更多的索引页,当你下层的索引页数量太多的时候,会导致你的根页指向的索引页太多了,此时根页继续分裂成多
|
|
|
|
|
个索引页,根页再次往上提上去去一个层级。
|
|
|
|
|
- 这其实就是你增删改的时候,整个聚簇索引维护的一个过程,其实其他的二级索引也是类似的一个原理
|
|
|
|
|
- 比如你name字段有一个索引,那么刚开始的时候你插入数据,一方面在聚簇索引的唯一的数据页里插入,一方面在name字段的索引B+树唯一的数据页里插入。
|
|
|
|
|
- 然后后续数据越来越多了,你的name字段的索引B+树里唯一的数据页也会分裂,整个分裂的过程跟上面说的是一样的,所以你插入数据的时候,本身就会自动去维护你的各个索引的B+树。
|
|
|
|
|
- 你的name字段的索引B+树里的索引页中,其实除了存放页号和最小name字段值以外,每个索引页里还会存放那个最小name字段值对应的主键值
|
|
|
|
|
- 这是因为有时候会出现多个索引页指向的下层页号的最小name字段值是一样的,此时就必须根据主键判断一下。
|
|
|
|
|
- 新的name字段值肯定是插入到主键值较大的那个数据页里去的。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 查询语句的执行原理
|
|
|
|
|
|
|
|
|
|
# 多表join语句的执行原理
|
|
|
|
|
|
|
|
|
|
# MySQL执行计划
|
|
|
|
|
|
|
|
|
|
# SQL语句调优
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 高可用及部署
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|