MySQL知识面试复盘(二)

本文主要是MySQL知识点的一些复盘,在面试中也被问道了。
第一篇见下链接:

MySQL知识面试复盘(一)

一、什么是数据库连接池?为什么需要建立连接池?

(一)什么是数据库连接池?

数据库连接池指在程序初始化时创建一定数量的数据库连接对象并将其保存在一块内存区中,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个;释放空闲时间超过最大空闲时间的数据库连接以避免因为没有释放数据库连接而引起的数据库连接遗漏。

即在程序初始化的时候创建一定数量的数据库连接,用完可以放回去,下一个在接着用,通过配置连接池的参数来控制连接池中的初始连接数、最小连接、最大连接、最大空闲时间这些参数保证访问数据库的数量在一定可控制的范围类,防止系统崩溃,使用户的体验好

(二)为什么需要建立连接池?

数据库连接是一种关键、有限且昂贵的资源,创建和释放数据库连接是一个很耗时的操作,频繁地进行这样的操作将占用大量的性能开销,进而导致网站的响应速度下降,严重的时候可能导致服务器崩溃;数据库连接池可以节省系统许多开销

数据库连接本质是一个 socket 连接。数据库服务端还要维护一些缓存和用户权限信息之等占用了一些内存。可以把数据库连接池是看做是维护的数据库连接的缓存,以便将来需要对数据库的请求时可以重用这些连接。为每个用户打开和维护数据库连接,尤其是对动态数据库驱动的网站应用程序的请求,既昂贵又浪费资源。 在连接池中,创建连接后,将其放置在池中,并再次使用它,因此不必建立新的连接。如果使用了所有连接,则会建立一个新连接并将其添加到池中。 连接池还减少了用户必须等待建立与数据库的连接的时间。

二、一条SQL语句在MySQL中如何执行?

(一)MySQL的架构

MySQL 主要分为 Server 层和存储引擎层:

  • Server 层:主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binglog 日志模块。
  • 存储引擎: 主要负责数据的存储和读取,采用可以替换的插件式架构,支持 InnoDB、MyISAM、Memory 等多个存储引擎,其中 InnoDB 引擎有自有的日志模块 redolog 模块。现在最常用的存储引擎是 InnoDB,从 MySQL 5.5.5 版本开始就被当做默认存储引擎。

Server 层基本组件介绍:

1)连接器: 身份认证和权限相关;

主要负责用户登录数据库,进行用户的身份认证,包括校验账户密码,权限等操作,如果用户账户密码已通过,连接器会到权限表中查询该用户的所有权限,之后在这个连接里的权限逻辑判断都是会依赖此时读取到的权限数据,也就是说,后续只要这个连接不断开,即时管理员修改了该用户的权限,该用户也是不受影响的。

2)查询缓存: 执行查询语句的时候,会先查询缓存;

MySQL 查询不建议使用缓存,因为查询缓存失效在实际业务场景中可能会非常频繁。在MySQL 8.0 版本后移除,因为这个功能不太实用。

3)分析器: 分析 SQL 语句的作用(select/update),检查语法是否正确;

第一步:词法分析,一条 SQL 语句有多个字符串组成,首先要提取关键字,比如 select,提出查询的表,提出字段名,提出查询条件等等。做完这些操作后,就会进入第二步。

第二步:语法分析,主要就是判断你输入的 sql 是否正确,是否符合 MySQL 的语法。

4)优化器: 按照 MySQL 认为最优的方案去执行;

优化器的作用就是它认为的最优的执行方案去执行(有时候可能也不是最优,这篇文章涉及对这部分知识的深入讲解),比如多个索引的时候该如何选择索引,多表查询的时候如何选择关联顺序等

5)执行器: 执行语句,然后从存储引擎返回数据;

当选择了执行方案后,MySQL 就准备开始执行了,首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会去调用引擎的接口,返回接口执行的结果。

(二)一条SQL语句如何执行?

sql 可以分为两种,一种是查询,一种是更新(增加,更新,删除),下面分开说明:

1、查询语句:
  • 先检查该语句是否有权限,如果没有权限,直接返回错误信息,如果有权限,在 MySQL8.0 版本以前,会先查询缓存,如果有直接缓存,如果没有,执行下一步。
  • 通过分析器进行词法分析,提取 sql 语句的关键元素,然后判断这个 sql 语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。
  • 接下来就是优化器进行确定执行方案,优化器根据自己的优化算法进行选择执行效率最好的一个方案(优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。
  • 进行权限校验,如果没有权限就会返回错误信息,如果有权限就会调用数据库引擎接口,返回引擎的执行结果。
2、更新语句:
  • 先查询到需要更新的这条数据,如果有缓存,也是会用到缓存;
  • 然后拿到查询的语句,把值更改为新值,然后调用引擎 API 接口,写入这一行数据,InnoDB 引擎把数据保存在内存中,同时记录 redo log,此时 redo log 进入prepare 状态,然后告诉执行器,执行完成了,随时可以提交。
  • 执行器收到通知后记录 binlog,然后调用引擎接口,提交 redo log 为提交状态。
  • 更新完成。

更新语句基本上会沿着上一个查询的流程走,只不过执行更新的时候要记录日志,MySQL 自带日志模块式 binlog(归档日志),所有的存储引擎都可以使用,常用的 InnoDB 引擎还自带了一个日志模块 redo log(重做日志)。

(三)为什么要用两个日志模块?

binlog 日志只能用来归档。redo log 是 InnoDB 引擎特有的,这就导致会没有 crash-safe 的能力。

crash-safe 的能力即使数据库发生异常重启,之前提交的记录都不会丢失

并不是说只用一个日志模块不可以,只是 InnoDB 引擎就是通过 redo log 来支持事务的。

(四)为什么 redo log 要引入 prepare 预提交状态?
  • 如果先写 redo log 直接提交,然后写 binlog,假设写完 redo log 后,机器挂了,binlog 日志没有被写入,那么机器重启后,这台机器会通过 redo log 恢复数据,但是这个时候 bingog 并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。
  • 如果先写 binlog,然后写 redo log,假设写完了 binlog,机器异常重启了,由于没有 redo log,本机是无法恢复这一条记录的,但是 binlog 又有记录,那么就会产生数据不一致的情况。

如果采用 redo log 两阶段提交的方式,写完 binglog 后,然后再提交 redo log 就能保证数据的一致性。

假设 redo log 处于预提交状态,binglog 也已经写完了,这个时候发生了异常重启会怎么样呢? MySQL 有自己的处理机制,MySQL 的处理过程如下:

  • 判断 redo log 是否完整,如果判断是完整的,就立即提交。
  • 如果 redo log 只是预提交但不是 commit 状态,这个时候就会去判断 binlog 是否完整,如果完整就提交 redo log, 不完整就回滚事务。

这样就解决了数据一致性的问题。

三、一条SQL语句执行得很慢的原因有哪些?

一条SQL语句执行很慢的原因需要分情况来讨论:

  • 第一种:大多数情况是正常的,只是偶尔很慢;
  • 第二种:在数据量不变的情况下,一直都执行的很慢;
(一)第一种:偶尔很慢的情况

偶尔很慢说明语句本身没有问题,有可能是以下的情况:

(1)数据库在刷新脏页(flush)

当要往数据库插入一条数据、或者要更新一条数据的时候,数据库会在内存中把对应字段的数据更新了,但是更新之后,这些更新的字段并不会马上同步持久化到磁盘中去,而是把这些更新的记录写入到 redo log 日记中去,等到空闲的时候,在通过 redo log 里的日记把最新的数据同步到磁盘中去。

当内存数据页跟磁盘数据页内容不一致的时候,称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘上的数据页的内容就一致了,称为“干净页”。

刷脏页有4种场景(后两种不用太关注“性能”问题):
1)redolog写满了: redo log 里的容量是有限的,如果数据库一直很忙,更新又很频繁,这个时候 redo log 很快就会被写满了,这个时候就没办法等到空闲的时候再把数据同步到磁盘的,只能暂停其他操作,全身心来把数据同步到磁盘中去的,这个时候,就会导致平时正常的SQL语句突然执行的很慢。
一句话总结:数据库在在同步数据到磁盘的时候,就有可能导致我们的SQL语句执行的很慢了。
2)内存不够用了: 如果一次查询较多的数据,恰好碰到所查数据页不在内存中时,需要申请内存,而此时恰好内存不足的时候就需要淘汰一部分内存数据页,如果是干净页,就直接释放,如果恰好是脏页就需要刷脏页。
3)MySQL 认为系统“空闲”的时候: 这时系统没什么压力。
4)MySQL 正常关闭的时候: MySQL 会把内存的脏页都 flush 到磁盘上,这样下次 MySQL 启动的时候,就可以直接从磁盘上读数据,启动速度会很快。

(2)拿不到锁

要执行的这条语句,刚好这条语句涉及到的表,别人在用,并且加锁了,拿不到锁,只能慢慢等待别人释放锁了。或者,表没有加锁,但要使用到的某个一行被加锁了,这个时候就只能等待了,导致SQL语句很慢。

如果要判断是否真的在等待锁,可以用 show processlist 这个命令来查看当前的状态

(二)第二种:一直很慢的情况

如果在数据量一样大的情况下,这条 SQL 语句每次都执行的这么慢,那可能是语句的问题了:
1、没用到索引

(1)字段没有索引

如果刚好查询的字段上没有索引,那只能走全表扫描了,导致这条查询语句很慢。

(2)字段有索引,但却没有用索引

如果在字段的左边做了运算,在查询的时候,就不会用上索引了。

(3)函数操作导致没有用上索引

如果在查询的时候,对字段进行了函数操作,也是会导致没有用上索引的

2、数据库自己选错了索引

举例说明一下:

select * from t where 100 < c and c < 100000;

上面这条如果走 c 这个字段的索引的话,最后会查询到对应主键的值,然后,再根据主键的值走主键索引,查询到整行数据返回。

然而,就算在 c 字段上有索引,系统也并不一定会走 c 这个字段上的索引,而是有可能会直接扫描扫描全表,找出所有符合 100 < c and c < 100000 的数据。

这是为什么?

系统在执行这条语句的时候,会进行预测:究竟是走 c 索引扫描的行数少,还是直接扫描全表扫描的行数少呢?扫描行数越少当然越好了,因为扫描行数越少,意味着I/O操作的次数越少。

如果是扫描全表的话,那么扫描的次数就是这个表的总行数了,假设为 n;而如果走索引 c 的话,通过索引 c 找到主键之后,还得再通过主键索引来找整行的数据,也就是说,需要走两次索引。而且,也不知道符合 100 c < and c < 10000 这个条件的数据有多少行,万一这个表是全部数据都符合呢?这个时候意味着,走 c 索引不仅扫描的行数是 n,同时还得每行数据走两次索引。

所以呢,系统是有可能走全表扫描而不走索引的。

系统是怎么判断呢?

判断来源于系统的预测,如果要走 c 字段索引的话,系统会预测走 c 字段索引大概需要扫描多少行。如果预测到要扫描的行数很多,它可能就不走索引而直接扫描全表了。

系统是通过索引的区分度来判断的,一个索引上不同的值越多,意味着出现相同数值的索引越少,意味着索引的区分度越高(区分度称之为基数,即区分度越高,基数越大)。所以,基数越大,意味着符合 100 < c and c < 10000 这个条件的行数越少。

所以,一个索引的基数越大,意味着走索引查询越有优势。

怎么知道这个索引的基数呢?

系统当然是不会遍历全部来获得一个索引的基数的,代价太大了,索引系统是通过遍历部分数据,即通过采样的方式,来预测索引的基数的。既然是采样,那就有可能出现失误的情况,也就是说,c 这个索引的基数实际上是很大的,但是采样的时候,把这个索引的基数预测成很小。例如采样的那一部分数据刚好基数很小,然后就误以为索引的基数很小。然后系统就不走 c 索引,直接走全部扫描。

结论:由于统计的失误,导致系统没有走索引,而是走了全表扫描

系统判断是否走索引,扫描行数的预测其实只是原因之一,这条查询语句是否需要使用使用临时表、是否需要排序等也是会影响系统的选择的。

--有时候也可以通过强制走索引的方式来查询,例如
select * from t force index(a) where c < 100 and c < 100000;
--也可以通过
show index from t;
--来查询索引的基数和实际是否符合,如果和实际很不符合的话
--可以重新来统计索引的基数,可以用这条命令:
analyze table t;
--来重新统计分析。

既然会预测错索引的基数,这也意味着,当查询语句有多个索引的时候,系统有可能也会选错索引哦,这也可能是 SQL 执行的很慢的一个原因。

主键索引和非主键索引是有区别的,主键索引存放的值是整行字段的数据,而非主键索引上存放的值不是整行字段的数据,而且存放主键字段的值。

本文知识点来源:

JavaGuide面试突击版

猜你喜欢

转载自blog.csdn.net/qq_42908549/article/details/109399740