Java 面试八股文之数据库篇(二)
前言
这是系列文章【 Java 面试八股文】数据库篇的第二期。
【 Java 面试八股文】系列会陆续更新 Java 面试中的高频问题,旨在从问题出发,理解 Java 基础,数据结构与算法,数据库,常用框架等。该系列前几期文章可以通过点击文末给出的链接进行查看~
按照惯例——首先要做几点说明:
【 Java 面试八股文】中的面试题来源于社区论坛,书籍等资源;感谢使我读到这些宝贵面经的作者们。
对于【 Java 面试八股文】中的每个问题,我都会尽可能地写出我自己认为的“完美解答”。但是毕竟我的身份不是一个“真理持有者”,只是一个秉承着开源分享精神的 “knowledge transmitter” & 菜鸡,所以,如果这些答案出现了错误,可以留言写出你认为更好的解答,并指正我。非常感谢您的分享。
知识在于“融释贯通”,而非“死记硬背”;现在市面上固然有很多类似于“Java 面试必考 300 题” 这类的文章,但是普遍上都是糟粕,仅讲述其果,而不追其源;希望我的【 Java 面试八股文】可以让你知其然,且知其所以然~
那么,废话不多说,我们正式开始吧!
往期文章
数据库篇(二)
1、如何定位并优化慢查询 sql?
答
我们从定位到慢查询 sql,再通过分析并进行优化的顺序如下:
通过开启慢日志定位到慢查询的 sql
使用 explain 工具分析 sql
修改并优化 sql
接下来我们就依次通过以上三个步骤来看一下,一条慢查询 sql 是如何被找到,并优化的。
1. 通过开启慢日志定位到慢查询 sql
首先,我们进入到客户端,输入命令:
命令返回结果如下:
在这里面,我们需要关注三个变量,分别是:slow_query_log
,slow_query_log_file
以及 long-query_time
。
slow_query_log
目前对应的 Value 值为 OFF,代表慢日志并未开启;slow_query_log_file
是记录慢 sql 的文件,当一条查询 sql 的时间超过 long_query_time
时,就会被记录到慢日志文件中,我们看到 long_query_time
的默认值为 10 s。
首先,我们需要开启慢日志,使用命令:
并且,通常我们会修改 long_query_time
的值。因为如果一条查询 sql 的执行时间超过 10 s 才被定义为慢查询的话,一般是不能被接受的。我们可以按照自己的业务需求,设定相应的值,譬如将其设置为 1 s:
重新连接数据库后,就可以看到刚刚设置的值已经生效了:
除了使用命令进行修改,我们也可以通过修改配置文件(my.cnf)对这些变量进行设置,修改配置文件这种方式会使得这些改动永久保存,不会因为重启数据库服务而失效。
这样,我们就可以通过开启慢日志定位到所有超时的慢查询 sql 语句了。
2. 使用 explain 工具分析 sql
explain 是 MySQL 内置的一个命令,可以获取一条语句的执行计划。语法为 explain + 要分析的语句。通过 explain 的分析我们可以知道表的读取顺序,数据读取操作的类型,是否有使用到索引,有无做全表扫描等信息。
目前我有一张学生表:
在慢查询日志文件中,有一条超时的查询语句:
我们可以使用 explain 工具对这条查询语句进行解析:
返回结果如下:
我来解释一下其中一些重点的列分别代表什么含义。
id 代表 select 查询语句的序列号。select_type 表示对应行是简单查询还是复杂查询,譬如,我们这条语句既不包含子查询也没有联合查询,所以被定义为简单查询,显示的信息为 SIMPLE。table 列的含义很简单了,表示我们当前解析的这条 sql 语句访问的是哪一张表。type 列表示 MySQL 查询数据行的方式,从最优到最差分别为:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL。ALL 表示走全表扫描,就意味着 MySQL 需要从头到尾遍历整张表去寻找我们所需要的数据行。所以当我们使用 explain 分析出的结果中,type 列显示的信息为 ALL 时,就需要留意这条查询语句是否有可以优化的空间。possible_keys 列显示查询可能使用哪些索引来查找。key 列显示 MySQL 实际采用哪个索引来优化对该表的访问,如果我们没有使用索引,那么该列的信息则为 NULL。rows 列对应的信息是 MySQL 估计要读取并检测的行数。Extra 列展示的是额外信息,Extra 列如果对应的是以下的两个值则说明我们的语句无法使用索引,效率会受重大影响:第一个是 Using filesort,第二个是 Using temporary。在出现这两个字段时,往往我们都会考虑对 sql 进行优化。
当 Extra 列为 Using filesort 时,表示 MySQL 会对结果使用一个外部索引排序,而不是从表里按照索引次序读到相关内容。可能是在内存或者磁盘上进行排序,MySQL 中无法利用索引完成的排序操作称为 “文件排序”。
而当 Extra 列为 Using temporary 时,表示 MySQL 创建了一张临时表来处理查询。Using teporary 常见于排序操作 order by 和分组查询 group by,当值为 Using temporary 时,sql 查询一般都是需要进行优化的。
3. 修改并优化 sql
解释了这些列表示的含义后,我们再回到 explain 解析的结果:
可以看到,我们的 type 列对应的值为 ALL,说明该查询语句走了全表扫描。
这条 sql 查询优化的方式很简单,我们可以为这张表的 name 字段加上一个索引:
我们再来执行一遍 explain 解析命令:
可以看到,此时的 type 列变为了 index,表示 MySQL 获取数据行的方式是扫描索引树,并且 Extra 列的值也变成了 Using index,表示我们使用了索引的方式来获取结果。
这样我们的慢查询 sql 就得到了优化。
总结
本题的解答中,这个示例非常简单,目的就是为了给大家提供一个思路。首先,我们需要开启慢日志定位并获取慢查询的 sql 语句,然后我们可以使用 explain 命令来对这条 sql 语句进行分析,看这条查询 sql 是否使用到索引,是否创建了临时表等。最后我们通过和业务场景进行结合,就可以分析得到优化 sql 的具体思路。
2、索引创建的越多越好吗?
答
当然不是。
索引可以比作是一本书的目录,当我们想要找到书本中的某些内容时,我们可以先通过目录快速定位到相应的章节,这便大大提升了我们的查找效率。
不过试想一下,如果我们的这本书只有两三页,那就大可不必再为这本书新增一页纸当作目录了。所以,对于一些小型表来说,创建索引是浪费存储空间的。小型表在大部分情况下即便是全表扫描效率也不会太低,所以这种情况下就没必要创建索引。
对于中型表,大型表来说,正确地创建索引的确会提升查询效率,但也绝不是越多越好。我们要知道,数据变更需要维护索引,因此更多的索引就意味着更多的维护成本,同时更多的索引也意味着需要更多的存储空间。
而对于特大型表来说,我们更要谨慎地使用索引,因为使用索引越多维护起来的代价也会越大。实际上,对于特大型表,我们应该考虑的是分库分表的策略,可以使用一些分库分表工具,譬如 sharding-sphere,TDDL,Mycat 等,分散并减轻库与表的压力。
总结
索引越多越好?答案必然是否定的。面试者应该从索引的使用场景,索引的利弊来回答本问题。
3、请谈一下 MySQL 的锁机制?
答
锁机制非常重要,在这个问题下涵盖了许多经常被考察到的面试题,所以接下来,我会用较长的篇幅来回答这个问题并详细解释其中的每个知识点。
数据库锁按照不同的分类可以做如下划分:
按照锁的粒度划分,可分为表级锁,行级锁,页级锁
按照锁的级别划分,可分为共享锁与排它锁
按照加锁的方式划分,可分为自动锁与显示锁
按照使用方式划分,可分为乐观锁,悲观锁
我们都知道 MySQL 的存储引擎是插件式的,不同的存储引擎有不同的锁机制,其中我们最常用到的两个存储引擎为 MyISAM 与 InnoDB,MyISAM 存储引擎采用的是表级锁(table-level-locking),InnoDB 存储引擎既支持行级锁(row-level-locking)也支持表级锁,但是默认的情况下采用的是行级锁。而我们不常使用到的一种存储引擎—— BDB 采用的是页级锁(page-level-locking),同时 BDB 也支持表级锁,不过,因为现在我们使用到的主流存储引擎几乎都是 MyISAM 与 InnoDB,而且 BDB 现在已经完全被 InnoDB 取代,所以我们对页级锁就不再介绍了,对此感兴趣的童鞋可以自行了解。
那么先来看一下 MyISAM 存储引擎采用的锁机制。
首先,我们要了解共享锁和排它锁的概念。为了不给大家带来太复杂的概念,你可以先这样认为:共享锁等价于读锁(Read Lock),排它锁等价于写锁(Write Lock)。当我们对一张表进行查询操作(select)时,MyISAM 会为这张表自动加上一个读锁;当我们对一张表进行更新操作(insert,update,delete)时,MyISAM 则在这张表上自动加上一个写锁。
共享锁(读锁)与排它锁(写锁)的兼容性如下表所示:
啥意思呢?在有多个用户(线程)对 MyISAM 表进行读操作时,并不会阻塞其他用户对同一表的读请求。所以,我们说读锁和读锁之间是相互兼容的,而其他的方式则是不兼容的。
举个例子🌰:现在我有一张 test 表,指定使用的存储引擎为 MyISAM :
假设,有两个用户正在对表进行查询(读)操作。
用户 A 输入了一条查询 sql :
在用户 A 还没有拿到结果时,用户 B 也输入了一条查询 sql :
此时,用户 B 并不会因为用户 A 还没有拿到结果而进入到线程阻塞的等待状态。原因就是,读锁和读锁是相互兼容的。
如果用户 A 输入了一条查询 sql:
在用户 A 还没有拿到查询结果时,用户 B 输入了一条更新 sql:
而这个时候,用户 B 输入的 update 语句会被阻塞,需要等待 test 表的读锁释放后才可以进行操作,原因就在于写锁是一种排它锁,并不会和其他线程共享。
以上的方式均为 MyISAM 自动加锁完成的,所以这种方式叫做自动锁。
我们也可以手动加锁。譬如有这样的一个场景:
有一个订单表 orders,其中记录着各订单的总金额 total;同时还有一个订单明细表 order_detail,记录着各订单每一类产品的金额小记 subtotal。我们现在有一个需求,就是检查这两个表的金额合计是否相同,那么就需要执行这样两条 sql 语句:
如果我们不先为这两个表加锁,就很有可能出现错误。比如第一个查询语句执行完毕并返回了结果,然后再对 orders 表做一个更新,那么两次查询的结果就不一致了。
为了避免这种情况的发生,我们需要在这两条查询语句的前后为这两个表手动加读锁,然后再释放:
对于 MyISAM 存储引擎,手动加锁的方式就是:
而锁的粒度是在整张表上的。
我们再来看一下 InnoDB 存储引擎的锁机制。
首先,我们创建一张使用 InnoDB 引擎的测试表 test2:
InnoDB 存储引擎支持事务,并且在默认的情况下是自动提交事务的,它的机制是为同一批事务提交之前加锁,然后 commit 后再一起释放。这里面我为了复现 InnoDB 锁的机制,对当前的几个 MySQL 客户端窗口进行了设置:
我们将 autocommit 设置为 0,这样就可以关闭事务的自动提交。
现在假设,用户 A 执行了一条 sql 查询语句:
InnoDB 默认的锁是行级锁,lock in share mode
表示我们对 test2 表中 id = 3 的行加了一个共享锁,如果要加排它锁,我们可以使用 for update
。现在用户 A 还没有执行 commit;用户 B 执行了一条语句:
此时,我们发现,这条语句是可以执行成功的,并没有发生阻塞:
如果用户 B 执行的语句为:
那么,用户 B 将进入阻塞等待状态。
这便验证了 InnoDB 默认加锁的粒度是行级锁——不过,有个前提条件,那就是查询的字段上必须有索引,如果没有索引 InnoDB 还是会对整个表加锁。
再举个例子:
test2 表的数据内容如下:
假如用户 A 执行了一条 sql 语句:
还未 commit,用户 B 执行了一条 sql 语句:
此时,用户 B 被阻塞。
我们看到,用户 A 在查询 id 为 9 的行,而用户 B 则在更新 id 为 3 的行,如果 InnoDB 走的是行级锁,那么用户 A 对第九行加了一个共享锁,用户 B 对第三行加了一个排它锁,理应是不会互相阻塞的,可是用户 B 仍然被阻塞了。这说明,当我们的 sql 语句操作字段没有走索引时,InnoDB 还是会在整个表这个粒度上加锁。所以说,InnoDB 既支持行级锁,也支持表级锁。
那么行级锁一定要比表级锁好么?
其实不然,行级锁的并发度虽然比表级锁要高,但是表级锁的开销比较小,加锁的速度很快;行级锁的开销则比较大,并且加锁的速度慢,最重要的是行级锁可能会出现死锁现象。
什么是死锁(Dead Lock)?
死锁是指两个或两个以上的进程或线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
如上图中所示,线程 A 持有锁 A,线程 B 持有锁 B。两个线程处于争抢状态,线程 A 等待线程 B 释放锁,线程 B 又等待线程 A 释放它的资源,这样相互等待就形成了死锁。
形成死锁必须满足四个必要条件:
互斥条件
请求与保持条件
不剥夺条件
环路等待条件
我们接下来对这些名字逐一进行解释。
互斥条件是指进程对所分配到的资源进行排它性使用。说白了就是在一段时间内,某个资源每次只能被一个进程所占用。
请求与保持条件是指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放,说人话就是“有了碗里的饼,还嚷嚷着要吃锅里的汤”。
不剥夺条件是指进程已获得的资源在未使用完之前,不能被剥夺,只能由自己释放,说人话就是“自己拿到了就不给别人用,自己用完了才给别人”。
环路等待条件是指当发生死锁时,必然存在着一个资源的请求环形链,若干进程在这个环形链中循环等待。
死锁形成的条件以上四点缺一不可!接下来,我们尝试依据这些条件写一个死锁的案例:
创建一张表 dead_lock_test,建表语句如下:
Session1 我们输入如下 sql:
Session2 我们输入如下 sql:
我们在 Session1 和 Session2 上按照 T1 ~ T4 的顺序执行上面的 sql,Session2 客户端最后显示的报错信息为:
可以看到,InnoDB 行级锁有可能导致死锁现象的发生,不过 MyISAM 则不会出现死锁,因为 MyISAM 只有表锁,并不满足死锁出现的四个必要条件。
避免死锁有以下几种常见的策略:
设置获得锁的超时时间
避免长事务
避免事务中的用户交互
降低隔离级别
... ...
讲完了什么是死锁后,我们再来看一下什么是乐观锁,什么是悲观锁?
锁按照使用的方式划分即可分为乐观锁与悲观锁。
先讲一下什么是悲观锁。悲观锁指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。
悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
乐观锁则相比于悲观锁来说使用了更加宽松的机制,它往往不依赖于数据库提供的锁机制,而是使用程序人为地实现。我来举个例子🌰:
我们有一个表:
在这个表中有一个 version 字段,version 字段维护的是一个数据版本信息,我们每一次写数据时,都会更新 version 字段;每一次读数据时,都会查看当前的 version 是否已经更新。
test_optimistic_lock 表中现有数据如下:
现在有一个进程 A 要对 id 为 2 的行的 money 做更新,将其值设置为 1000,他便会使用这样的语句:
可以看到,我们的策略是在每次对该表写时,为了防止发生冲突,都会先去检查 version 再做更新操作,如果更新成功的话,便让 version + 1。
目前表中数据更新如下:
如果进程 B 也要对 id 为 2 的行的 money 做更新,将其值设置为 2000,执行 sql 如下:
很显然,因为第二行的 version 值已经更新到了 1 ,这条语句是无法执行成功的。
我们使用了一种宽松的机制,改变了锁的方式。悲观锁“悲观地”认为数据访问一定会被外界所修改,所以在 commit 之前就加好了锁。这样虽然安全得到了保障,但是也带来了许多问题,譬如数据库性能会造成很大的开销(尤其在长事务),有可能出现死锁等。而乐观锁则采用了一种“乐观”的方式,它只有在数据 commit 时,才会进行排它性的检查。不过乐观锁也不是没有缺点,乐观锁适用于写操作比较少的情况,即冲突很少发生的情况。如果经常发生冲突的话,使用乐观锁反而会影响性能,降低系统的吞吐量。
总结
本题又是一个涵盖了巨多知识点的面试题,大家可以从我的解答中找到很多面试题的 answer,譬如:
MyISAM 与 InnoDB 的锁机制有什么区别?
什么是共享锁?什么是排它锁?
InnoDB 在什么时候使用行级锁?什么时候使用表级锁?
什么是死锁?死锁的四个必要条件是什么?
请写出一个死锁的案例?如何避免死锁?
什么是乐观锁,什么是悲观锁?
等等...
如果你仔细阅读了本文,相信你一定可以从文章中找到这些问题的所有答案~
4、MyISAM 与 InnoDB 存储引擎有什么不同?如何选择?
答
其实我们从数据库篇开始,就一直在总结 MyISAM 与 InnoDB 这两个存储引擎的不同了,这里我来给大家总结一下:
除了以上的不同之处外,还有一点需要特殊说明一下。那就是 InnoDB 存储引擎不会保存表的具体行数,而 MyISAM 则使用了一个变量来保存整张表有多少行。
所以,当我们执行:
时,该语句没有任何的 where 条件。InnoDB 执行的速度要比 MyISAM 慢很多,因为 InnoDB 需要走全表扫描来计算数据共有多少行~
那么对于 MyISAM 与 InnoDB 这两种存储引擎,我们该如何选择呢?
MyISAM 适合的场景为:
需要频繁执行全表 count 语句
对数据进行增删改的频率不高,查询非常频繁
没有事务
第一点我们已经解释过了;第二点是因为 MyISAM 使用的是表级锁,如果增删改操作频繁,那么就要频繁地对整张表加锁,这个性能开销无疑是巨大的。而如果更新操作频率不高,MyISAM 相比于 InnoDB 还有一个优势,那就是 MyISAM 使用的索引是非聚簇索引,不用像 InnoDB 的普通索引查询那样需要进行会回表。第三点也无需解释,MyISAM 不支持事务,如果你的业务需求需要事务,就应该使用 InnoDB 存储引擎。
InnoDB 适合的场景为:
数据增删改查都比较频繁
可靠性要求高,需要支持事务
这两点我就不再赘述了,相信大家都能理解:-)
总结
这是一道很好的前菜,面试官可以通过 MyISAM 与 InnoDB 的这些不同之处,向面试者继续深入地提问二者索引的区别,锁的区别等。
总结
又是不知不觉写了快 7000 多字了......
本来想在这一篇文章中将 MySQL 事务相关的面试问题全部写完的,不过这样写下去可能就要 2 万字了......
总之,后续内容我会抓紧更新,感谢您的阅读!
好啦,至此为止,这篇文章就到这里了~欢迎大家关注我的公众号,在这里希望你可以收获更多的知识,我们下一期再见!
版权声明: 本文为 InfoQ 作者【Dobbykim】的原创文章。
原文链接:【http://xie.infoq.cn/article/4174899a1cab52a576a149cf4】。文章转载请联系作者。
评论